I have a requirement to parse a JSON object, using play-json and distinguish between a missing value, a string value and a null value.
So for example I might want to deserialize into the following case class:
case class MyCaseClass(
a: Option[Option[String]]
)
Where the values of 'a' mean:
- None - "a" was missing - normal play-json behavipr
- Some(Some(String)) - "a" had a string value
- Some(None) - "a" had a null value
So examples of the expected behavior are:
{}
should deserialize to myCaseClass(None)
{
"a": null
}
should deserialize as myCaseClass(Some(None))
{
"a": "a"
}
should deserialize as myCaseClass(Some(Some("a"))
I've tried writing custom formatters, but the formatNullable and formatNullableWithDefault methods don't distinguish between a missing and null value, so the code I've written below cannot generate the Some(None) result
object myCaseClass {
implicit val aFormat: Format[Option[String]] = new Format[Option[String]] {
override def reads(json: JsValue): JsResult[Option[String]] = {
json match {
case JsNull => JsSuccess(None) // this is never reached
case JsString(value) => JsSuccess(Some(value))
case _ => throw new RuntimeException("unexpected type")
}
}
override def writes(codename: Option[String]): JsValue = {
codename match {
case None => JsNull
case Some(value) => JsString(value)
}
}
}
implicit val format = (
(__ \ "a").formatNullableWithDefault[Option[String]](None)
)(MyCaseClass.apply, unlift(MyCaseClass.unapply))
}
Am I missing a trick here? How should I go about this? I am very much willing to encode the final value in some other way than an Option[Option[Sting]] for example some sort of case class that encapsulates this:
case class MyContainer(newValue: Option[String], wasProvided: Boolean)