如何使用play-json格式/读/写带有标记类型的宏

时间:2015-11-10 18:58:45

标签: json scala playframework

我在模型中使用了类型安全标签。

标签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]
}

示例模型:

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

问题是:当case类中有标记类型时,调用 play-json Json.format宏不能编译:

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}

但如果我手动创建Format实例,那么一切顺利:

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))

3 个答案:

答案 0 :(得分:4)

您可以将其称为错误,或者可能是缺少的功能。您可以使用编译器标志来查看生成的宏代码。 scalacOptions ++= Seq("-Ymacro-debug-verbose")

你得到的是(使用Reads代替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

简化了可读性:

{
  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

请注意递归调用(__ \ ("id")).lazyRead(this.lazyStuff),即Reads[User]。出于某种原因,id被推断为在宏中具有类型User,这会导致调用错误的Reads,并且您在宏扩展时会出现类型不匹配。

答案 1 :(得分:3)

Play-json格式宏不支持标记类型,但您可以自己为标记类型提供Json.format。 (示例使用scalaz.Tag,但其他实现的想法是相同的。)

如果您要标记Long类型,则可以使用此选项:

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))
}

或者,如果您要标记String类型,则可以使用此选项:

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))
}

现在format可以直接创建包含标记类型的每个case class

implicit val userFormat = Json.format[User]

答案 2 :(得分:2)

经过一番调查后发现问题是由于使用“递归”问题引起的。输入标签位置,例如:

case class User(userId: Long @@ User)

它既不适用于我的代码API也不适用于Shapeless&#39; (怀疑与Scalaz标签相同,但我没有尝试过)。

因此,我的解决方法是为标签使用单独的类型,例如:

sealed trait U
case class User(userId: Long @@ U)