How to serialize and deserialize traits, to and from Json, in Scala? - json

First Attempt:
So far I have tried spray-json. I have:
trait Base
case class A ( id: String) extends Base
case class B (id: String) extends Base
Now, for serializing and deserializing my Base type, I have the code:
implicit object BaseFormat extends RootJsonFormat[Base]{
def write(obj: Base): JsValue = {
obj match {
case a: A => a.toJson
case b: B => b.toJson
case unknown # _ => serializationError(s"Marshalling issue with ${unknown}")
}
}
def read(json: JsValue): Base = {
//how to know whether json is encoding an A or a B?
}
}
The problem is that, for implementing the read method for deserialization, I can't figure out a way to know whether the JsValue is encoding an A or a B.
Second Attempt:
For solving this in spray-json, I ended up simply renaming the field id in A to aID, and in B to bID.
Third Attempt:
Since spray-json was not as sophisticated as the alternative libraries such as zio-json or circe, which handle this issue by themselves without additional code, I started using zio-json
Now I get the error
magnolia: could not infer DeriveJsonEncoder.Typeclass for type
for all the case classes taking type parameters.
Also, it has problems with chained trait inheritance. It seems like circe uses magnolia, too. So it’s likely this would be replicated with circe, as well.
Any help would be appreciated.

You should address this problem using a json encoding/decoding library.
Here is an example using circe and it's semi-automatic mode.
Since you mentionned in the comments that you are struggling with generic types in your case classes, I'm also including that. Basically, to derive an encoder or decoder for a class Foo[T] that contains a T, you have to prove that there is a way to encode and decode T. This is done by asking for an implicit Encoder[T] and Decoder[T] where you derive Encoder[Foo[T]] and Decoder[Foo[T]]. You can generalize this reasoning to work with more than one generic type of course, you just have to have an encoder/decoder pair implicitly available where you derive the encoder/decoder for the corresponding case class.
import io.circe._, io.circe.generic.semiauto._, io.circe.syntax._
case class Foo[T](a: Int, b: String, t: T)
object Foo {
implicit def decoder[T: Encoder: Decoder]: Decoder[Foo[T]] = deriveDecoder
implicit def encoder[T: Encoder: Decoder]: Encoder[Foo[T]] = deriveEncoder
}
case class Bar(a: Int)
object Bar {
implicit val encoder: Encoder[Bar] = deriveEncoder
implicit val decoder: Decoder[Bar] = deriveDecoder
}
case class Baz(a: Int)
println(Foo(42, "hello", 23.4).asJson) // Works because circe knows Encoder[Float]
println(Foo(42, "hello", Bar(42)).asJson) // Works because we defined Encoder[Bar]
//println(Foo(42, "hello", Baz(42)).asJson) // Doesn't compile: circe doesn't know Encoder[Baz] in semi-auto mode
Try it live on Scastie
Note that a different encoder/decoder for Foo[T] needs to be generated for each type T that you are using, which is why the encoder and decoder derivation for Foo have to be methods, not values like for Bar.
There is also a fully-automatic mode, but it tends to produce compile-time errors that are harder to debug for beginner, so I would start with semi-auto. Another problem, is that auto-mode can take much longer to compile on large projects. If you're feeling adventurous, it's even more beautiful when you don't make mistakes!
import io.circe._, io.circe.generic.auto._, io.circe.syntax._
case class Foo[T](a: Int, b: String, t: T)
case class Bar(a: Int)
case class Baz(a: Int)
println(Foo(42, "hello", 23.4).asJson) // circe knows Encoder[Float] and automatically derives Encoder[Foo[Float]]
println(Foo(42, "hello", Bar(42)).asJson) // circe wants Encoder[Foo[Bar]], so it ma(cro)gically derives Encoder[Bar] and then Encoder[Foo[Bar]]
println(Foo(42, "hello", Baz(42)).asJson) // Does compile this time, circe automatically find the encoder/decoder for Baz the same way it does for Bar
Try it live on Scastie

