Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type parameter in play-json read/write macro

I have a parametrized case class CaseClass[T](name: String, t: T) for which I would like to have serialization/deserialization using play-json (2.5).

Of course, I cannot have this if I do not have the equivalent for the type T, so I define

object CaseClass {
  implicit def reads[T: Reads] = Json.reads[CaseClass[T]]
}

But I get the following compiler error:

overloaded method value apply with alternatives:
   [B](f: B => (String, T))(implicit fu: play.api.libs.functional.ContravariantFunctor[play.api.libs.json.Reads])play.api.libs.json.Reads[B] <and>
   [B](f: (String, T) => B)(implicit fu: play.api.libs.functional.Functor[play.api.libs.json.Reads])play.api.libs.json.Reads[B]
   cannot be applied to ((String, Nothing) => CaseClass[Nothing])

If I try to do the same with the Json.writes macro, I get the error

type mismatch;
   found   : CaseClass[Nothing] => (String, Nothing)
   required: CaseClass[T] => (String, T)

What is most surprising, is that neither error occur when I use the Json.format macro.

I know I have different solutions to by-pass this problem (using Json.format, writing my (de)serializer by hand, ...), but I'm rather curious about why this can occur here.

like image 584
Cyrille Corpet Avatar asked Apr 26 '26 16:04

Cyrille Corpet


1 Answers

It's either a limitation in the Json.reads macro, type inference, or both. Type inference has a little bit to do with it at least, because you can see that something is being inferred as Nothing in the error message.

If you use the compiler flag -Ymacro-debug-lite, you can see the macro generated AST.

implicit def reads[T](implicit r: Reads[T]): Reads[CaseClass[T]] = 
  Json.reads[CaseClass[T]]

Translates to:

_root_.play.api.libs.json.JsPath.$bslash("name").read(json.this.Reads.StringReads)
  .and(_root_.play.api.libs.json.JsPath.$bslash("t").read(r))
  .apply((CaseClass.apply: (() => <empty>)))

Cleaned up, it looks like:

implicit def reads[T](implicit w: Reads[T]): Reads[CaseClass[T]] = (
  (JsPath \ "name").read(Reads.StringReads) and
  (JsPath \ "t" ).read(r)
)(CaseClass.apply _)

Unfortunately, it doesn't compile because the type parameter of CaseClass.apply isn't supplied and is inferred as Nothing. Manually adding T to apply fixes the issue, but the macro likely doesn't know that T in CaseClass[T] is important.

To address the type inference issue with more detail, with the Reads combinators, we're calling FunctionalBuilder.CanBuild2#apply, which expects a (A1, A2) => B. But the compiler cannot properly infer A2.

For Writes, there is a similar issue, where we need a B => (A1, A2), but compiler is unable to infer B or A2 correctly (which is CaseClass[T] and T, respectively).

Format requires both of the above functions, and the compiler is able to reason that A2 must be T.

like image 96
Michael Zajac Avatar answered May 01 '26 05:05

Michael Zajac