Jonik
Jonik

Reputation: 81812

Explicitly output JSON null in case of missing optional value

Consider this example using Play's JSON API (play.api.libs.json):

case class FooJson(
   // lots of other fields omitted
   location: Option[LocationJson]
)

object FooJson {
  implicit val writes = Json.writes[FooJson]
}

and

case class LocationJson(latitude: Double, longitude: Double)

object LocationJson {
  implicit val writes = Json.writes[LocationJson]
}

If location is None, the resulting JSON won't have location field at all. This is fine and understadable. But if I wanted for some reason (say, to make my API more self-documenting), how can I explicitly output null in JSON?

{      
  "location": null
}

I also tried defining the field as location: LocationJson and passing option.orNull to it, but it does not work (scala.MatchError: null at play.api.libs.json.OWrites$$anon$2.writes). For non-custom types such as String or Double, this approach would produce null in JSON output.

So, while using Json.writes[FooJson] as shown above (or something equally simple, i.e. not having to write a custom Writes implementation), is there a clean way to include nulls in JSON?

What I'm asking is analogous to JsonInclude.Include.ALWAYS in the Jackson library (also Jackson's default behaviour). Similarly in Gson this would be trivial
(new GsonBuilder().serializeNulls().create()).

Play 2.4.4

Upvotes: 10

Views: 9983

Answers (4)

Alan Thomas
Alan Thomas

Reputation: 1034

Hello from the future.

As of Play 2.7, a fairly simple solution was introduced for automated JSON codecs. We can introduce the appropriate implicit value for JsonConfiguration in the scope for the Format/Reads/Writes. The following configuration will write nulls for empty Options instead of omitting the fields entirely.

import play.api.libs.json._

implicit val config = JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)
implicit val residentWrites = Json.writes[Resident]

Reference

Upvotes: 3

Shankar P
Shankar P

Reputation: 47

if we have null values then we have to add the option with members in case class which will resolve the issue

case class response(
 name:String,
 age: option[int]
)

 object response {
    implicit val format = Json.format[response]
  }

Here the option is the answer for us. and if we are the JSON response for age is coming as null and this will handle the solution for us.

Upvotes: -2

Jonik
Jonik

Reputation: 81812

Greg Methvin, a Play committer, wrote this answer to me in a related GitHub issue:

The JSON macros only support one way of encoding optional values, which is to omit None values from the JSON. This is not a bug but rather a limitation of the implementation. If you want to include nulls you're unfortunately going to have to implement your own Writes.

I do think we should try to provide more configurability for the macros though.

In this case, I'll let Play exclude this field when the value is null, even if it slightly sacrifices API consistency and self-documentability. It's still such a minor thing (in this particular API) that it doesn't warrant uglifying the code as much as a custom Writes would take for a case class with a dozen values.

I'm hoping they do make this more configurable in future Play versions.

Upvotes: 8

nattyddubbs
nattyddubbs

Reputation: 2095

Here's a way to do it:

object MyWrites extends DefaultWrites{

  override def OptionWrites[T](implicit fmt: Writes[T]): Writes[Option[T]] = new Writes[Option[T]] {
    override def writes(o: Option[T]): JsValue = {
      o match {
        case Some(a) => Json.toJson(a)(fmt)
        case None => JsNull
      }
    }
  }
}

This will overwrite the default implementation which will not create an element. I used this in your sample code:

case class FooJson(
                // ...
                location: Option[LocationJson]
              )

case class LocationJson(latitude: Double, longitude: Double)

object LocationJson {
  implicit val writes = Json.writes[LocationJson]
}

implicit val fooJsonWriter: Writes[FooJson] = new Writes[FooJson] {
  override def writes(o: FooJson): JsValue = {
    JsObject(Seq(
      "location" -> Json.toJson(o.location)
      // Additional fields go here.
    ))
  }
}

Json.toJson(FooJson(None))

And got this result res0: play.api.libs.json.JsValue = {"location":null}.

Upvotes: 1

Related Questions