I'm experimenting with Scala Play and don't get why Null json Serialization doesn't work out of the box. I wrote a simple class to encapsulate data in response (ServiceResponse) which return a parametrical nullable field data
and also made a Writes[Null] implicit, what am I missing? The compiler suggests to write a Writes[ServiceResponse[Null]] which works, but feels cumbersome and I wonder if there is a more concise way of solving the problem. Below is the code and error.
// <!-- EDIT -->
// The data I want to transfer
case class Person (name: String, age: Int)
object Person {
def apply(name: String, age: Int): Person = new Person(name, age)
implicit val reader: Reads[Person] = Json.reads[Person]
implicit val writer: OWrites[Person] = Json.writes[Person]
}
// <!-- END EDIT -->
case class ServiceResponse[T] (data: T, errorMessage: String, debugMessage: String)
object ServiceResponse {
def apply[T](data: T, errorMessage: String, debugMessage: String): ServiceResponse[T] =
new ServiceResponse(data, errorMessage, debugMessage)
implicit def NullWriter: Writes[Null] = (_: Null) => JsNull
implicit def writer[T](implicit fmt: Writes[T]) = Json.writes[ServiceResponse[T]]
}
// <!-- EDIT -->
// Given the above code I would expect
// the call ` Json.toJson(ServiceResponse(null, "Error in deserializing json", null))`
// to produce the following Json: `{ "data": null, "errorMessage": "Error in deserializing json", "debugMessage": null }`
No Json serializer found for type models.ServiceResponse[Null]. Try to implement an implicit Writes or Format for this type.
In C:\...\EchoController.scala:19
15 val wrapAndEcho = Action(parse.json) {
16 request =>
17 request.body.validate[Person] match {
18 case JsSuccess(p, _) => Ok(Json.toJson(echoService.wrapEcho(p)))
19 case JsError(errors) => BadRequest(Json.toJson(ServiceResponse(null,
20 "Error in deserializing json", null)))
21 }
22 }
EDIT
Tried to use Option instead (a more Scala-ish solution, indeed) but the structure of the code is the same and reports the same error
object ServiceResponse {
def apply[T](data: Option[T], errorMessage: Option[String], debugMessage: Option[String]): ServiceResponse[T] =
new ServiceResponse(data, errorMessage, debugMessage)
implicit def optionWriter[T](implicit fmt: Writes[T]) = new Writes[Option[T]] {
override def writes(o: Option[T]): JsValue = o match {
case Some(value) => fmt.writes(value)
case None => JsNull
}
}
implicit def writer[T](implicit fmt: Writes[Option[T]]) = Json.writes[ServiceResponse[T]]
}
I think I need some structural change, but I can't figure what. Also tried changing the type of fmt to Writes[T]
(being T the only real variable type) and got a new Error.
diverging implicit expansion for type play.api.libs.json.Writes[T1]
[error] starting with object StringWrites in trait DefaultWrites
[error] case JsError(errors) => BadRequest(Json.toJson(ServiceResponse(None,
...
Thought I found a reasonable solution, but still getting error in the expansion of the implicit :( . I really need a hint.
object ServiceResponse {
def apply[T](data: Option[T], errorMessage: Option[String], debugMessage: Option[String]): ServiceResponse[T] =
new ServiceResponse(data, errorMessage, debugMessage)
implicit def writer[T](implicit fmt: Writes[T]): OWrites[ServiceResponse[T]] = (o: ServiceResponse[T]) => JsObject(
Seq(
("data", o.data.map(fmt.writes).getOrElse(JsNull)),
("errorMessage", o.errorMessage.map(JsString).getOrElse(JsNull)),
("debugMessage", o.debugMessage.map(JsString).getOrElse(JsNull))
)
)
}
NOTE
I think I got the problem. Since null is a possible value of an instance of Person I was expecting it to be handled by Writes[Person]. The fact is that Writes is defined as invariant in its type paramater (it's trait Writes[A]
not trait Writes[+A]
) so Writes[Null]
does not match the definition of the implicit parameter as would happen if it was defined as Writes[+A]
(Which in turns would be wrong violating Liskow substitution principle; it could have been Writes[-A]
, but this would have not solved the problem either, as we are trying to use the subtype Null of Person). Summing up: there is no shorter way to handle a ServiceResponse with a null data field than writing a specific implementation of Writes[ServiceResponse[Null]]
, which is neither a super nor a sub type of Write[ServiceResponse[Person]]
. (An approach could be a union type, but I think it's an overkill). 90% sure of my reasoning, correct me if I'm wrong :)
To explain it better, the ServiceResponse case class takes a type parameter T, which can be anything. And play-json can only provide JSON formats for standard scala types, for custom types you need to define the JSON formatter.
import play.api.libs.json._
case class ServiceResponse[T](
data: T,
errorMessage: Option[String],
debugMessage: Option[String]
)
def format[T](implicit format: Format[T]) = Json.format[ServiceResponse[T]]
implicit val formatString = format[String]
val serviceResponseStr = ServiceResponse[String]("Hello", None, None)
val serviceResponseJsValue = Json.toJson(serviceResponseStr)
val fromString =
Json.fromJson[ServiceResponse[String]](serviceResponseJsValue)
serviceResponseJsValue
fromString
serviceResponseJsValue.toString
Json.parse(serviceResponseJsValue.toString).as[ServiceResponse[String]]
In the above example, you can see that I wanted to create a ServiceResponse with data being a string, so I implement a format string which's necessary for Json.toJson, as well as Json.fromJson to have the readers and writers implemented for the type T. Since T being String and is a standard type, play-json by default is resolving the same.
I have added the scastie snippet, which will help you understand the same better, and you can play around with the same.
<script src="https://scastie.scala-lang.org/shankarshastri/spqJ1FQLS7ym1vm1ugDFEA/8.js"></script>
The above explanation suffices a use-case wherein in case of None, the key won't even be present as part of the json, but the question clearly calls out for having key: null
, in case if data is not found.
import play.api.libs.json._
implicit val config =
JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)
case class ServiceResponse[T](
data: Option[T],
errorMessage: Option[String],
debugMessage: Option[String]
)
def format[T](implicit format: Format[T]) = Json.format[ServiceResponse[T]]
implicit val formatString = format[String]
val serviceResponseStr = ServiceResponse[String](None, None, None)
val serviceResponseJsValue = Json.toJson(serviceResponseStr)
val fromString =
Json.fromJson[ServiceResponse[String]](serviceResponseJsValue)
serviceResponseJsValue
fromString
serviceResponseJsValue.toString
Json.parse(serviceResponseJsValue.toString).as[ServiceResponse[String]]
Bringing the below line in the scope, will ensure to write nulls for optional.
implicit val config =
JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)
<script src="https://scastie.scala-lang.org/shankarshastri/rCUmEqXLTeuGqRNG6PPLpQ/6.js"></script>