Getting a Play JSON JsValueWrapper for a class that extends a trait

左心房为你撑大大i 提交于 2020-01-04 09:27:11

问题


I'm generating JSON for a speed where the units may vary. I have a SpeedUnit trait and classes that extend it (Knots, MetersPerSecond, MilesPerHour). The JSON Play documentation said "To convert your own models to JsValues, you must define implicit Writes converters and provide them in scope." I got that to work in most places but not when I had a class extending a trait. What am I doing wrong? Or is there an Enum variant I could or should have used instead?

// Type mismatch: found (String, SpeedUnit), required (String, Json.JsValueWrapper)
// at 4th line from bottom:  "speedunit" -> unit

import play.api.libs.json._

trait SpeedUnit {
  // I added this to SpeedUnit thinking it might help, but it didn't.
  implicit val speedUnitWrites = new Writes[SpeedUnit] {
    def writes(x: SpeedUnit) = Json.toJson("UnspecifiedSpeedUnit")
  }
}

class Knots extends SpeedUnit {
  implicit val knotsWrites = new Writes[Knots] {
    def writes(x: Knots) = Json.toJson("KT")
  }
}
class MetersPerSecond extends SpeedUnit {
  implicit val metersPerSecondWrites = new Writes[MetersPerSecond] {
    def writes(x: MetersPerSecond) = Json.toJson("MPS")
  }
}
class MilesPerHour extends SpeedUnit {
  implicit val milesPerHourWrites = new Writes[MilesPerHour] {
    def writes(x: MilesPerHour) = Json.toJson("MPH")
  }
}

// ...

class Speed(val value: Int, val unit: SpeedUnit) {
  implicit val speedWrites = new Writes[Speed] {
    def writes(x: Speed) = Json.obj(
      "value" -> value,
      "speedUnit" -> unit  // THIS LINE DOES NOT TYPE-CHECK
    )
  }
}

回答1:


Writes is an example of a type class, which means you need a single instance of a Writes[A] for a given A, not for every A instance. If you're coming from a Java background, think Comparator instead of Comparable.

import play.api.libs.json._

sealed trait SpeedUnit
case object Knots extends SpeedUnit
case object MetersPerSecond extends SpeedUnit
case object MilesPerHour extends SpeedUnit

object SpeedUnit {
  implicit val speedUnitWrites: Writes[SpeedUnit] = new Writes[SpeedUnit] {
    def writes(x: SpeedUnit) = Json.toJson(
      x match {
        case Knots => "KTS"
        case MetersPerSecond => "MPS"
        case MilesPerHour => "MPH"
      }
    )
  }
}

case class Speed(value: Int, unit: SpeedUnit)

object Speed {
  implicit val speedWrites: Writes[Speed] = new Writes[Speed] {
    def writes(x: Speed) = Json.obj(
      "value" -> x.value,
      "speedUnit" -> x.unit
    )
  }
}

And then:

scala> Json.toJson(Speed(10, MilesPerHour))
res0: play.api.libs.json.JsValue = {"value":10,"speedUnit":"MPH"}

I've put the Writes instances in the companion objects for the two types, but they can go elsewhere (if you don't want to mix up serialization concerns in your model, for example).

You can also simplify (or at least concise-ify) this a lot with Play JSON's functional API:

sealed trait SpeedUnit
case object Knots extends SpeedUnit
case object MetersPerSecond extends SpeedUnit
case object MilesPerHour extends SpeedUnit

case class Speed(value: Int, unit: SpeedUnit)

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

implicit val speedWrites: Writes[Speed] = (
  (__ \ 'value).write[Int] and
  (__ \ 'speedUnit).write[String].contramap[SpeedUnit] {
    case Knots => "KTS"
    case MetersPerSecond => "MPS"
    case MilesPerHour => "MPH"
  }
)(unlift(Speed.unapply))

Which approach you take (functional or explicit) is largely a matter of taste.



来源:https://stackoverflow.com/questions/28748786/getting-a-play-json-jsvaluewrapper-for-a-class-that-extends-a-trait

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!