问题
I'm migrating some of my services from Play 2.7.x to the newest 2.8.2, together with scala 2.13.2 and sbt 1.3.12.
I'm hitting an obstacle with the play-json though, and the Reads[A]
I have the following setup:
sealed trait Charge {}
case class ChargeOne(one: Int) extends Charge
case class ChargeTwo(two: Int) extends Charge
object Charge {
implicit val writes: Writes[Charge] = (charge: Charge) => {...}
}
We have some tests looking like so
val chargeOne = ChargeOne(1)
val json = Json.toJson(charge)
json mustBe "..."
And these work fine in play 2.7.x with scala 2.12.x, and it does also work with scala 2.13.2, but after upgrading play to 2.8.2, the following error occurs when compiling:
No Json serializer found for type ChargeOne. Try to implement an implicit Writes or Format for this type.
And i have to add .asInstanceOf[Charge]
to my tests to make it work.
What has happened here? Is it play-json? or is it scala? And does anyone know how to "fix" it?
回答1:
As indicated in the error message, an instance of Writes
(or a Format
) for ChargeOne
is required, whereas the code only provide a Reads
.
import play.api.libs.json._ // BTW Recommend to provide import in code examples
sealed trait Charge {}
case class ChargeOne(one: Int) extends Charge
case class ChargeTwo(two: Int) extends Charge
object Charge {
implicit val format: OFormat[Charge] = {
// Need to define instance for the subtypes (no auto-materialization)
implicit def one = Json.format[ChargeOne]
implicit def two = Json.format[ChargeTwo]
Json.format[Charge]
}
}
Then you can see that Writes
and Reads
are invariant (typeclasses) so only resolved for parent type Charge
:
scala> Json.toJson(ChargeOne(1))
<console>:17: error: No Json serializer found for type ChargeOne. Try to implement an implicit Writes or Format for this type.
Json.toJson(ChargeOne(1))
^
scala> Json.toJson(ChargeOne(1): Charge)
res1: play.api.libs.json.JsValue = {"one":1,"_type":"ChargeOne"}
If you don't want to have to annotate on each toJson
:
import play.api.libs.json._ // BTW Recommend to provide import in code examples
sealed trait Charge {}
case class ChargeOne(one: Int) extends Charge
case class ChargeTwo(two: Int) extends Charge
object Charge {
implicit val format: OFormat[Charge] = {
// Need to define instance for the subtypes (no auto-materialization)
implicit def one = Json.format[ChargeOne]
implicit def two = Json.format[ChargeTwo]
Json.format[Charge]
}
implicit def genericWrites[T <: Charge]: OWrites[T] =
format.contramap[T](c => c: Charge)
implicit def genericReads[T <: Charge](implicit evidence: scala.reflect.ClassTag[T]): Reads[T] = format.collect[T](JsonValidationError(s"Type mismatch: ${evidence.runtimeClass.getName}")) {
case `evidence`(t) => t
}
}
scala> Json.toJson(ChargeOne(1))
res0: play.api.libs.json.JsValue = {"one":1,"_type":"ChargeOne"}
scala> Json.toJson(ChargeOne(1)).validate[Charge]
res0: play.api.libs.json.JsResult[Charge] = JsSuccess(ChargeOne(1),)
Note#1: It's important to see that (de)serialization ChargeOne
(or any subtype) as Charge
(or any similar parent type) to/from JSON is not the same as doing it "directly". The JSON representation is not the same as sealed family serialization requires a discriminator JSON field (see _type
).
scala> Json.writes[ChargeOne].writes(ChargeOne(1))
res1: play.api.libs.json.JsObject = {"one":1}
scala> Json.toJson(ChargeOne(1)) // .. as Charge parent type
res2: play.api.libs.json.JsValue = {"one":1,"_type":"ChargeOne"}
Note#2: If some similar use cases were wrongly working in previous versions, it was leading to hardly predictable JSON representation, and critical implicit resolution in a lot of cases, which is the reason for this behavior change/fix (see pull request).
来源:https://stackoverflow.com/questions/62592017/play-framework-2-8-2-no-json-serializer-found-for-type-subclass