Search code examples
scaladata-structuresdeserializationjson-deserializationjson4s

How to deserialize a scala tree with JSON4S


Serialization works fine but I have nothing for deserialization. I found interesting solution for abstract class here How to serialize sealed abstract class with Json4s in Scala? but it doesn't deal with trees.

This the code of my test with a standard JSON4S :

import org.json4s._
import org.json4s.native.JsonMethods._
import org.json4s.native.Serialization.{ read, write }
import org.json4s.native.Serialization

abstract class Tree
case class Node(nameN: String, trees: List[Tree]) extends Tree
case class Leaf(nameL: String) extends Tree

object Tree extends App {
  implicit val formats = Serialization.formats(NoTypeHints)

  // object creation to test the serialization
  val root =
    Node(
      "Grand Pavois project",
      List(
        Node(
          "studies",
          List(
            Leaf("preliminary studies"),
            Leaf("detailled studies")
          )
        ),
        Node(
          "realization",
          List(
            Leaf("ground"),
            Leaf("building"),
            Leaf("roof")
          )
        ),
        Node(
          "delivery",
          List(
            Leaf("quality inspection"),
            Leaf("customer delivery")
          )
        )
      )
    )

  val serialized = write(root) // object creation and serialization
  println(s"serialized: $serialized") // print the result, this is OK

  // and now what about deserialization?
  // string creation for deserialization
  // ( it is the same as serialized above, I do like that to trace for the demo)
  val rootString = """
{
  "nameN": "Grand Pavois project",
  "trees": [
    {
      "nameN": "studies",
      "trees": [
        {
          "nameL": "preliminary studies"
        },
        {
          "nameL": "detailled studies"
        }
      ]
    },
    {
      "nameN": "realization",
      "trees": [
        {
          "nameL": "ground"
        },
        {
          "nameL": "building"
        },
        {
          "nameL": "roof"
        }
      ]
    },
    {
      "nameN": "delivery",
      "trees": [
        {
          "nameL": "quality inspection"
        },
        {
          "nameL": "customer delivery"
        }
      ]
    }
  ]
}
"""
//standard deserialization below that produce an error :
// "Parsed JSON values do not match with class constructor"
val rootFromString = read[Tree](rootString)
}

Now I guess the solution is with a custom deserializer probably a recusive one but how to define it? That is the question. Thanks for your help.


Solution

  • This solution doesn't use a custom deserializer, but instead creates a type that matches both Node and Leaf and then converts to the appropriate type later.

    case class JsTree(nameN: Option[String], nameL: Option[String], trees: Option[List[JsTree]])
    
    def toTree(node: JsTree): Tree = node match {
      case JsTree(Some(name), None, Some(trees)) =>
        Node(name, trees.map(toTree))
      case JsTree(None, Some(name), None) =>
        Leaf(name)
      case _ =>
        throw new IllegalArgumentException
    }
    
    val rootFromString = toTree(read[JsTree](rootString))
    

    The JsTree class will match both Node and Leaf values because it has option fields that match all the fields in both classes. The toTree method recursively converts the JsTree to the appropriate Tree subclass based on which fields are actually present.

    Update: Custom serializer

    Here is the solution using a custom serializer:

    import org.json4s.JsonDSL._
    
    class TreeSerializer extends CustomSerializer[Tree](format => ({
      case obj: JObject =>
        implicit val formats: Formats = format
    
        if ((obj \ "trees") == JNothing) {
          Leaf(
            (obj \ "nameL").extract[String]
          )
        } else {
          Node(
            (obj \ "nameN").extract[String],
            (obj \ "trees").extract[List[Tree]]
          )
        }
    }, {
      case node: Node =>
        JObject("nameN" -> JString(node.nameN), "trees" -> node.trees.map(Extraction.decompose))
      case leaf: Leaf =>
        "nameL" -> leaf.nameL
    }))
    

    Use it like this:

    implicit val formats: Formats = DefaultFormats + new TreeSerializer
    
    read[Tree](rootString)