Search code examples
scalatypeclassimplicitplay-json

Compile error with Tagged Type and Play Json Format typeclass derivation


In the following example, I want to use tagged types for the ids of my classes. I've created an utility trait to reduce some boilerplate (of tags/reads/writes declaration):

import java.util.UUID
import play.api.libs.json.{Format, Json, Reads, Writes}

trait Opaque[A] {
  protected type Tagged[U] = { type Tag = U }
  type @@[U, T] = U with Tagged[T]

  trait Tag

  def tag(a: A): A @@ Tag = a.asInstanceOf[A @@ Tag]
  def untag(a: A @@ Tag): A = a

  implicit def reads(implicit r: Reads[A]): Reads[A @@ Tag] =
    r.map(tag)

  implicit def writes(implicit w: Writes[A]): Writes[A @@ Tag] =
    w.contramap(untag)

  implicit def format(implicit r: Reads[A], w: Writes[A]): Format[A @@ Tag] =
    Format(reads(r), writes(w))
}

final case class Foo(id: Foo.FooId.T, f1: Boolean)

object Foo {
  object FooId extends Opaque[UUID] {
    type T = UUID @@ Tag
  }

  import FooId._
  implicit val fmt: Format[Foo] = Json.format[Foo]
}

final case class Bar(id: Bar.BarId.T, fooId: Foo.FooId.T, b1: String)

object Bar {
  object BarId extends Opaque[UUID] {
    type T = UUID @@ Tag
  }

  import Foo.FooId._
  import BarId._
  implicit val format: Format[Bar] = Json.format[Bar]
}

I have the following error from the compiler:

  implicit val format: Format[Bar] = Json.format[Bar]
                                                ^
<pastie>:43: error: No instance of play.api.libs.json.Format is available for Opaque.<refinement>, Opaque.<refinement> in the implicit scope (Hint: if declared in the same file, make sure it's declared before)

I'm not able to explain why I'm having this behaviour, the error message is not explicit. I'm importing the Format for FooId and BarId needed for deriving a format for Bar class.


Solution

  • The thing is that names of implicits are significant.

    Very simple example of that is following:

    object MyObject {
      implicit val i: Int = ???
    }
    
    import MyObject._
    
    implicit val i: String = ???
    
    // implicitly[Int] // doesn't compile
    // implicitly[String] // doesn't compile
    

    but

    object MyObject {
      implicit val i: Int = ???
    }
    
    import MyObject._
    
    implicit val i1: String = ???
    
    implicitly[Int] // compiles
    implicitly[String] // compiles
    

    If you want derivation Json.format[Bar] to work, there should be implicits Format[Bar.BarId.T], Format[Foo.FooId.T] in scope i.e. Format instances for fields of Bar. If you make the only import

    import Foo.FooId._
    implicitly[Format[Foo.FooId.T]] // compiles
    

    and

    import BarId._
    implicitly[Format[Bar.BarId.T]] // compiles
    

    but if you import both, since names of implicits collide

    import Foo.FooId._
    import BarId._
    // implicitly[Format[Foo.FooId.T]] // doesn't compiles
    // implicitly[Format[Bar.BarId.T]] // doesn't compiles
    

    For example you can move trait Tag outside trait Opaque and make the only import. Then

    implicitly[Format[Foo.FooId.T]]
    implicitly[Format[Bar.BarId.T]]
    Json.format[Bar]
    

    will compile.

    https://youtu.be/1h8xNBykZqM?t=681 Some Mistakes We Made When Designing Implicits, Mistake #1

    NullPointerException on implicit resolution