Json Serialization for Trait with Multiple Case Classes (Sum Types) in Scala's Play
Asked Answered
S

3

17

I often have to serialize/deserialize sum types (like Either[S,T]), and I haven't yet found a general or elegant way to do it. Here's an example type (essentially equivalent to Either)

sealed trait OutcomeType
case class NumericOutcome(units: String)              extends OutcomeType
case class QualitativeOutcome(outcomes: List[String]) extends OutcomeType

Here's my best effort at a companion object that implements serialization. It works, but it's very tiresome to write these sorts of things over and over for every sum type. Are there any suggestions for making it nicer and/or more general?

import play.api.libs.json._
import play.api.libs.functional.syntax._

object OutcomeType {

  val fmtNumeric     = Json.format[NumericOutcome]
  val fmtQualitative = Json.format[QualitativeOutcome]

  implicit object FormatOutcomeType extends Format[OutcomeType] {
    def writes(o: OutcomeType) = o match {
      case n@NumericOutcome(_)     => Json.obj("NumericOutcome"     -> Json.toJson(n)(fmtNumeric))
      case q@QualitativeOutcome(_) => Json.obj("QualitativeOutcome" -> Json.toJson(q)(fmtQualitative))
    }

    def reads(json: JsValue) = (
      Json.fromJson(json \ "NumericOutcome")(fmtNumeric) orElse
      Json.fromJson(json \ "QualitativeOutcome")(fmtQualitative)
    )
  }
}
Smarm answered 26/8, 2013 at 23:35 Comment(3)
Have you tried json4s json4s.org ? Also if you want to use it with play then you should look here: github.com/tototoshi/play-json4s or implement this yourself.Lyndes
Looks nice to me. Could you update your best effort to play2.5? Thanks!Charissa
Never mind, I found out how to do this in play2.5 and put it in an answer.Charissa
C
1

I think that is about as simple as you can make it, if you want to avoid writing the code for each explicit subtype maybe you could do it with reflection, use jackson directly or some other json library with reflection support. Or write your own macro to generate the Format from a list of subtypes.

Crier answered 27/8, 2013 at 14:58 Comment(0)
C
0

I have a systematic solution for the problem of serializing sum-types in my json pickling library Prickle. Similar ideas could be employed with Play. There is still some config code required, but its high signal/noise, eg final code like:

implicit val fruitPickler = CompositePickler[Fruit].concreteType[Apple].concreteType[Lemon]

CompositePicklers associated with a supertype are configured with one PicklerPair for each known subtype (ie sum type option). The associations are setup at configure time.

During pickling a descriptor is emitted into the json stream describing which subtype the record is.

During unpickling, the descriptor is read out of the json and then used to locate the appropriate Unpickler for the subtype

Cartage answered 30/11, 2015 at 10:23 Comment(0)
C
0

An example updated for play 2.5:

object TestContact extends App {

  sealed trait Shape

  object Shape {
    val rectFormat = Json.format[Rect]
    val circleFormat = Json.format[Circle]

    implicit object ShapeFormat extends Format[Shape] {
      override def writes(shape: Shape): JsValue = shape match {
        case rect: Rect =>
          Json.obj("Shape" ->
            Json.obj("Rect" ->
              Json.toJson(rect)(rectFormat)))
        case circle: Circle =>
          Json.obj("Shape" ->
            Json.obj("Circle" ->
              Json.toJson(circle)(circleFormat)))
      }

      override def reads(json: JsValue): JsResult[Shape] = {
        json \ "Shape" \ "Rect" match {
          case JsDefined(rectJson) => rectJson.validate[Rect](rectFormat)
          case _ => json \ "Shape" \ "Circle" match {
            case JsDefined(circleJson) => circleJson.validate[Circle](circleFormat)
            case _ => JsError("Not a valide Shape object.")
          }
        }
      }
    }

  }

  case class Rect(width: Double, height: Double) extends Shape

  case class Circle(radius: Double) extends Shape

  val circle = Circle(2.1)
  println(Json.toJson(circle))
  val rect = Rect(1.3, 8.9)
  println(Json.toJson(rect))

  var json = Json.obj("Shape" -> Json.obj("Circle" -> Json.obj("radius" -> 4.13)))
  println(json.validate[Shape])
  json =
    Json.obj("Shape" ->
      Json.obj("Rect" ->
        Json.obj("width" -> 23.1, "height" -> 34.7)))
  println(json.validate[Shape])
}
Charissa answered 22/3, 2017 at 13:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.