A working example using traits with generics is this:
import io.circe.Decoder.Result
import io.circe._
import io.circe.generic.semiauto._
import io.circe.syntax._
trait MyBase[T <: MyBase[T]] {
def myid: String
def issue: MyBaseIssue
def summary: MyBaseSummary[T]
}
trait MyBaseIssue
object MyBaseIssue {
implicit val decodeIssue: Decoder[MyBaseIssue] =
Decoder[SimpleBaseIssue].map[MyBaseIssue](identity).or(
Decoder[SophisticatedBaseIssue].map[MyBaseIssue](identity)
)
implicit val encodeIssue: Encoder[MyBaseIssue] = Encoder.instance {
case simple # SimpleBaseIssue(_) => simple.asJson
case sophisticated # SophisticatedBaseIssue(_) => sophisticated.asJson
}
}
trait MyBaseSummary[T <: MyBase[T]]
object MyBaseSummary {
implicit def decodeSummary[T <: MyBase[T]: Encoder: Decoder]
: Decoder[MyBaseSummary[T]] =
Decoder[SimpleBaseSummary].map[MyBaseSummary[SimpleBase]](identity).asInstanceOf[Decoder[MyBaseSummary[T]]].or(
Decoder[SophisticatedBaseSummary].map[MyBaseSummary[SophisticatedBase]](identity).asInstanceOf[Decoder[MyBaseSummary[T]]]
)
implicit def encodeSummary[T <: MyBase[T]: Encoder: Decoder]
: Encoder[MyBaseSummary[T]] = Encoder.instance {
case simple # SimpleBaseSummary(_) => simple.asJson
case sophisticated # SophisticatedBaseSummary(_) => sophisticated.asJson
}
}
case class SimpleBase(
myid: String,
override val issue: SimpleBaseIssue,
override val summary: SimpleBaseSummary
) extends MyBase[SimpleBase]
case class SophisticatedBase(
myid: String,
extraField: String,
override val issue: SophisticatedBaseIssue,
override val summary: SophisticatedBaseSummary
) extends MyBase[SophisticatedBase]
object SimpleBase {
implicit val encoder: Encoder[SimpleBase] = deriveEncoder
implicit val decoder: Decoder[SimpleBase] = deriveDecoder
}
object SophisticatedBase {
implicit val encoder: Encoder[SophisticatedBase] = deriveEncoder
implicit val decoder: Decoder[SophisticatedBase] = deriveDecoder
}
case class SimpleBaseIssue(simpleIssueType: String) extends MyBaseIssue
case class SophisticatedBaseIssue(sophisticatedIssueType: String) extends MyBaseIssue
object SimpleBaseIssue {
implicit val encoder: Encoder[SimpleBaseIssue] = deriveEncoder
implicit val decoder: Decoder[SimpleBaseIssue] = deriveDecoder
}
object SophisticatedBaseIssue {
implicit val encoder: Encoder[SophisticatedBaseIssue] = deriveEncoder
implicit val decoder: Decoder[SophisticatedBaseIssue] = deriveDecoder
}
case class SimpleBaseSummary(simpleSummary: String) extends MyBaseSummary[SimpleBase]
case class SophisticatedBaseSummary(sophisticatedSummary: String) extends MyBaseSummary[SophisticatedBase]
object SimpleBaseSummary {
implicit val encoder: Encoder[SimpleBaseSummary] = deriveEncoder
implicit val decoder: Decoder[SimpleBaseSummary] = deriveDecoder
}
object SophisticatedBaseSummary {
implicit val encoder: Encoder[SophisticatedBaseSummary] = deriveEncoder
implicit val decoder: Decoder[SophisticatedBaseSummary] = deriveDecoder
}
case class MyBaseList[T <: MyBase[T]](
myid: String,
var membersMap: Map[String, T] = Map.empty,
override val issue: MyBaseListIssues[T],
override val summary: MyBaseListSummary[T]
) extends MyBase[MyBaseList[T]]
object MyBaseList {
implicit def membersMapDecoder[T <: MyBase[T]: Encoder: Decoder]
: Decoder[Map[String, T]] = Decoder.decodeMap
implicit def membersMapEncoder[T <: MyBase[T]: Encoder: Decoder]
: Encoder[Map[String, T]] = Encoder.encodeMap
implicit def encoder[T <: MyBase[T]: Encoder: Decoder]
: Encoder[MyBaseList[T]] = deriveEncoder
implicit def decoder[T <: MyBase[T]: Encoder: Decoder]
: Decoder[MyBaseList[T]] = deriveDecoder
}
case class MyBaseListIssues[T <: MyBase[T]](
issue: String,
var set: Set[MyBaseIssue] = Set[MyBaseIssue]()
) extends MyBaseIssue
object MyBaseListIssues {
implicit def membersMapDecoder: Decoder[Set[MyBaseIssue]] =
Decoder.decodeSet[MyBaseIssue]
implicit def membersMapEncoder: Encoder[Set[MyBaseIssue]] = Encoder.encodeSet
implicit def encoder[T <: MyBase[T]: Encoder: Decoder]
: Encoder[MyBaseListIssues[T]] = deriveEncoder
implicit def decoder[T <: MyBase[T]: Encoder: Decoder]
: Decoder[MyBaseListIssues[T]] = deriveDecoder
}
case class MyBaseListSummary[T <: MyBase[T]](
summaryList: Map[String, MyBaseSummary[T]]
) extends MyBaseSummary[MyBaseList[T]]
object MyBaseListSummary {
implicit def membersMapDecoder[T <: MyBase[T]: Encoder: Decoder]
: Decoder[Map[String, MyBaseSummary[T]]] = Decoder.decodeMap
implicit def membersMapEncoder[T <: MyBase[T]: Encoder: Decoder]
: Encoder[Map[String, MyBaseSummary[T]]] = Encoder.encodeMap
implicit def encoder[T <: MyBase[T]: Encoder: Decoder]
: Encoder[MyBaseListSummary[T]] = deriveEncoder
implicit def decoder[T <: MyBase[T]: Encoder: Decoder]
: Decoder[MyBaseListSummary[T]] = deriveDecoder
}
object MainObject extends App {
val simpleList = MyBaseList[SimpleBase]("simpleId",
Map("one" -> SimpleBase("baseid", SimpleBaseIssue("the mistake"), SimpleBaseSummary("very concise"))),
MyBaseListIssues[SimpleBase]("listIssue", Set(SimpleBaseIssue("a disaster"))),
MyBaseListSummary[SimpleBase]( Map("simplebaseid" -> SimpleBaseSummary("super concise")))
)
val simpleJson = simpleList.asJson
println(simpleJson)
val convertedSimpleList = simpleJson.as[MyBaseList[SimpleBase]]
println(convertedSimpleList)
val sphisticatedList = MyBaseList[SophisticatedBase]("sophisticatedId",
Map("one" -> SophisticatedBase("baseid", "further detail", SophisticatedBaseIssue("the mistake"), SophisticatedBaseSummary("very concise"))),
MyBaseListIssues[SophisticatedBase]("listIssue", Set(SophisticatedBaseIssue("a disaster"))),
MyBaseListSummary[SophisticatedBase]( Map("sophisticatedbaseid" -> SophisticatedBaseSummary("super concise")))
)
val sophisticatedJson = sphisticatedList.asJson
println(sophisticatedJson)
val convertedSophisticatedListDecoder: Result[MyBaseList[SophisticatedBase]] = sophisticatedJson.as[MyBaseList[SophisticatedBase]]
println(convertedSophisticatedListDecoder)
convertedSophisticatedListDecoder match {
case Left(failure) => println(failure)
case Right(list) => println(list)
}
}
It is still essential to make sure every case class inheriting a particular trait has a different number of fields from the others inheriting the same trait, or it at least differs with the other case classes in the naming of those fields.
Otherwise, the decoder would not do its job correctly and decode the json as the first case class matching it in the decoder function, instead of the correct intended one.

Related

Scala - Ignore case class field when decoding JSON

I'm playing with the example ADT in the circe documentation to reproduce an issue that I have with JSON decoding.
To achieve that, I'm using ShapesDerivation :
scala> object ShapesDerivation {
|
| implicit def encodeAdtNoDiscr[Event, Repr <: Coproduct](implicit
| gen: Generic.Aux[Event, Repr],
| encodeRepr: Encoder[Repr]
| ): Encoder[Event] = encodeRepr.contramap(gen.to)
|
| implicit def decodeAdtNoDiscr[Event, Repr <: Coproduct](implicit
| gen: Generic.Aux[Event, Repr],
| decodeRepr: Decoder[Repr]
| ): Decoder[Event] = decodeRepr.map(gen.from)
|
| }
defined object ShapesDerivation
The ADT to decode is composed by two values : a simple case class and another one that I have dedicated Encoder / Decoder (to reproduce in minimal example the issue that I really have) :
scala> :paste
// Entering paste mode (ctrl-D to finish)
sealed trait Event
object Event {
case class Foo(i: Int) extends Event
case class Bar(f : FooBar) extends Event
case class FooBar(x : Int)
implicit val encoderFooBar : Encoder[FooBar] = new Encoder[FooBar] {
override def apply(a: FooBar): Json = Json.obj(("x", Json.fromInt(a.x)))
}
implicit val decodeFooBar: Decoder[FooBar] = new Decoder[FooBar] {
override def apply(c: HCursor): Result[FooBar] =
for {
x <- c.downField("x").as[Int]
} yield FooBar(x)
}
}
Then when I try to decode a simple value like this, it's working well :
scala> import ShapesDerivation._
import ShapesDerivation._
scala> decode[Event](""" { "i" : 10 }""")
res1: Either[io.circe.Error,Event] = Right(Foo(10))
But if I tried to decode something that should be a Bar that contains a Foobar, I get a decoding failure :
scala> decode[Event](""" { "x" : 10 }""")
res2: Either[io.circe.Error,Event] = Left(DecodingFailure(CNil, List()))
But this one works because I explicitely put the case class field name :
scala> decode[Event](""" { "f" : { "x" : 10 }}""")
res7: Either[io.circe.Error,Event] = Right(Bar(FooBar(10)))
I don't what to put the case class field, directly the JSON but I think it's not possible to achieve a such behaviour. The reason why I think it's impossible is how it will know to match the good case class if there is not the field but I want to be sure that there is no way with circe to do that
Here's how you do it using just semi-auto derivation.
import io.circe.Decoder.Result
import io.circe.{Decoder, Encoder, HCursor, Json}
import io.circe.parser._
import io.circe.generic.semiauto._
object Example extends App {
sealed trait Event
object Event {
case class Foo(i: Int) extends Event
object Foo {
implicit val decoder: Decoder[Foo] = deriveDecoder
}
case class Bar(f: FooBar) extends Event
object Bar {
implicit val decoder: Decoder[Bar] = Decoder[FooBar].map(Bar.apply)
}
implicit val decoder: Decoder[Event] = Decoder[Foo].widen.or(Decoder[Bar].widen)
}
case class FooBar(x: Int)
object FooBar {
implicit val encoderFooBar: Encoder[FooBar] = deriveEncoder
implicit val decodeFooBar: Decoder[FooBar] = deriveDecoder
}
println(decode[Event](""" { "x" : 10 }"""))
}
Outputs
Right(Bar(FooBar(10)))
It gets a bit noisy with the explicit decoders, but if you care about compilation speed, it's the way to go since you'll only derive decoders once.

