What's the most elegant way to deconstruct a JSON array with JSON4s?

981 views Asked by At

I have to deconstruct the following JSON into a list of case classes:

{
  "data": [
    [49, true, 14, null, null],
    [52, false, null, null, null],
    [72, true, 4, 2, 1]
  ]
}

case class:

case class Data(i1: Int, b: Bool, i2: Option[Int], i3: Option[Int], i4: Option[Int])

I started with a for comprehension, but was not able to finish it:

for {
  JArray(data) <- json \ "data"
  JArray(d) <- data
  JInt(line) <- d.head // ???
} yield Data()

Any help is much appreciated.

Thanks,

Michael

2

There are 2 answers

0
Peter Neyens On BEST ANSWER

You can write a CustomSerializer for Data.

I introduced a JOptionInt extractor to turn a JInt or a JNull into a Option[Int], it is possible it can be done in json4s directly.

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

case class Data(i1: Int, b: Boolean, i2: Option[Int], i3: Option[Int], i4: Option[Int])

object DataSerializer extends CustomSerializer[Data]( format => ( 
  {
    case JArray(List(JInt(i1), JBool(b), JOptionInt(i2), JOptionInt(i3), JOptionInt(i4))) =>
      Data(i1.toInt, b, i2, i3 , i4) 
  }, {
    case d: Data => JArray(List(d.i1, d.b, d.i2, d.i3, d.i4)) 
  }  
))

object JOptionInt {
  def unapply(x: JValue) : Option[Option[Int]] = x match {
    case JInt(i) => Option(Option(i.toInt))
    case JNull   => Option(None)
    case _       => None
  }
}

Which can be used as:

implicit val formats = DataSerializer

val json = parse("""
      {
        "data": [
          [49, true, 14, null, null],
          [52, false, null, null, null],
          [72, true, 4, 2, 1]
        ]
      }
      """)

val result = (json \ "data").extract[Array[Data]]
// Array(Data(49,true,Some(14),None,None), Data(52,false,None,None,None), Data(72,true,Some(4),Some(2),Some(1)))
1
Jon Pretty On

If you can permit including the Rapture JSON library, this can be done as follows, still using the JSON4S backend. This requires the following imports:

import rapture.json._, jsonBackends.json4s._

If you already have the JSON as a JValue, you can convert it to Rapture's Json type as follows:

val json = Json(jValue)

Given your case class definition, you need to redefine the JSON extractor for Data types (there is already a default extractor which expects a JSON object), like this:

implicit val dataExtractor = Json.extractor[Json].map { j =>
  Data(j(0).as[Int], j(1).as[Boolean], j(2).as[Option[Int]],
      j(3).as[Option[Int]], j(4).as[Option[Int]])
}

and you can then extract it with:

val list = json.as[List[Data]]