10

How to deserialize nested objects correctly in spray-json?

    import spray.json._

    case class Person(name: String)

    case class Color(n: String, r: Int, g: Int, b: Int, p: Person)

    object MyJsonProtocol extends DefaultJsonProtocol {

      implicit object ColorJsonFormat extends RootJsonFormat[Color] {
        def write(c: Color) = JsObject(
          "color-name" -> JsString(c.n),
          "Green" -> JsNumber(c.g),
          "Red" -> JsNumber(c.r),
          "Blue" -> JsNumber(c.b),
          "person-field" -> JsObject("p-name" -> JsString(c.p.name))
        )

        def read(value: JsValue) = {
          value.asJsObject.getFields("color-name", "Red", "Green", "Blue", "person-field") match {
            case Seq(JsString(name), JsNumber(red), JsNumber(green), JsNumber(blue), JsObject(person)) =>
              Color(name, red.toInt, green.toInt, blue.toInt, null) //gotta replace null with correct deserializer
            case _ => throw new DeserializationException("Color expected")
          }
        }
      }

    }

    import MyJsonProtocol._

    val jsValue = Color("CadetBlue", 95, 158, 160, Person("guest")).toJson

    jsValue.prettyPrint

    val color = jsValue.convertTo[Color] //person is missing of course

On a side-note, how to spray-json help serializing to a map of fields (with nested map for nested objects)?

user3103600
  • 173
  • 1
  • 2
  • 5

3 Answers3

17

The example below demonstrates JSON -> Abstract Syntax Tree -> Scala Case Classes and back with custom field names and support for optional case class members. The example is derived from the spray-json documentation at https://github.com/spray/spray-json for version 1.2.5.

package rando

import spray.json._

case class Color(name: String, red: Int, green: Int, blue: Int)

case class Team(name: String, color: Option[Color])

object MyJsonProtocol extends DefaultJsonProtocol {
  implicit val colorFormat = jsonFormat(Color, "name", "r", "g", "b")
  implicit val teamFormat = jsonFormat(Team, "name", "jersey")
}
import MyJsonProtocol._

object GoSox extends App {
  val obj = Team("Red Sox", Some(Color("Red", 255, 0, 0)))
  val ast = obj.toJson
  println(obj)
  println(ast.prettyPrint)
  println(ast.convertTo[Team])
  println("""{ "name": "Red Sox", "jersey": null }""".asJson.convertTo[Team])
  println("""{ "name": "Red Sox" }""".asJson.convertTo[Team])
}

The example outputs the following when executed:

Team(Red Sox,Some(Color(Red,255,0,0)))
{
  "name": "Red Sox",
  "jersey": {
    "name": "Red",
    "r": 255,
    "g": 0,
    "b": 0
  }
}
Team(Red Sox,Some(Color(Red,255,0,0)))
Team(Red Sox,None)
Team(Red Sox,None)
Dave Swartz
  • 890
  • 6
  • 14
  • How about Option[Color]? So Team class becomes case class Team(name: String, color: Option[Color]) – user3103600 Feb 12 '14 at 22:28
  • Can you tell me what you have already tried? Was the spray-json documentation helpful? – Dave Swartz Feb 13 '14 at 01:30
  • I want to customize field names during serialization. – user3103600 Feb 14 '14 at 02:10
  • I ask that you accept my answer to the original question and then ask subsequent questions in a separate post per the guidance in http://meta.stackexchange.com/a/39224. – Dave Swartz Feb 14 '14 at 03:26
  • Dude I can't accept it as an answer because it takes me even further away from what I already have i.e. customized serial field names. If you read my snipped in question, you'll notice the following line: `Color(name, red.toInt, green.toInt, blue.toInt, null) //gotta replace null with correct deserializer` I need to know the way to be able to handle "null" values during (de)serialization. – user3103600 Feb 15 '14 at 05:51
  • 1
    I assumed you were just playing around with this code to understand spray-json, which was further reinforced by your question, "How to deserialize nested objects correctly in spray-json?". I suggest in the future you use more specific questions. E.g., "I have the following classes. The code does not compile and produces the following error... To be clear I need to handle nulls and want to use custom field names for serialization/deserialization." I will update the answer now with custom field names and null handling. All I am doing is reading the docs FYI. – Dave Swartz Feb 15 '14 at 14:04
  • I went through the docs and took away that you should use Option[T] rather than null. But how to omit and/or (de)serialize etc is unclear. – user3103600 Feb 15 '14 at 17:21
  • 1
    The documentation is really unhelpful there. Specially for Scala newbies. Anyways, i'll try this snippet and mark it accepted. Thanks – user3103600 Feb 15 '14 at 17:59
  • Spray JSON documentation is not unlike many other open source projects that were born out of someone's hobby or frustration against another library. The docs convert a case class to and from JSON. Big deal. In real world, nested json often needs to be converted to objects that are not our own and hence can't be readily fit into the typical "hello world" examples. – Abhijit Sarkar Jan 30 '16 at 05:57
  • what if there are multiple nested classes (say, `Color` and `Size` inside the `Team`)? – techkuz Aug 08 '20 at 14:10
1

To your remaining question - how to reuse JSON conversions within a wrapping type:

"person-field" -> JsObject("p-name" -> JsString(c.p.name))

I would change this to:

"person-field" -> p.toJson

This way, you are letting the Person case class JSONify itself, instead of introducing another way in the wrapping object. DRY and simpler.

And:

case Seq(JsString(name), JsNumber(red), JsNumber(green), JsNumber(blue), JsObject(person)) => 
  Color(name, red.toInt, green.toInt, blue.toInt, null)

Use the .convertTo[Person] here:

case Seq(JsString(name), JsNumber(red), JsNumber(green), JsNumber(blue), jsv) => 
  Color(name, red.toInt, green.toInt, blue.toInt, jsv.convertTo[Person])

If there are problems, please ask for more help. I have similar structure in my own project but didn't try to run them in this context.

akauppi
  • 14,244
  • 12
  • 73
  • 94
0
import models.{Address, Patient}
import spray.json._

object Hospital {

  object MyJsonProtocol extends DefaultJsonProtocol {
    implicit val address = jsonFormat(Address, "country", "state", "zip")
    implicit val patient = jsonFormat(Patient, "name", "regNumber", "address")
  }

  import MyJsonProtocol._

  def main(args: Array[String]): Unit = {
    val p1 = Patient(name = "Amar", regNumber = 234, address = Address("IN", "KA", 49))
    println(p1.toJson.sortedPrint)
  }

}

Output

{
  "address": {
    "country": "IN",
    "state": "KA",
    "zip": 49
  },
  "name": "Amar",
  "regNumber": 234
}
Amar Kumar
  • 31
  • 7