Outputting 'null' for Option[T] in play-json serialization when value is None
Asked Answered
B

7

20

I'm using play-json's macros to define implicit Writes for serializing JSON. However, it seems like by default play-json omits fields for which Option fields are set to None. Is there a way to change the default so that it outputs null instead? I know this is possible if I define my own Writes definition, but I'm interested in doing it via macros to reduce boilerplate code.

Example

case class Person(name: String, address: Option[String])

implicit val personWrites = Json.writes[Person]    
Json.toJson(Person("John Smith", None))

// Outputs: {"name":"John Smith"}
// Instead want to output: {"name":"John Smith", "address": null}
Barthold answered 5/3, 2014 at 7:43 Comment(0)
F
8

You can use a custom implicit JsonConfiguration, see Customize the macro to output null

implicit val config = JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)

implicit val personWrites = Json.writes[Person]    
Json.toJson(Person("John Smith", None))
Francescafrancesco answered 17/7, 2019 at 7:52 Comment(0)
Z
18

The Json.writes macro generates a writeNullable[T] for optional fields. Like you know (or not), writeNullable[T] omits the field if the value is None, whereas write[Option[T]] generates a null field.

Defining a custom writer is the only option you have to get this behavior.

( 
  (__ \ 'name).write[String] and
  (__ \ 'address).write[Option[String]]
)(unlift(Person.unapply _))
Zink answered 5/3, 2014 at 18:54 Comment(0)
F
8

You can use a custom implicit JsonConfiguration, see Customize the macro to output null

implicit val config = JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)

implicit val personWrites = Json.writes[Person]    
Json.toJson(Person("John Smith", None))
Francescafrancesco answered 17/7, 2019 at 7:52 Comment(0)
U
6

Not a real solution for you situation. But slightly better than having to manually write the writes

I created a helper class that can "ensure" fields.

implicit class WritesOps[A](val self: Writes[A]) extends AnyVal {
    def ensureField(fieldName: String, path: JsPath = __, value: JsValue = JsNull): Writes[A] = {
      val update = path.json.update(
        __.read[JsObject].map( o => if(o.keys.contains(fieldName)) o else o ++ Json.obj(fieldName -> value))
      )
      self.transform(js => js.validate(update) match {
        case JsSuccess(v,_) => v
        case err: JsError => throw new JsResultException(err.errors)
      })
    }

    def ensureFields(fieldNames: String*)(value: JsValue = JsNull, path: JsPath = __): Writes[A] =
      fieldNames.foldLeft(self)((w, fn) => w.ensureField(fn, path, value))

}

so that you can write

Json.writes[Person].ensureFields("address")()
Umbel answered 6/3, 2015 at 16:53 Comment(1)
more safe and concise version of the same thing : implicit class WritesOps[A](val self: OWrites[A]) extends AnyVal { def ensureFields(fieldNames: String*)(value: JsValue = JsNull, path: JsPath = _): OWrites[A] ={ def tf(o: JsObject): JsObject = o ++ JsObject(fieldNames.withFilter(v => !o.keys.contains(v)).map( -> value)(collection.breakOut)) self.transform(tf(_)) } }Imbecility
F
2

Similar answer to above, but another syntax for this:

implicit val personWrites = new Writes[Person] {
  override def writes(p: Person) = Json.obj(
    "name" -> p.name,
    "address" -> p.address,
  )
}
Feast answered 26/5, 2017 at 20:0 Comment(0)
A
0

This is simple:

implicit val personWrites = new Writes[Person] {
  override def writes(p: Person) = Json.obj(
    "name" -> p.name,
    "address" -> noneToString(p.address),
  )
}

def optToString[T](opt: Option[T]) = 
   if (opt.isDefined) opt.get.toString else "null"
Ander answered 18/7, 2017 at 14:57 Comment(0)
P
0

You can define something like this :

  implicit class JsPathExtended(path: JsPath) {
    def writeJsonOption[T](implicit w: Writes[T]): OWrites[Option[T]] = OWrites[Option[T]] { option =>
      option.map(value =>
        JsPath.createObj(path -> w.writes(value))
      ).getOrElse(JsPath.createObj(path -> JsNull))
    }
  }

And if you are using play framework :

  implicit val totoWrites: Writes[Toto] = (
      (JsPath \ "titre").write[String] and
        (JsPath \ "option").writeJsonOption[String] and
        (JsPath \ "descriptionPoste").writeNullable[String]
      ) (unlift(Toto.unapply))

   implicit val totoReads: Reads[Toto] = (
      (JsPath \ "titre").read[String] and
        (JsPath \ "option").readNullable[String] and
        (JsPath \ "descriptionPoste").readNullable[String]
      ) (Toto.apply _)
Palladin answered 22/3, 2018 at 10:31 Comment(0)
I
0

You may wrap your option and redefine serialization behavior:

case class MyOption[T](o: Option[T])
implicit def myOptWrites[T: Writes] = Writes[MyOption[T]](_.o.map(Json.toJson).getOrElse(JsNull))

CAVEATS:

1)this approach is good only for case classes used solely as a serialization protocol definition. If you are reusing some data model classes in controllers - define custom serializers for them not to pollute the model.

2)(related only to Option) if you use the same class for writes and reads. Play will require the wrapped fields to be present (possibly null) during deserialization.

P.S.: Failed to do the same with type tagging. Compiler error is like No instance of play.api.libs.json.Writes is available for tag.<refinement> (given the required writes were explicitly defined). Looks like Play's macro fault.

Imbecility answered 12/4, 2018 at 13:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.