Dia Kharrat
Dia Kharrat

Reputation: 6006

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

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}

Upvotes: 20

Views: 7834

Answers (7)

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))

Upvotes: 8

ievgen.garkusha
ievgen.garkusha

Reputation: 129

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.

Upvotes: 0

user9534069
user9534069

Reputation:

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 _)

Upvotes: 0

Bday
Bday

Reputation: 1199

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"

Upvotes: 0

jmills
jmills

Reputation: 21

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,
  )
}

Upvotes: 2

KailuoWang
KailuoWang

Reputation: 1314

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")()

Upvotes: 6

Julien Lafont
Julien Lafont

Reputation: 7877

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 _))

Upvotes: 18

Related Questions