PlayFramework: How to generate JSON without optional fields set to null
json, playframework, scala
Solution
With `Writes` produced by `Json.writes` you'll get no `null`s:
case class Account(id: Option[String],
name: String,
ownerId: String,
openingTime: Int,
closingTime: Option[Int])
// in general you should add this to companion object Account
implicit val accountWrites = Json.writes[Account]
val acc = Account(None, "Default", "52dfc13ec20900c2093155cf", 666, None)
Json prettyPrint Json.toJson(acc)
// String =
// {
// "name" : "Default",
// "ownerId" : "52dfc13ec20900c2093155cf",
// "openingTime" : 666
// }
You could implement `Writes[(Option[String], String, String, Int, Option[Int])]` by yourself if you don't want to use a custom class `Account` like this:
import play.api.libs.json._
import play.api.libs.functional.syntax._
val customWrites = (
(JsPath \ "id").writeNullable[String] ~
(JsPath \ "name").write[String] ~
(JsPath \ "ownerId").write[String] ~
(JsPath \ "openingTime").write[Int] ~
(JsPath \ "closingTime").writeNullable[Int]
).tupled
customWrites writes (None, "Default", "52dfc13ec20900c2093155cf", 666, None)
// JsObject = {"name":"Default","ownerId":"52dfc13ec20900c2093155cf","openingTime":666}
Problem
Here below is a class for creating an account: ``` class Account private(private var json: JsValue) { private def setValue(key: JsPath, value: JsValue) = { value match { case JsNull => json.transform(key.json.prune).map(t => json = t) case _ => json.transform((__.json.update(key.json.put(value)))).map(t => json = t) } } def asJson = json def id_= (v: Option[String]) = setValue((__ \ 'id), Json.toJson(v)) def id = json as (__ \ 'id).readNullable[String] def name = json as (__ \ 'name).read[String] def name_= (v: String) = setValue((__ \ 'name), Json.toJson(v)) def ownerId = json as (__ \ 'ownerId).read[String] def ownerId_= (v: String) = setValue((__ \ 'ownerId), Json.toJson(v)) def openingTime = json as (__ \ 'openingTime).read[LocalDateTime] def openingTime_= (v: LocalDateTime) = setValue((__ \ 'openingTime), Json.toJson(v)) def closingTime = json as (__ \ 'closingTime).readNullable[LocalDateTime] def closingTime_= (v: Option[LocalDateTime]) = setValue((__ \ 'closingTime), Json.toJson(v)) def copy(json: JsValue) = Account(this.json.as[JsObject] ++ json.as[JsObject]).get } object Account { val emptyObj = __.json.put(Json.obj()) def apply(json: JsValue): JsResult[Account] = { validateAccount.reads(json).fold( valid = { validated => JsSuccess(new Account(validated)) }, invalid = { errors => JsError(errors) } ) } def apply( id: Option[String], name: String, ownerId: String, openingTime: LocalDateTime, closingTime: Option[LocalDateTime] ): JsResult[Account] = apply(Json.obj( "id" -> id, "name" -> name, "ownerId" -> ownerId, "openingTime" -> openingTime, "closingTime" -> closingTime )) def unapply(account: Account) = { if (account eq null) None else Some(( account.id, account.name, account.ownerId, account.openingTime, account.closingTime )) } implicit val accountFormat = new Format[Account] { def reads(json: JsValue) = Account(json) def writes(account: Account) = account.json } /** * Validates the JSON representation of an [[Account]]. */ private[auth] val validateAccount = ( ((__ \ 'id).json.pickBranch or emptyObj) ~ ((__ \ 'name).json.pickBranch) ~ ((__ \ 'ownerId).json.pickBranch) ~ ((__ \ 'openingTime).json.pickBranch) ~ ((__ \ 'closingTime).json.pickBranch or emptyObj) ).reduce } ``` As you can see, there are some fields that are optional like `id` and `closingTime`. If the optional fields are `None`, the `apply` method above produces the following JSON: ``` { "id" : null, "name" : "Default", "ownerId" : "52dfc13ec20900c2093155cf", "openingTime" : "2014-02-02T19:22:54.708", "closingTime" : null } ``` Even if this might be correct, it is not what I'm looking for. For instance, if the optional fields are `None`, I need to get the following JSON: ``` { "name" : "Default", "ownerId" : "52dfc13ec20900c2093155cf", "openingTime" : "2014-02-02T19:22:54.708", } ``` Having said that, how do I prevent `apply` from generating the `null` fields? Shall I replace the `Json.obj(...)` stuff with something like this? ``` JsObject( Seq() ++ (if (id.isDefined) Seq("id" -> JsString(id.get)) else Seq() ) ++ Seq( "name" -> JsString(name), "ownerId" -> JsString(ownerId), "openingTime" -> Json.toJson(openingTime) ) ++ (if (closingTime.isDefined) Seq("closingTime" -> Json.toJson(closingTime)) else Seq() )) ``` Is there a better way?