Extracting a case class with an upper bound

I want to extract a case class from a JSON String, and reuse the code for every class.
Something like this question would have been perfect. But this means that I have to write for every class I want to extract.
I was hoping to do something like:
abstract class SocialMonitorParser[C <: SocialMonitorData] extends Serializable {
def toJSON(socialMonitorData: C): String = {
Requirements.notNull(socialMonitorData, "This field cannot be NULL!")
implicit val formats = DefaultFormats
write(socialMonitorData)
}
def fromJSON(json: String): Option[C] = {
implicit val formats = DefaultFormats // Brings in default date formats etc.
val jsonObj = liftweb.json.parse(json)
try {
val socialData = jsonObj.extract[C]
Some(socialData)
} catch {
case e: Exception => {
Logger.get(this.getClass.getName).warn("Unable to parse the following JSON:\n" + json + "\nException:\n" + e.toString())
None
}
}
}
}
But it gives me the following error:
Error:(43, 39) No Manifest available for C.
val socialData = jsonObj.extract[C]
Error:(43, 39) not enough arguments for method extract: (implicit formats: net.liftweb.json.Formats, implicit mf: scala.reflect.Manifest[C])C.
Unspecified value parameter mf.
val socialData = jsonObj.extract[C]
I was hoping I could do something like this, and maybe there is a way. But I can't wrap my head around this.
I will try to extend the question with some other information. Supposing I have Twitter and Facebook data, in case class like these:
case class FacebookData(raw_data: String, id: String, social: String) extends SocialMonitorData
case class TwitterData(...) extends SocialMonitorData{ ...}
I wish I could reuse the fromJSON and toJSON just once passing the Upper Bound type
class TwitterParser extends SocialMonitorParser[TwitterData] {
override FromJSON}
class FacebookParser extends SocialMonitorParser[FacebookData]
Much obliged.
I'm not sure why you want SocialMonitorParser to be abstract or Serializable or how are you going to use it but if you look closer at the error you may see that the compilers wants a Manifest for C. Manifest is a Scala way to preserve type information through the type erasure enforced onto generics by JVM. And if you fix that, then the code like this compiles:
import net.liftweb.json._
import net.liftweb.json.Serialization._
trait SocialMonitorData
case class FacebookData(raw_data: String, id: String, social: String) extends SocialMonitorData
class SocialMonitorParser[C <: SocialMonitorData : Manifest] extends Serializable {
def toJSON(socialMonitorData: C): String = {
// Requirements.notNull(socialMonitorData, "This field cannot be NULL!")
implicit val formats = DefaultFormats
write(socialMonitorData)
}
def fromJSON(json: String): Option[C] = {
implicit val formats = DefaultFormats // Brings in default date formats etc.
val jsonObj = parse(json)
try {
val socialData = jsonObj.extract[C]
Some(socialData)
} catch {
case e: Exception => {
// Logger.get(this.getClass.getName).warn("Unable to parse the following JSON:\n" + json + "\nException:\n" + e.toString())
None
}
}
}
}
and you can use it as
def test(): Unit = {
val parser = new SocialMonitorParser[FacebookData]
val src = FacebookData("fb_raw_data", "fb_id", "fb_social")
println(s"src = $src")
val json = parser.toJSON(src)
println(s"json = $json")
val back = parser.fromJSON(json)
println(s"back = $back")
}
to get the output exactly as one would expect.

