skip to Main Content

I’ve got some JSON that I need to parse, and my project already uses Play, so that seems like the right place to start.

Given these definitions:

sealed trait Thing
case class Foo(i: Int) extends Thing
case class Bar(s: String, t: List[Thing]) extends Thing

I would want this JSON:

{
  s: "doomy doomy doom",
  t: [ 24, { s: "doooom!", t: [ 1, 2, 3 ] }, 42, 126 ]
}

To become this object:

Bar("doomy doomy doom", List(Foo(24), Bar("doooom!", List(Foo(1), Foo(2), Foo(3))), Foo(42), Foo(126)))

Any suggestions?

2

Answers


  1. Try to define custom codecs for the trait (without discriminator) and lazy codecs for the recursive type

    import play.api.libs.json._
    import play.api.libs.functional.syntax._
    
    sealed trait Thing
    object Thing {
      implicit val thingReads: Reads[Thing] = Foo.fooReads.or[Thing](Bar.barReads.widen)
    
      implicit val thingWrites: OWrites[Thing] = {
        case x: Foo => Foo.fooWrites.writes(x)
        case x: Bar => Bar.barWrites.writes(x)
      }
    }
    
    case class Foo(i: Int) extends Thing
    object Foo {
      implicit val fooReads: Reads[Foo] = Json.reads[Foo]
      implicit val fooWrites: OWrites[Foo] = Json.writes[Foo]
    }
    
    case class Bar(s: String, t: List[Thing]) extends Thing
    object Bar {
      implicit val barReads: Reads[Bar] = (
        (__  "s").read[String] and
          (__  "t").lazyRead(Reads.list[Thing](Thing.thingReads))
      )(Bar.apply _)
    
      implicit val barWrites: OWrites[Bar] = (
        (__  "s").write[String] and
          (__  "t").lazyWrite(Writes.list[Thing](Thing.thingWrites))
      )(unlift(Bar.unapply))
    }
    
    val thing: Thing = Bar("doomy doomy doom", List(Foo(24), Bar("doooom!", List(Foo(1), Foo(2), Foo(3))), Foo(42), Foo(126)))
    val str = Json.stringify(Json.toJson(thing))
    //{"s":"doomy doomy doom","t":[{"i":24},{"s":"doooom!","t":[{"i":1},{"i":2},{"i":3}]},{"i":42},{"i":126}]}
    val thing1 = Json.parse(str).as[Thing]
    // Bar(doomy doomy doom,List(Foo(24), Bar(doooom!,List(Foo(1), Foo(2), Foo(3))), Foo(42), Foo(126)))
    thing1 == thing // true
    

    Scala play json nested cyclic dependency json parsing

    Login or Signup to reply.
  2. To add to @dmytro-mitin answer, you can use Scala’s value class for Foo. Play JSON documentation includes Reads/Writes/Formats for value classes. Then you can use Int instead of an object with single field in your original example. Here is an updated example with a value class:

    import play.api.libs.json._
    import play.api.libs.functional.syntax._
    
    sealed trait Thing extends Any
    
    object Thing {
      implicit val thingReads: Reads[Thing] = Foo.fooReads.or[Thing](Bar.barReads.widen)
    
      implicit val thingWrites: Writes[Thing] = {
        case f: Foo => Foo.fooWrites.writes(f)
        case b: Bar => Bar.barWrites.writes(b)
      }
    }
    
    case class Foo(i: Int) extends AnyVal with Thing
    
    object Foo {
      implicit val fooReads: Reads[Foo] = Json.valueReads[Foo]
      implicit val fooWrites: Writes[Foo] = Json.valueWrites[Foo]
    }
    
    case class Bar(s: String, t: List[Thing]) extends Thing
    
    object Bar {
      implicit val barReads: Reads[Bar] = (
        (__  "s").read[String] and
          (__  "t").lazyRead(Reads.list[Thing](Thing.thingReads))
      )(Bar.apply _)
    
      implicit val barWrites: Writes[Bar] = (
        (__  "s").write[String] and
          (__  "t").lazyWrite(Writes.list[Thing](Thing.thingWrites))
      )(unlift(Bar.unapply))
    }
    
    val thing: Thing = Bar("doom", List(Foo(12), Bar("doom", List(Foo(1), Foo(2), Foo(3)))))
    val json = Json.toJson(thing)
    val str = Json.stringify(json)
    // {"s":"doom","t":[12,{"s":"doom","t":[1,2,3]}]}
    val thing1 = Json.parse(str).as[Thing]
    // Bar(doom,List(Foo(12), Bar(doom,List(Foo(1), Foo(2), Foo(3)))))
    thing1 == thing // true
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search