Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use play-json format/reads/writes macro with tagged types

I use typesafe tags in my models.

Tags API:

/** Tag of type `U`. */
type Tag[+U] = { type Tag <: U }
/** Type `T` tagged with tag of type `U`. */
type @@[T, +U] = T with Tag[U]

implicit class Taggable[T](val t: T) extends AnyVal {
  /** Tag with type `U`. */
  def tag[U]: T @@ U = t.asInstanceOf[T @@ U]
  /** Tag with type `U`. */
  def @@[U]: T @@ U = tag[U]
}

Example model:

case class User(id: Long @@ User, name: String)

The problem is: invocation of play-json's Json.format macro not compile when there is a tagged type in the case class:

import play.api.libs.json._

implicit val userIdFormat: Format[Long @@ User] = ???
Json.format[User] // doesn't compile

Error:(22, 14) type mismatch; found : id.type (with underlying type com.artezio.util.tags.User) required: com.artezio.util.tags.@@[Long,com.artezio.util.tags.User] (which expands to) Long with AnyRef{type Tag <: com.artezio.util.tags.User}

But all goes smoothly if I create Format instance manually:

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

implicit val userIdFormat: Format[Long @@ User] = ???
implicit val userFormat: Format[User] = (
  (JsPath \ "id").format[Long @@ User] and
  (JsPath \ "name").format[String]
)(User.apply, unlift(User.unapply))
like image 360
Tvaroh Avatar asked Dec 18 '25 20:12

Tvaroh


2 Answers

You can call it a bug, or perhaps a missing feature. You can use a compiler flag to see the generated code of the macro. scalacOptions ++= Seq("-Ymacro-debug-verbose")

What you get is (with Reads instead of Format):

{
  import play.api.libs.functional.syntax._;
  final class $anon extends play.api.libs.json.util.LazyHelper[Reads, User] {
    def <init>() = {
      super.<init>();
      ()
    };
    override lazy val lazyStuff: Reads[User] = play.api.libs.json.JsPath.$bslash("id").lazyRead(this.lazyStuff).and(play.api.libs.json.JsPath.$bslash("name").read(json.this.Reads.StringReads)).apply(((id, name) => User.apply(id, name)))
  };
  new $anon()
}.lazyStuff

Simplified for readability:

{
  import play.api.libs.functional.syntax._
  final class $anon extends play.api.libs.json.util.LazyHelper[Reads, User] {
    override lazy val lazyStuff: Reads[User] = {
      (__ \ ("id")).lazyRead(this.lazyStuff)
      .and((__ \ ("name")).read(Reads.StringReads))
      .apply(((id, name) => User.apply(id, name)))    
    }

  }
  new $anon()
}.lazyStuff

Notice the recursive call (__ \ ("id")).lazyRead(this.lazyStuff), which is a Reads[User]. For some reason, id is inferred as having type User within the macro, which causes the wrong Reads to be called, and you get the type mismatch on macro expansion.

like image 188
Michael Zajac Avatar answered Dec 20 '25 09:12

Michael Zajac


Play-json format macro doesn't support tagged types, but you can provide Json.format for your tagged type yourself. (Examples are using scalaz.Tag, but idea is the same for other implementations).

If your are tagging Long type you can use this:

implicit def taggedLongFormat[T]: Format[Long @@ T] = new Format[Long @@ T] {
  def reads(json: JsValue): JsResult[Long @@ T] = json match {
    case JsNumber(v) => JsSuccess(Tag.of[T](v.toLong))
    case unknown => JsError(s"Number value expected, got: $unknown")
  }
  def writes(v: Long @@ T): JsValue = JsNumber(Tag.unwrap(v))
}

Or if you are tagging String type you can use this:

implicit def taggedStringFormat[T]: Format[String @@ T] = new Format[String @@ T] {
  def reads(json: JsValue): JsResult[String @@ T] = json match {
    case JsString(v) => JsSuccess(Tag.of[T](v))
    case unknown => JsError(s"String value expected, got: $unknown")
  }
  def writes(v: String @@ T): JsValue = JsString(Tag.unwrap(v))
}

Now format for every case class containing tagged type can be created directly:

implicit val userFormat = Json.format[User]
like image 22
Josef Vlach Avatar answered Dec 20 '25 08:12

Josef Vlach



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!