2017-03-15 2 views
1

Ich möchte eine Basis entwerfen Merkmal/Klasse in Scala, die den folgenden json produzieren können:Wie kann ich ein dynamisches Basis-JSON-Objekt definieren?

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

ich dieses Merkmal in einem Fall, Klasse erben würde:

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

die Ausgabe sollte wie folgt aussehen : {"product":{"name":"car"}}

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

Ich kämpfe mit den richtigen Abstraktionen, um dies zu erreichen. Ich bin offen für Lösungen mit einer beliebigen JSON-Bibliothek (verfügbar in Scala).

Antwort

2

Hier ist über die einfachste Art der ich denken kann, den Singular Teil allgemein mit circe zu tun:

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

Und dann, wenn Sie einige Fallklasse haben GenericResource wie diese erstreckt:

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

Sie kann dies tun (vorausgesetzt, dass alle Mitglieder der Fallklasse codierbar sind):

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

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

Kein vorformulierten, keine zusätzlichen Importe usw.

Der Seq Fall ist ein wenig komplizierter, da circe einen Seq[A] Encoder für jede A liefert automatisch die eine Encoder hat, aber es nicht tut, was Sie wollen-es einfach codiert die Objekte und steckt sie in ein JSON-Array. Sie können so etwas schreiben:

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

Und es wie folgt verwenden:

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

Aber man kann nicht einfach kleben Sie es in dem Begleitobjekt und erwarten, alles zu arbeiten-Sie haben es zu setzen irgendwo und importieren Sie es, wenn Sie es brauchen (ansonsten hat es die gleiche Priorität wie die Standard Seq[A] Instanzen).

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

Dies liegt daran, dass der Plural Name der Ressource auf Instanzebene angebracht ist, und wenn Sie: wenn die Seq ist leer

Ein weiteres Problem mit dieser encodeResources Implementierung ist, dass es nur ein leeres Objekt zurück habe keine Instanz, es gibt keine Möglichkeit, es zu bekommen (kurz vor dem Nachdenken). Sie könnten natürlich eine falsche Instanz heraufbeschwören, indem Sie Nullen an den Konstrukteur oder Ähnliches übergeben, aber das scheint außerhalb des Umfangs dieser Frage zu liegen.

Dieses Problem (die Ressourcennamen, die an Instanzen angehängt werden) wird ebenfalls problematisch, wenn Sie diesen JSON dekodieren müssen, den Sie codiert haben. Wenn das der Fall ist, würde ich vorschlagen, einen etwas anderen Ansatz zu wählen, bei dem Sie beispielsweise ein GenericResourceCompanion Merkmal haben, das Sie in das Begleitobjekt für den spezifischen Ressourcentyp mischen und die Namen dort angeben. Wenn das keine Option ist, sind Sie wahrscheinlich mit Reflektions- oder Fake-Instanzen oder beidem beschäftigt (aber wiederum wahrscheinlich nicht im Umfang für diese Frage).

+0

Wow, danke für die detaillierte Lösung. –

Verwandte Themen