简体   繁体   中英

How can I define a dynamic base json object?

I would like to design a base trait/class in Scala that can produce the following json:

trait GenericResource {
  val singularName: String
  val pluralName: String
}

I would inherit this trait in a case class:

case class Product(name: String) extends GenericResource {
  override val singularName = "product"
  override val pluralName = "products"
}
val car = Product("car")
val jsonString = serialize(car)

the output should look like: {"product":{"name":"car"}}

A Seq[Product] should produce {"products":[{"name":"car"},{"name":"truck"}]} etc...

I'm struggling with the proper abstractions to accomplish this. I am open to solutions using any JSON library (available in Scala).

Here's about the simplest way I can think of to do the singular part generically with circe :

import io.circe.{ Decoder, Encoder, Json }
import io.circe.generic.encoding.DerivedObjectEncoder

trait GenericResource {
  val singularName: String
  val pluralName: String
}

object GenericResource {
  implicit def encodeResource[A <: GenericResource](implicit
    derived: DerivedObjectEncoder[A]
  ): Encoder[A] = Encoder.instance { a =>
    Json.obj(a.singularName -> derived(a))
  }
}

And then if you have some case class extending GenericResource like this:

case class Product(name: String) extends GenericResource {
  val singularName = "product"
  val pluralName = "products"
}

You can do this (assuming all the members of the case class are encodeable):

scala> import io.circe.syntax._
import io.circe.syntax._

scala> Product("car").asJson.noSpaces
res0: String = {"product":{"name":"car"}}

No boilerplate, no extra imports, etc.

The Seq case is a little trickier, since circe automatically provides a Seq[A] encoder for any A that has an Encoder , but it doesn't do what you want—it just encodes the items and sticks them in a JSON array. You can write something like this:

implicit def encodeResources[A <: GenericResource](implicit
  derived: DerivedObjectEncoder[A]
): Encoder[Seq[A]] = Encoder.instance {
  case values @ (head +: _) => 
    Json.obj(head.pluralName -> Encoder.encodeList(derived)(values.toList))
  case Nil => Json.obj()
}

And use it like this:

scala> Seq(Product("car"), Product("truck")).asJson.noSpaces
res1: String = {"products":[{"name":"car"},{"name":"truck"}]}

But you can't just stick it in the companion object and expect everything to work—you have to put it somewhere and import it when you need it (otherwise it has the same priority as the default Seq[A] instances).

Another issue with this encodeResources implementation is that it just returns an empty object if the Seq is empty:

scala> Seq.empty[Product].asJson.noSpaces
res2: String = {}

This is because the plural name is attached to the resource at the instance level, and if you don't have an instance there's no way to get it (short of reflection). You could of course conjure up a fake instance by passing nulls to the constructor or whatever, but that seems out of the scope of this question.

This issue (the resource names being attached to instances) is also going to be trouble if you need to decode this JSON you've encoded. If that is the case, I'd suggest considering a slightly different approach where you have something like a GenericResourceCompanion trait that you mix into the companion object for the specific resource type, and to indicate the names there. If that's not an option, you're probably stuck with reflection or fake instances, or both (but again, probably not in scope for this question).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM