3

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.

Cyrille Corpet
  • 5,265
  • 1
  • 14
  • 31

1 Answers1

1

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.

Michael Zajac
  • 55,144
  • 7
  • 113
  • 138
  • What I still don't get is why the compiler error says there is an ambiguity on the `FunctionalBuilder.CanBuild2#apply`, between two methods, one of which clearly cannot apply (`Reads` is not a contravariant functor, and the parameter's type is nowhere near `B => (String, Nothing)`), while it is able to choose the right one for `Writes`, although with a problem on type inference that you explained clearly. From what you say. I guess the `Format` case is explained by the fact that type inference is easier on invariant functors, since there are more parameters (the two functions, as you said). – Cyrille Corpet Mar 29 '17 at 21:14