Search code examples
jsonscalapolymorphismdeserializationlift-json

Polymorphic lift-json deserialization in a composed class


I am trying to automatically deserialize json object to a scala class using Lift-Json with a coordinate class inside used to store GeoJson information.

case class Request(name:String, geometry:Geometry)

sealed abstract class Geometry

case class Point(coordinates:(Double,Double)) extends Geometry
case class LineString(coordinates:List[Point]) extends Geometry
case class Polygon(coordinates:List[LineString]) extends Geometry

I want to deserialize a json string like this:

{
name:"test",
geometry:{
   "type": "LineString",
   "coordinates": [ [100.0, 0.0], [101.0, 1.0] ]
  }
}

into a Request case class with the right LineString runtime class in the Geometry field. I guess I should use a TypeHint but how?. Is this the correct approach or should I create three different Request (RequestPoint,RequestLineString and RequestPolygon)? This would be the Scala code to deserialize:

val json = parse(message)
json.extract[Request]

Solution

  • Yes, you need to use type hints for sum types like Geometry. Here's one example:

    implicit val formats = DefaultFormats.withHints(ShortTypeHints(List(classOf[Point], classOf[LineString], classOf[Polygon])))
    
    val r = Request("test", LineString(List(Point(100.0, 0.0), Point(101.0, 1.0))))
    
    Serialization.write(r)
    
    {
    "name":"test",
    "geometry":{
      "jsonClass":"LineString",
      "coordinates":[{"jsonClass":"Point","coordinates":{"_1$mcD$sp":100.0,"_2$mcD$sp":0.0}},{"jsonClass":"Point","coordinates":{"_1$mcD$sp":101.0,"_2$mcD$sp":1.0}}]}
    }
    

    Not quite what you wanted. Since you want to change the default serialization scheme for Points, you need to provide a custom serializer for that type.

    class PointSerializer extends Serializer[Point] {
      private val Class = classOf[Point]
    
      def deserialize(implicit format: Formats) = {
        case (TypeInfo(Class, _), json) => json match {
          case JArray(JDouble(x) :: JDouble(y) :: Nil) => Point(x, y)
          case x => throw new MappingException("Can't convert " + x + " to Point")
        }
      }
    
      def serialize(implicit format: Formats) = {
        case p: Point => JArray(JDouble(p.coordinates._1) :: JDouble(p.coordinates._2) :: Nil)
      }
    }
    
    // Configure it
    implicit val formats = DefaultFormats.withHints(ShortTypeHints(List(classOf[Point], classOf[LineString], classOf[Polygon]))) + new PointSerializer
    
    Serialization.write(r)
    
    {
      "name":"test",
      "geometry":{
        "jsonClass":"LineString",
        "coordinates":[[100.0,0.0],[101.0,1.0]]
      }
    }
    

    Better, but you need one more configuration if you need to change the default field named as 'jsonClass' to 'type':

    implicit val formats = new DefaultFormats { 
      override val typeHintFieldName = "type" 
      override val typeHints = ShortTypeHints(List(classOf[Point], classOf[LineString], classOf[Polygon]))
    } + new PointSerializer
    
    Serialization.write(r)
    
    {
      "name":"test",
      "geometry":{
        "type":"LineString",
        "coordinates":[[100.0,0.0],[101.0,1.0]]
      }
    }