Play JSON - How to generify this in Scala for Json handling?

I currently have this in scala, and it does what I want:
private def prepareResponse(response: Array[SomeItem]): String = {
implicit val writes = Json.writes[SomeItem]
Json.stringify(JsObject(Map("message" -> Json.toJson(response))))
}
however, I want to generify this so that I could put it anything as the response and, as long as there are Json.writes defined for the type I'm trying to convert to Json, it would stringify it.
For example:
private def prepareResponse(response: Any): String = {
implicit val writes = Json.writes[SomeItem]
implicit val writes2 = Json.writes[SomeOtherItem]
...
Json.stringify(JsObject(Map("message" -> Json.toJson(response))))
}
This doesn't work, of course, as it says that there is no implicit write defined for Any. Adding one for Any also doesn't work, as I get the error:
No unapply or unapplySeq function found
[scalac-2.11] implicit val writeAny = Json.writes[Any]
[scalac-2.11]
What's an ideal way to do this the "right" way (if any)?
Thanks in advance!
import play.api.libs.json._
case class SomeItem(a: String, b: String)
object SomeItem {
implicit val codec = Json.format[SomeItem]
}
case class SomeOtherItem(a: String, b: String, c: String)
object SomeOtherItem {
implicit val codec = Json.format[SomeOtherItem]
}
// ...
object PlayJson extends App {
def prepareResponse[T](response: T)(implicit tjs: Writes[T]): String = {
Json.stringify(JsObject(Map("message" -> Json.toJson(response))))
}
println(prepareResponse(SomeItem("aa", "bb")))
println(prepareResponse(SomeOtherItem("aa", "bb", "cc")))
// ...
}

