1

How to deserialize a sealed trait. I am using play JSON framework. I have read many post for writing companion object. Does anyone know how to write for Json.writes?

sealed trait DataFormat             
case object JSON extends DataFormat                      
case object INVALID extends DataFormat
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Aman
  • 123
  • 1
  • 2
  • 10

1 Answers1

1

As indicated in the documentation, the Reads/OWrites/OFormat instances for sealed family (sealed trait and subtypes) can be generated using Json.{read,write,format} macros.

import play.api.libs.json._

sealed trait DataFormat
case object JsonFormat extends DataFormat
case object InvalidFormat extends DataFormat

object DataFormat {
  implicit val format: OFormat[DataFormat] = {
    implicit def jsf = Json.format[JsonFormat.type]
    implicit def ivf = Json.format[InvalidFormat.type]

    Json.format[DataFormat]
  }
}

Please use the Scala naming, and do not name types (classes, objects, traits in UPPERCASE).

Then:

scala> Json.toJson[DataFormat](JsonFormat)
res1: play.api.libs.json.JsValue = {"_type":"JsonFormat"}

scala> Json.toJson(InvalidFormat: DataFormat)
res2: play.api.libs.json.JsValue = {"_type":"InvalidFormat"}

You can have a look at this previous question for the limitations.

You could also have a look at Enumeratum support for Play-JSON (to use enumerated types with Play-JSON).


About alternative code in comment:

object DataFormat {
  // Note: dataFormatObject is not an object name according Scala naming
  implicit object DataFormatObject extends Format[DataFormat] {
    implicit def reads(json: JsValue) = json match {
      case JsString("JSON") =>
        JsSuccess(JSON)
      case _ =>
        JsError(INVALID.toString)
    }

    implicit def writes(dataFormat: DataFormat) =
      JsString(dataFormat.toString)
  }
}

// No need to define it again as available in companion object
implicit lazy val dataFormatWrites: Format[DataFormat] =
  DataFormat.dataFormatObject

So there is wrong naming and val dataFormatWrites is not need.

If you want to represent enumerated type in Play JSON, I strongly advice to have a look at Enumeratum-Play-JSON as mentioned previously. Anyway it can be done by composing already provided Reads[String] and Writes[String] instances.

import play.api.libs.json._

sealed trait DataFormat
case object JsonFormat extends DataFormat
case object InvalidFormat extends DataFormat

object DataFormat {
  implicit def writes: Writes[DataFormat] =
    implicitly[Writes[String]]. // Resolve default `Writes[String]`
      contramap[DataFormat] {
        case JsonFormat => "JSON"
        case _ => "INVALID"
      }

  implicit def reads: Reads[DataFormat] =
    implicitly[Reads[String]]. // Resolve default `Reads[String]`
      collect[DataFormat](JsonValidationError("Unsupported DataFormat")) {
        case "JSON" => JsonFormat
        case _ => InvalidFormat
      }
}

scala> Json.toJson[DataFormat](JsonFormat)
res1: play.api.libs.json.JsValue = "JSON"

scala> Json.toJson(InvalidFormat: DataFormat)
res3: play.api.libs.json.JsValue = "INVALID"

scala> JsString("JSON").validate[DataFormat]
res4: play.api.libs.json.JsResult[DataFormat] = JsSuccess(JsonFormat,)

scala> JsString("INVALID").validate[DataFormat]
res5: play.api.libs.json.JsResult[DataFormat] = JsSuccess(InvalidFormat,)

Moreover, I would not define case object InvalidFormat, as it doesn't represent a format, but rather an error when trying to work with supported format. That's where validation types are useful, there with Play-JSON it's JsResult, so InvalidFormat should rather be a JsError[DataFormat].

import play.api.libs.json._

sealed trait DataFormat
case object JsonFormat extends DataFormat
case object AnotherFormat extends DataFormat // at least 2 subtypes, otherwise no need to have a DataFormat trait but rather directly use JsonFormat

object DataFormat {
  implicit def writes: Writes[DataFormat] =
    implicitly[Writes[String]]. // Resolve default `Writes[String]`
      contramap[DataFormat] {
        case JsonFormat => "JSON"
        case AnotherFormat => "ANOTHER"
      }

  implicit def reads: Reads[DataFormat] =
    implicitly[Reads[String]]. // Resolve default `Reads[String]`
      collect[DataFormat](JsonValidationError("Invalid DataFormat")) {
        case "JSON" => JsonFormat
        case "ANOTHER" => AnotherFormat
      }
}

So then:

scala> Json.toJson[DataFormat](JsonFormat)
res0: play.api.libs.json.JsValue = "JSON"

scala> Json.toJson(AnotherFormat: DataFormat)
res2: play.api.libs.json.JsValue = "ANOTHER"

scala> JsString("JSON").validate[DataFormat]
res3: play.api.libs.json.JsResult[DataFormat] = JsSuccess(JsonFormat,)

scala> JsString("ANOTHER").validate[DataFormat]
res4: play.api.libs.json.JsResult[DataFormat] = JsSuccess(AnotherFormat,)

scala> JsString("FOO").validate[DataFormat]
res5: play.api.libs.json.JsResult[DataFormat] = JsError(List((,List(JsonValidationError(List(Invalid DataFormat),WrappedArray())))))
cchantep
  • 9,118
  • 3
  • 30
  • 41
  • object DataFormat { implicit object dataFormatObject extends Format[DataFormat] { implicit def reads(json: JsValue) = json match { case JsString("JSON") => JsSuccess(JSON) case _ => JsError(INVALID.toString) } implicit def writes(dataFormat: DataFormat) = JsString(dataFormat.toString) } } implicit lazy val dataFormatWrites: Format[DataFormat] = DataFormat.dataFormatObject I wrote like this is this also fine? – Aman Jul 03 '20 at 08:34
  • yes thanks,but which one should I use,the one you told or the later one? – Aman Jul 03 '20 at 14:33
  • The answer explains, but you should rather use enumeratum – cchantep Jul 03 '20 at 14:39
  • hey, say I only have a writer and I don't want/need this additional "_type" field, is there a way to get rid of it? – Julian Barathieu Jun 30 '22 at 13:49
  • Writing without a discriminator would be a bad idea, as data won't be readable, or not readable without a much more complex logic to detect the struct type. – cchantep Jun 30 '22 at 14:11