Json Serialization for Trait с несколькими классами классов (типы сумм) в Scala Play

Мне часто приходится сериализовать/десериализовать типы сумм (например, Either[S,T]), и я еще не нашел общий или элегантный способ сделать это. Здесь примерный тип (по существу эквивалентный Either)

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

Вот мое самое лучшее усилие в сопутствующем объекте, который реализует сериализацию. Это работает, но очень утомительно писать такие вещи снова и снова для каждого типа суммы. Есть ли какие-либо предложения, чтобы сделать его более приятным и/или более общим?

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 [email protected](_)     => Json.obj("NumericOutcome"     -> Json.toJson(n)(fmtNumeric))
      case [email protected](_) => Json.obj("QualitativeOutcome" -> Json.toJson(q)(fmtQualitative))
    }

    def reads(json: JsValue) = (
      Json.fromJson(json \ "NumericOutcome")(fmtNumeric) orElse
      Json.fromJson(json \ "QualitativeOutcome")(fmtQualitative)
    )
  }
}

Ответы

Ответ 1

Я думаю, что это примерно так же просто, как вы можете это сделать, если вы хотите избежать написания кода для каждого явного подтипа, возможно, вы могли бы сделать это с помощью рефлексии, использовать джексон напрямую или какую-либо другую библиотеку json с поддержкой рефлексии. Или напишите свой собственный макрос, чтобы сгенерировать формат из списка подтипов.

Ответ 2

У меня есть систематическое решение проблемы сериализации типов сумм в моей библиотеке json pickling Prickle. Аналогичные идеи можно использовать в Play. Существует еще некоторый код конфигурации, но его высокий сигнал/шум, например, конечный код, например:

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

CompositePicklers, связанный с супертипом, настроен с одним PicklerPair для каждого известного подтипа (например, тип суммы). Ассоциации настраиваются во время настройки.

Во время травления дескриптор испускается в поток json, описывающий, какой подтип записи.

Во время unpickling дескриптор считывается из json, а затем используется для поиска соответствующего Unpickler для подтипа

Ответ 3

Пример, обновленный для воспроизведения 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])
}