Play 2.4 parameterized algebraic data types JSON validation

Is it possible to do something like that in Scala / Play 2.4:
sealed trait Location { val `type`: String }
case class CityLocation(zip_code: String, override val `type`: String = "city") extends Location
case class AddressLocation(
society: Option[String],
first_name: String,
last_name: String,
...
override val `type`: String = "address"
) extends Location
sealed trait Point[T <: Location] { val `type`: String; val location: T }
case class ShippingPoint[T](override val location: T, override val `type`: String = "shipping") extends Point[T]
case class PickupPoint[T](override val location: T, override val `type`: String = "pickup") extends Point[T]
object CityLocation {
implicit val format = Json.format[CityLocation]
}
object AddressLocation {
implicit val format = Json.format[AddressLocation]
}
object ShippingPoint {
implicit def write[T] = Json.writes[ShippingPoint[T]]
implicit def read[T]: Reads[ShippingPoint[T]] = (
(__ \ "location").read[T] and
(__ \ "type").read[String](exactWordRead("shipping", "error.route.shipping.type"))
)(ShippingPoint[T].apply _)
}
object PickupPoint {
implicit def write[T] = Json.writes[ShippingPoint[T]]
implicit def read[T]: Reads[PickupPoint[T]] = (
(__ \ "location").read[T] and
(__ \ "type").read[String](exactWordRead("pickup", "error.route.pickup.type"))
)(PickupPoint[T].apply _)
}
??
For now, It does not compile.
The exactWordRead method is defined as follow :
def exactWordRead(word: String, errorMessage: String): Reads[String] = Reads.constraints.pattern(s"^$word${"$"}".r, errorMessage)
Edit:
It seems that I maid a step forward thanks to this answer https://stackoverflow.com/a/29834113/2431728 :
sealed trait Location { val `type`: String }
case class CityLocation(zip_code: String, override val `type`: String = "city") extends Location
case class AddressLocation(
society: Option[String],
first_name: String,
last_name: String,
...
override val `type`: String = "address"
) extends Location
sealed trait Point[T <: Location] { val location: T; val `type`: String }
case class ShippingPoint[T](override val location: T, override val `type`: String = "shipping") extends Point[T]
case class PickupPoint[T](override val location: T, override val `type`: String = "pickup") extends Point[T]
object CityLocation {
implicit val format = Json.format[CityLocation]
}
object AddressLocation {
implicit val format = Json.format[AddressLocation]
}
object ShippingPoint {
implicit def format[T: Format]: Format[ShippingPoint[T]] = (
(__ \ "location").format[T] and
(__ \ "type").format[String](exactWordRead("shipping", "error.route.shipping.type"))
)(ShippingPoint.apply, unlift(ShippingPoint.unapply))
}
object PickupPoint {
implicit def format[T: Format]: Format[PickupPoint[T]] = (
(__ \ "location").format[T] and
(__ \ "type").format[String](exactWordRead("pickup", "error.route.pickup.type"))
)(PickupPoint.apply, unlift(PickupPoint.unapply))
}
However, I doesn't compile yet. The compile error is :
[error] type arguments [T] do not conform to trait Point's type parameter bounds [T <: services.coliswebApi.data.Location]
[error] case class ShippingPoint[T](override val location: T, override val `type`: String = "shipping") extends Point[T]
[error] ^
[error] type arguments [T] do not conform to trait Point's type parameter bounds [T <: services.coliswebApi.data.Location]
[error] case class PickupPoint[T](override val location: T, override val `type`: String = "pickup") extends Point[T]
As said by the compiler, on one side you have,
sealed trait Point[T <: Location]
... and on the other side,
case class ShippingPoint[T](???) extends Point[T]
case class PickupPoint[T](???) extends Point[T]
But nothing in the declarations of ShippingPoint and PickupPoint is proving the type parameter is compatible with the constraint <: Location, required to extend Point.
So you need to fix that.
case class ShippingPoint[T <: Location](???) extends Point[T]
case class PickupPoint[T <: Location](???) extends Point[T]
I think you also need to bound your type in your format like this
implicit def format[T <: Location: Format]: Format[ShippingPoint[T]] = (
(__ \ "location").format[T] and
(__ \ "type").format[String](exactWordRead("shipping", "error.route.shipping.type"))
)(ShippingPoint.apply, unlift(ShippingPoint.unapply))

