blob: f6a3f7b7adc2e0603bd9ee3669a734cb85ba51c3 (
plain) (
tree)
|
|
package spray.json
import magnolia._
import scala.language.experimental.macros
/** Mixin that enables derivation of JSON formats for any product
* (case classes) or coproduct (sealed traits) types. */
trait DerivedFormats { self: BasicFormats =>
type Typeclass[T] = JsonFormat[T]
/** Convert the name of a parameter to that of a field in a JSON object. This
* method can be overriden to use alternative naming conventions. */
@inline def extractFieldName(paramName: String): String = paramName
/** Determines if `None` instances of options should be included in JSON output.
*
* By default, `None` values are ommitted entirely from resulting JSON
* objects. If overridden, they will be included as `null`s instead.
*
* Note that this has no effect in *reading* option types; undefined or null
* values are always converted to `None`. */
def printNull: Boolean = false
def combine[T](ctx: CaseClass[JsonFormat, T]): JsonFormat[T] =
new JsonFormat[T] {
override def write(value: T): JsValue = {
val fields: Seq[(String, JsValue)] = ctx.parameters.collect {
case param
if !param.option || param.dereference(value) != None || printNull =>
extractFieldName(param.label) -> param.typeclass.write(
param.dereference(value)
)
}
JsObject(fields: _*)
}
override def read(value: JsValue): T = value match {
case obj: JsObject =>
if (ctx.isObject) {
ctx.rawConstruct(Seq.empty)
} else {
ctx.construct { param =>
val fieldName = extractFieldName(param.label)
val fieldValue = if (param.option) {
obj.fields.getOrElse(fieldName, JsNull)
} else {
obj.fields(fieldName)
}
param.typeclass.read(fieldValue)
}
}
case js =>
deserializationError(
s"Cannot read JSON '$js' as a ${ctx.typeName.full}"
)
}
}
def dispatch[T](ctx: SealedTrait[JsonFormat, T]): JsonFormat[T] = {
val typeFieldName = ctx.annotations
.collectFirst {
case g: adt => g.typeFieldName
}
.getOrElse("type")
new JsonFormat[T] {
override def write(value: T): JsValue = ctx.dispatch(value) { sub =>
sub.typeclass.write(sub.cast(value)) match {
case obj: JsObject =>
JsObject(
(Map(typeFieldName -> JsString(sub.typeName.short)) ++
obj.fields).toSeq: _*
)
case js => js
}
}
override def read(js: JsValue): T = {
val typeName: String = js match {
case obj: JsObject if obj.fields.contains(typeFieldName) =>
obj.fields(typeFieldName).convertTo[String]
case JsString(str) =>
str
case _ =>
deserializationError(
s"Cannot deserialize JSON to ${ctx.typeName.full} " +
"because serialized type cannot be determined."
)
}
ctx.subtypes.find(_.typeName.short == typeName) match {
case Some(tpe) => tpe.typeclass.read(js)
case None =>
deserializationError(
s"Cannot deserialize JSON to ${ctx.typeName.full} " +
s"because type '${typeName}' is unsupported."
)
}
}
}
}
def jsonFormat[T]: RootJsonFormat[T] =
macro DerivedFormatMacros.derivedFormat[T]
}
@deprecated("use DerivedJsonProtocol", "spray-json-derivation 0.4.3")
object DerivedFormats extends DerivedFormats with DefaultJsonProtocol
trait ImplicitDerivedFormats extends DerivedFormats { self: BasicFormats =>
implicit def implicitJsonFormat[T]: RootJsonFormat[T] =
macro DerivedFormatMacros.derivedFormat[T]
}
@deprecated("use ImplicitDerivedJsonProtocol", "spray-json-derivation 0.4.3")
object ImplicitDerivedFormats
extends ImplicitDerivedFormats
with DefaultJsonProtocol
trait DerivedJsonProtocol extends DefaultJsonProtocol with DerivedFormats
object DerivedJsonProtocol extends DerivedJsonProtocol
trait ImplicitDerivedJsonProtocol
extends DefaultJsonProtocol
with ImplicitDerivedFormats
object ImplicitDerivedJsonProtocol extends ImplicitDerivedJsonProtocol
object DerivedFormatMacros {
import scala.reflect.macros.whitebox._
/** Utility that converts a magnolia-generated JsonFormat to a RootJsonFormat. */
def derivedFormat[T: c.WeakTypeTag](c: Context): c.Tree = {
import c.universe._
val tpe = weakTypeOf[T]
val sprayPkg = c.mirror.staticPackage("spray.json")
val valName = TermName(c.freshName("format"))
q"""{
val $valName = ${Magnolia.gen[T](c)}
new $sprayPkg.RootJsonFormat[$tpe] {
def write(value: $tpe) = $valName.write(value)
def read(value: $sprayPkg.JsValue) = $valName.read(value)
}
}"""
}
}
|