Is there a way to perform conditional logic while parsing json using Scala/Play?
For example, I would like to do something like the following:
implicit val playlistItemInfo: Reads[PlaylistItemInfo] = (
(if(( (JsPath \ "type1").readNullable[String]) != null){ (JsPath \ "type1" \ "id").read[String]} else {(JsPath \ "type2" \ "id").read[String]}) and
(JsPath \ "name").readNullable[String]
)(PlaylistItemInfo.apply _)
In my hypothetical JSON parsing example, there are two possible ways to parse the JSON. If the item is of "type1", then there will be a value for "type1" in the JSON. If this is not present in the JSON or its value is null/empty, then I would like to read the JSON node "type2" instead.
The above example does not work, but it gives you the idea of what I am trying to do.
Is this possible?
We can use a well-known Lift-JSON library to parse the JSON string. This library contains many methods which could be used to deserialize the JSON string into Scala objects. In the above code, we have created a case class Employee to match the JSON data.
So the objective here is that given a JSON string with an array of objects, we want to deserialize it into Scala objects so we can use it in our application. We can parse the JSON using the plain Scala methods and features or use different APIs and libraries to parse JSON files like Lift-JSON library and Circe.
Like Reads and Writes, we usually write Format as implicit objects. When our program finds that Format implicit object in scope, our program will encode that Scala type into a JSON or decode a JSON into a Scala type. As Format is a mix of Reads and Writes, we have defined our format implicit object by using both apply and unapply functions.
The proper way to do this with JSON combinators is to use orElse
. Each piece of the combinator must be a Reads[YourType]
, so if/else doesn't quite work because your if
clause doesn't return a Boolean
, it returns Reads[PlaylistItemInfo]
checked against null
which will always be true
. orElse
let's us combine one Reads
that looks for the type1
field, and a second one that looks for the type2
field as a fallback.
This might not follow your exact structure, but here's the idea:
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class PlaylistItemInfo(id: Option[String], tpe: String)
object PlaylistItemInfo {
implicit val reads: Reads[PlaylistItemInfo] = (
(__ \ "id").readNullable[String] and
(__ \ "type1").read[String].orElse((__ \ "type2").read[String])
)(PlaylistItemInfo.apply _)
}
// Read type 1 over type 2
val js = Json.parse("""{"id": "test", "type1": "111", "type2": "2222"}""")
scala> js.validate[PlaylistItemInfo]
res1: play.api.libs.json.JsResult[PlaylistItemInfo] = JsSuccess(PlaylistItemInfo(Some(test),111),)
// Read type 2 when type 1 is unavailable
val js = Json.parse("""{"id": "test", "type2": "22222"}""")
scala> js.validate[PlaylistItemInfo]
res2: play.api.libs.json.JsResult[PlaylistItemInfo] = JsSuccess(PlaylistItemInfo(Some(test),22222),)
// Error from neither
val js = Json.parse("""{"id": "test", "type100": "fake"}""")
scala> js.validate[PlaylistItemInfo]
res3: play.api.libs.json.JsResult[PlaylistItemInfo] = JsError(List((/type2,List(ValidationError(error.path.missing,WrappedArray())))))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With