Play 2.1 Json serialization for traits?

I have this:
package models
import play.api.libs.json._
import play.api.libs.functional.syntax._
object ModelWrites {
implicit val tmoWrites= Json.writes[TestModelObject]
implicit val ihWrites = Json.writes[IntHolder]
}
case class TestModelObject(s1:String, s2:String)
case class IntHolder(i1:Int, i2:Int)
trait HasInts {
val ints: List[IntHolder]
}
When I do this:
scala> val tmo = new TestModelObject("hello", "world") with HasInts {
val ints = List(IntHolder(1,2), IntHolder(3,4))
}
scala> Json.toJson(tmo)
res0: play.api.libs.json.JsValue = {"s1":"hello","s2":"world"}
how can I implicity serialize the mixed-in val 'ints'? Like:
scala> val someInts = List(IntHolder(8,9), IntHolder(10,11))
someInts: List[models.IntHolder] = List(IntHolder(8,9), IntHolder(10,11))
scala> Json.toJson(someInts)
res1: play.api.libs.json.JsValue = [{"i1":8,"i2":9},{"i1":10,"i2":11}]
Note: if I try: implicit val hasIntsWrites = Json.writes[HasInts] I (expectedly?) get:
[error] Models.scala:10: No unapply function found
[error] implicit val hasIntsWrites = Json.writes[HasInts]
[error] ^
You're not going to be able to use the experimental "Inception" feature (Json.writes[...]) directly here, since that only works on case classes. You can, however, build on the Writes instances that Inception can provide to accomplish what you want with only a very little boilerplate.
Note that I'm ignoring the question of whether mixing in a trait when instantiating a case class like this is a good idea—it probably isn't—but the approach I give here will work in the more general case as well.
First for the classes and imports (no changes here):
case class TestModelObject(s1: String, s2: String)
case class IntHolder(i1: Int, i2: Int)
trait HasInts { val ints: List[IntHolder] }
import play.api.libs.json._
import play.api.libs.functional.syntax._
Now we need to put all our lower-priority instances into a trait to make sure that the compiler will pick the right one, since TestModelObject with HasInts is a subtype of both TestModelObject and HasInts:
trait LowPriorityWritesInstances {
implicit val tmoWrites = Json.writes[TestModelObject]
implicit val ihWrites = Json.writes[IntHolder]
implicit object hiWrites extends OWrites[HasInts] {
def writes(hi: HasInts) = Json.obj("ints" -> hi.ints)
}
}
And now the main event:
object WritesInstances extends LowPriorityWritesInstances {
implicit val tmowhiWrites = new Writes[TestModelObject with HasInts] {
def writes(o: TestModelObject with HasInts) =
tmoWrites.writes(o) ++ implicitly[OWrites[HasInts]].writes(o)
}
}
And we're done:
scala> import WritesInstances._
import WritesInstances._
scala> val tmo = new TestModelObject("hello", "world") with HasInts {
| val ints = List(IntHolder(1, 2), IntHolder(3, 4))
| }
scala> println(Json.toJson(tmo))
{"s1":"hello","s2":"world","ints":[{"i1":1,"i2":2},{"i1":3,"i2":4}]}
As desired.