5

I have a JSON document where some values can be null. Using for expressions in json4s, how I can yield None, instead of nothing?

The following will fail to yield when the value for either of the fields FormattedID or PlanEstimate is null.

val j: json4s.JValue = ...
for {
  JObject(list) <- j
  JField("FormattedID", JString(id)) <- list
  JField("PlanEstimate", JDouble(points)) <- list
} yield (id, points)

For example:

import org.json4s._
import org.json4s.jackson.JsonMethods._

scala> parse("""{
     |   "FormattedID" : "the id",
     |   "PlanEstimate" : null
     | }""")
res1: org.json4s.JValue = JObject(List((FormattedID,JString(the id)), 
    (PlanEstimate,JNull)))

scala> for {                                      
     | JObject(thing) <- res1                     
     | JField("FormattedID", JString(id)) <- thing
     | } yield id                                 
res2: List[String] = List(the id)

scala> for {                                      
     | JObject(thing) <- res1                     
     | JField("PlanEstimate", JDouble(points)) <- thing
     | } yield points
res3: List[Double] = List()
// Ideally res3 should be List[Option[Double]] = List(None)
Synesso
  • 37,610
  • 35
  • 136
  • 207

4 Answers4

3
scala> object OptExtractors {
     |
     |   // Define a custom extractor for using in for-comprehension.
     |   // It returns Some[Option[Double]], instead of Option[Double].
     |   object JDoubleOpt {
     |     def unapply(e: Any) = e match {
     |       case d: JDouble => Some(JDouble.unapply(d))
     |       case _ => Some(None)
     |     }
     |   }
     | }
defined object OptExtractors

scala>

scala> val j = parse("""{
     |   "FormattedID" : "the id",
     |   "PlanEstimate" : null
     | }""")
j: org.json4s.JValue = JObject(List((FormattedID,JString(the id)), (PlanEstimate,JNull)))

scala>

scala> import OptExtractors._
import OptExtractors._

scala>

scala> for {
     |   JObject(list) <- j
     |   JField("FormattedID", JString(id)) <- list
     |   JField("PlanEstimate", JDoubleOpt(points)) <- list
     | } yield (id, points)
res1: List[(String, Option[Double])] = List((the id,None))
thirstycrow
  • 2,806
  • 15
  • 18
  • Is it somehow possible to deal in same manner with missed JSON fields? I.e. I want to get `None` if field is absent or field value is `null`. – WelcomeTo Feb 19 '16 at 22:53
  • I'm afraid it's not going to work if there is a missing field. In that case, I think it would be better to avoid using for-comprehension. – thirstycrow Feb 23 '16 at 15:20
1

According to the documentation,

Any value can be optional. Field and value is completely removed when it doesn't have a value.

scala> val json = ("name" -> "joe") ~ ("age" -> (None: Option[Int]))

scala> compact(render(json))

res4: String = {"name":"joe"}

Explaining why your for comprehension doesn't yield anything.
Of course, a null value is mapped to None internally.

Mik378
  • 21,881
  • 15
  • 82
  • 180
  • The JSON is fetched & parsed at runtime. The json4s object contains a JNull for this field, so it's not removed - it's just treated as *empty* by the for expression. – Synesso Aug 08 '14 at 13:49
0

The last command should look like:

for {
  JObject(thing) <- res1
} yield thing.collectFirst{case JField("PlanEstimate", JDouble(points)) => points}

Or like

for {
  JObject(thing) <- res1
  points = thing.collectFirst{case JField("PlanEstimate", JDouble(p)) => p}
} yield points
Oleg Rudenko
  • 708
  • 4
  • 12
0

What about this

 for {
      JObject(thing) <- res1      
      x = thing.find(_._1 == "PlanEstimate").flatMap(_._2.toOption.map(_.values))
     } yield x
S.Karthik
  • 1,389
  • 9
  • 21