Search code examples
jsonscalaplayframework-2.0play-json

Scala PlayJson Cyclic Reference


Context

I have a case class which is an item in a hierarchy, which refers to itself like so:

case class Node(
  name:     String,
  children: Option[Seq[Node]] = None
)

I would like a PlayJson Format for this.

Usually, you can just do:

implicit lazy val formatter = Json.format[MyCaseClass]

But this doesn't work.

Why?

PlayJson uses a Scala macro to produce a Format for the case class, it will go through all fields, when it gets to the field children it will look for an existing formatter for Node which it hasn't constructed yet, ending with a compilation error:

No implicit format for Option[Seq[Node]] available.
[error]   implicit lazy val formatter = Json.format[Node]

Questions

What's the best way to approach this?
Is this a known issue with PlayJson format macro?


Solution

  • This is something that can be found under recursive types in play-json docs:

    import play.api.libs.functional.syntax._
    import play.api.libs.json.{Reads, Writes, _}
    
    case class Node(name: String, children: Option[Seq[Node]] = None)
    
    implicit lazy val nodeReads: Reads[Node] = (
      (__ \ "name").read[String] and
      (__ \ "children").lazyReadNullable(Reads.seq[Node](nodeReads))
    )(Node)
    
    implicit lazy val nodeWrites: Writes[Node] = (
      (__ \ "name").write[String] and
      (__ \ "children").lazyWriteNullable(Writes.seq[Node](nodeWrites))
    )(unlift(Node.unapply))
    

    Since in that case Reads and Writes are symmetrical, you can create the whole thing as a single Format:

    implicit lazy val nodeFormat: Format[Node] = (
      (__ \ "name").format[String] and
      (__ \ "children").lazyFormatNullable(Reads.seq[Node](nodeFormat), Writes.seq[Node](nodeFormat))
    )(Node.apply, unlift(Node.unapply))