简体   繁体   中英

Null serialization in Scala Play (implicit Writes)

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>

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