How to pick a field's type based off of another field's value when reading JSON using Play (Scala)?

216 views Asked by At

New to Scala and Play and running into a problem where I have the following:

case class Media(
    name: String,
    id: Id,
    type: String,
    properties: PodcastProperties
)
object Media {
    implicit val format: OFormat[Media] = Json.format[Media]

case class PodcastProperties(
   x: Int,
   y: DateTime,
   z: String
)
object PodcastProperties {
    implicit val format: OFormat[PodcastProperties] = Json.format[PodcastProperties]

Say I want to define Media to accept different media types. Let's say I have a Json Media object, and it's type is "newspaper" and it's properties should be parse using "NewspaperProperties"

case class NewspaperProperties(
   Title: String,
   Publisher: String
)
object NewspaperProperties {
    implicit val format: OFormat[NewspaperProperties] = Json.format[NewspaperProperties]

How can I define Media, so it can parse the "type" field, and then read the "properties" field correctly using the right Json parser?

1

There are 1 answers

1
cchantep On

You need to defined the media properties as sealed family.

import play.api.libs.json._
import java.time.OffsetDateTime

sealed trait MediaProperties

case class NewspaperProperties(
   title: String, // Do not use 'Title' .. initial cap is not valid
   publisher: String // ... not 'Publisher'
) extends MediaProperties

object NewspaperProperties {
    implicit val format: OFormat[NewspaperProperties] = Json.format[NewspaperProperties]
}

case class PodcastProperties(
   x: Int,
   y: OffsetDateTime,
   z: String
) extends MediaProperties

object PodcastProperties {
  implicit val format: OFormat[PodcastProperties] =
    Json.format[PodcastProperties]
}

Then a OFormat can be materialized for MediaProperties.

implicit val mediaPropertiesFormat: OFormat[MediaProperties] = Json.format

This managed discriminator in the JSON representation (by default _type field, naming can be configured).

val props1: MediaProperties = PodcastProperties(1, OffsetDateTime.now(), "z")

val obj1 = Json.toJson(props1)
// > JsValue = {"_type":"PodcastProperties","x":1,"y":"2020-11-23T22:53:35.301603+01:00","z":"z"}

obj1.validate[MediaProperties]
JsResult[MediaProperties] = JsSuccess(PodcastProperties(1,2020-11-23T23:02:24.752063+01:00,z),)

The implicit format for MediaProperties should probably be defined in the companion object MediaProperties.

Then the format for Media can be materialized automatically.

final class Id(val value: String) extends AnyVal

object Id {
  implicit val format: Format[Id] = Json.valueFormat
}

case class Media(
    name: String,
    id: Id,
    //type: String, -- Not needed for the JSON representation
    properties: MediaProperties
)

object Media {
  implicit val format: OFormat[Media] = Json.format[Media] // <--- HERE
}