Reputation: 32284
I have a Scala case class
case class NumericParam(minValue: Option[Int] = None,
maxValue: Option[Int] = None,
decimalPlaces: Option[Int] = None,
signed: Option[Boolean] = None,
hintText: Option[String] = None)
and its companion object, where I defined an implicit writes
method
object NumericParam {
implicit val writes = new Writes[NumericParam] {
override def writes(value: NumericParam): JsValue = {
Json.obj(
"dataType" -> "Numeric",
"minValue" -> value.maxValue,
"maxValue" -> value.maxValue,
"decimalPlaces" -> value.decimalPlaces,
"signed" -> value.signed,
"hintText" -> value.hintText
)
}
}
}
I am adding the field dataType
. Is there any way to use the macro-derived Writes
value (from Json.writes[NumericParam]
) and just add the additional dataType
field?
Upvotes: 2
Views: 1440
Reputation: 55569
You can use Writes#transform
to do it. One way would be to use a function JsValue => JsValue
. The safe way:
implicit val writes = Json.writes[NumericParam] transform { js =>
js match {
case obj: JsObject => obj + ("dataType" -> JsString("Numeric"))
case _ => js
}
}
However, we really know that js
should always be a JsObject
, since we're operating on a specific type, so we can shorten it.
implicit val writes = Json.writes[NumericParam]
.transform(_.as[JsObject] + ("dataType" -> JsString("Numeric")))
Example:
scala> val num = NumericParam(Some(1), Some(10), Some(2), Some(false), Some("test"))
num: NumericParam = NumericParam(Some(1),Some(10),Some(2),Some(false),Some(test))
scala> Json.toJson(num)
res5: play.api.libs.json.JsValue = {"minValue":1,"maxValue":10,"decimalPlaces":2,"signed":false,"hintText":"test","dataType":"Numeric"}
To make the above more type-safe and generic, we can use some implicit magic to extend OWrites
(which always writes to JsObject
).
implicit class OWritesExt[A](owrites: OWrites[A]) {
/** Add a (key, value) pair to the JSON object,
* where the value is constant.
*/
def withConstant[B : Writes](key: String, value: B): OWrites[A] =
withValue(key, _ => value)
/** Add a key, value pair to the JSON object that is written, where
* the value depends on the original object.
*/
def withValue[B : Writes](key: String, value: A => B): OWrites[A] =
new OWrites[A] {
def writes(a: A): JsObject = owrites.writes(a) ++ Json.obj(key -> value(a))
}
}
Usage:
implicit val writes = Json.writes[NumericParam]
.withValue("type", _.getClass.toString)
.withConstant("other", "something")
scala> Json.toJson(NumericParam(Some(1)))
res6: play.api.libs.json.JsValue = {"minValue":1,"type":"class NumericParam","other":"something"}
Now you can scrap some of the original boilerplate, and chain calls like this together. Now I'm just wondering why I haven't been doing this all along.
Upvotes: 7