1
votes

I can not figure out why following case class constructs can not be handled with play-json's Json.format[] macro. More specifically, the error raised by the compiler does not give me any clue about what the problem is and how I might address it.

Given the following definitions:

trait Evt[T <: Evt[T]] {
  def meta: Meta[T]
}
case class Meta[T <: Evt[T]](value: String)
case class X(meta: Meta[X], data: String) extends Evt[X]

I tried to implement JSON formats like follows. The format for Meta[X] is just a preliminary try. I am more concerned here with the standard way of using Json.format[X] and the compilation error described below.

import play.api.libs.json._

object Formats {
  implicit val metaFormat = new Format[Meta[X]] {
    override def writes(o: Meta[X]): JsValue = JsString(o.value)
    override def reads(json: JsValue): JsResult[Meta[X]] = json.validate[String] match {
      case JsSuccess(value, path) => JsSuccess(Meta[X](value), path)
      case error: JsError => error
    }
  }
  implicit val xFormat: Format[X] = Json.format[X]
}

Trying to compile this yields the following error:

[error] ...: type mismatch;
[error]  found   : X
[error]  required: Meta[X]
[error]   implicit val xFormat: Format[X] = Json.format[X]
[error]                                                ^

I'd like to understand why this happens and possibly some clues about ways to work around this so I can format constructs like the above.

Using

  • Scala 2.11.4
  • play-json_2.11-2.3.6

Update

After some more thinking and digging I'd suspect the code that is generated by the compiler macro to cause this compilation error, as the error message at least for me does not make any sense in relation to the code line printed. I tried to inspect the code with scalac option -Ymacro-debug-lite and this is what I got:

{
  import play.api.libs.functional.syntax._;
  final class $anon extends play.api.libs.json.util.LazyHelper[Format, X] {
    def <init>() = {
      super.<init>();
      ()
    };
    override lazy val lazyStuff: Format[X] = play.api.libs.json.JsPath.$bslash("meta").lazyFormat(this.lazyStuff).and(play.api.libs.json.JsPath.$bslash("data").format(json.this.Format.GenericFormat[String](json.this.Reads.StringReads, json.this.Writes.StringWrites))).apply(((meta, data) => X.apply(meta, data)), play.api.libs.functional.syntax.unlift(X.unapply))
  };
  new $anon()
}.lazyStuff

I think the part $bslash("meta").lazyFormat(this.lazyStuff) might result in the compilation error, as this.lazyStuff returns a Format[X] whereas at this place (for path meta) a Format[Meta[X]] would be required.

With this analysis it looks like a shortcoming of the format macro. However, I am not sure if this analysis is correct, so still hoping for some clarification.

Update 2

Some more experimentation revealed that this problem is not limited to recursively defined types only. Also trying to use the format macro for this construct fails with the same error:

trait Evt[T] { 
  def meta: Meta[T]
}
case class Meta[T](value: String, anotherValue: String)
case class X(meta: Meta[X], data: String) extends Evt[X]

I found some possibly related work on the macro here. But also using 2.4.0-M1 of the play framework still revealed the same issues.

1

1 Answers

0
votes

Json.format[A] is a macro meant for serializing simple case classes. I believe somewhere in the documentation it warns against using it with recursively defined types. It seems to work using json combinators:

import play.api.libs.json._
import play.api.libs.functional.syntax._

implicit val XFormat: Format[X] = (
    (__ \ "meta").format[Meta[X]] and 
    (__ \ "data").format[String]
)(X.apply _, unlift(X.unapply))


scala> val x = X(Meta[X]("meta"), "data")
x: X = X(Meta(meta),data)

scala> Json.toJson(x)
res1: play.api.libs.json.JsValue = {"meta":"meta","data":"data"}