Play Json: reading multiple optional nested objects - json

We have a case class and Json combinators similar to the below:
case class Thing(param1: Option[BigDecimal],
param2: Option[BigDecimal])
object Thing {
implicit val writes: Writes[Thing] = Json.writes[Thing]
implicit val reads: Reads[Thing] = (
(__ \ "parent" \ "param1").readNullable[BigDecimal] and
(__ \ "param2").readNullable[BigDecimal]
)(Thing.apply _)
The Json reads combinator for Thing will handle incoming Json matching the below:
{
"parent": {
"param1: 1111.11
},
"param2": 2222.22
}
The standard ReadNullable works as we need it to when param1 is not present, resulting in the creation of Thing(None, 2222.22). However the parent object is also optional, and reading the path explicitly like the above results in a path missing exception if it does not exist. We need this to result in a Thing(None, 2222.22), the same as if the param1 field wasn't present.
We could use a recursive read such as (__ \\ "param1") to bipass this exception, but ideally we would like to preserve the explicit path as there are other Json objects that have similar, if not identical fields at the same level.
Is this possible by using Json combinators in this way?

You can do in following way by creating two case classes.
case class Thing(param1: Option[BigDecimal])
case class ParentJson(parent: Option[Thing], param2: Option[BigDecimal])

You can replace .readNullable[BigDecimal] with read(Reads.optionNoError[BigDecimal]) to map errors to None.

Related

play framework json reads from empty string to empty list

Hi everyone recently I faced an issue in converting json into my own data model.
I have a json format message which may contain an empty string:
{
"name" : "John Doe",
"hobbies": ""
}
or a list of hobby types:
{
"name" : "John Doe",
"hobbies": [{"name":"basketball"}]
}
And the following is my case class data model in scala play framework:
case class Person(name: String, hobbies: List[Hobby])
case class Hobby(name: String)
Right now I'm using the default json formatter but of course it's not working well when we have empty string as value.
implicit val HobbyJson= Json.format[Hobby]
implicit val PersonJson = Json.format[Person]
it will throw exception if the hobbies has a empty string. I want to convert it into an empty list when it's the empty string. I search the document Play provides but couldn't find infomation. Can anyone give some suggestions?
Thanks in advance.
As you mentioned, the default Format macros won't work for you here because of the inconsistent treatment of hobbies. So you need to implement your own Reads[Person] - here's how I'd do it:
object PersonJson {
implicit val hobbyConverter = Json.format[Hobby]
val personReads = new Reads[Person] {
override def reads(json: JsValue): JsResult[Person] = {
for {
personName <- (json \ "name").validate[String]
hobbies <- (json \ "hobbies").validate[JsValue]
} yield {
val maybeHobbyList = hobbies.validate[List[Hobby]].asOpt
Person(personName, maybeHobbyList.getOrElse(Nil))
}
}
}
implicit val personConverter = Format(personReads, Json.writes[Person])
}
The key thing to note here is surrounding the whole thing in a JsResult courtesy of the for-comprehension and the yield. This gives us all the necessary checking (like the name field being there and being a String, and the hobbies field being there).
The code within the yield block only runs if we've got something that looks pretty close to a Person. Then we can safely try validating the hobbies as a List[Hobby], and convert the result to an Option[List[Hobby]]. It'll be a None if it didn't work (thus it must have been a string) and so we default it to the empty list as required.
Thanks #millhouse answer, it definitely works. Like he said we need a custom Reads[Person] to properly convert it.
I also post my code as reference.
implicit val personJsonReads: Reads[Person] = (
(__ \ "name").read[String] and
(__ \ "hobbies").read[List[Hobby]].orElse(Reads.pure(List()))
) (Person.apply _)
read[List[Hobby]].orElse(Reads.pure(List())) will generate the empty list when the value cannot convert to List[Hobby].

Play Scala JSON - conditionally add field to JSON object in Writes

In our app, we have pretty complex structure of objects that is getting converted to JSON and back. Until now most of the formatted are symmetrical (except some very specific cases, and even these for security reasons).
Now we are facing a more complex case where conversion of an object into JSON (writes) needs to create an additional field at a time of conversion while the case class does not have that field.
For example, here is one of our existing formatters:
case class ChecklistColumn(kind: ColumnKind.Value, descriptor: Descriptor.Value, data: JsValue) extends Column
implicit val checklistResultChecklistDataFormat: Format[ChecklistColumn] = (
(__ \ "kind").format[ColumnKind.Value] and
(__ \ "descriptor").format[Descriptor.Value] and
(__ \ "data").format[JsValue]
)(ChecklistColumn.apply, unlift(ChecklistColumn.unapply))
This one creates a json that will looks like:
{
"kind": <String>,
"descriptor": <String>,
"data": <JsValue>
}
What we need to achieve is:
{
"kind": <String>,
"descriptor": <String>,
"data": <JsValue>,
"normalized_data": <JsString>
}
But, only in case when the data is type of JsString (in any other case normalized_data can be left empty, byt ideally should not even exist).
I do understand that we have to create separate Reads & Writes for that.
But, I am not sure, how to implement the logic that will react differently to a different type of data.
Of course, there is always an option to create fully custom writes:
override def writes(column: ChecklistColumn): JsValue = {...}
But, this will create a huge complexity in a code that will be hard to maintain.
What is the cleanest way to implement something like that?
Have a look at ScalaJsonTransformers. You can create a transformer that creates the normalised field from a string data value and use it to, erm, transform your original Format to a new Writes. Here's a slightly simplified example that could doubtless be improved (you'll want to check various edge cases):
case class ChecklistColumn(kind: String, descriptor: String, data: JsValue)
// The original format.
val checklistFormat: Format[ChecklistColumn] = (
(__ \ "kind").format[String] and
(__ \ "descriptor").format[String] and
(__ \ "data").format[JsValue]
)(ChecklistColumn.apply, unlift(ChecklistColumn.unapply))
// A transformer that looks for a "data" field with a string
// value and adds the normalized_data field if it finds one.
val checklistTransformer: Reads[JsObject] = JsPath.json.update(
(__ \ "data").read[String].flatMap (
str => (__ \ "normalized_data").json.put(JsString(str + "!!!"))))
// A new derived Writes which writes the transformed value if
// the transformer succeeds (a data string), otherwise the
// original value.
implicit val checklistWrites: Writes[ChecklistColumn] = checklistFormat
.transform (js => js.transform(checklistTransformer).getOrElse(js))
That gives me:
Json.prettyPrint(Json.toJson(ChecklistColumn("a", "b", JsNumber(1))))
// {
// "kind" : "a",
// "descriptor" : "b",
// "data" : 1
// }
Json.prettyPrint(Json.toJson(ChecklistColumn("a", "b", JsString("c"))))
// {
// "kind" : "a",
// "descriptor" : "b",
// "data" : "c",
// "normalized_data" : "c!!!"
// }

JSON Reads/Writes/Format Combinators Play - Use different key name while creating json

I have following json:
{
"key_as_string":"StringOne",
"doc_count":1
}
How should I create a Play Scala Read combinators by using JsonParse as case class object.
case class JsonParse(key:String,doc_count:Long)
implicit val placeReads: Reads[JsonParse] = (
(JsPath \ "key_as_string").read[String] and
(JsPath \ "doc_count").read[Long]
)(JsonParse.apply _)
I want to map "key_as_string" which is present in json to "key" which is present in the case class
JsonParse(key = "StringOne",doc_count = 1)
What you have works fine. As long as the json key specified in the JsPath matches the actual json schema -- it does in your snippet -- you're free to name the case class param whatever you'd like.

Play ScalaJSON Reads[T] parsing

I am writing an Json parse for an rest webservice response, I have an Json file looking as:
{"program": {
"name": "myname",
"#id": "12345",
"$": "text text text"
}, etc. etc.
I wrote an case class for the Reads object:
case class program(name:String)
implicit val programFormat = Json.format[program]
And this pseudo code for get a data:
val x=(jobj \ "program").validate[program]
x match {
case JsSuccess(pr, _) => println("JsSuccess:"+pr)
for(p<- pr.program)
{
println(p.name)
}
case error: JsError => ....
}
For the field name no problem, the code work well, but I don't understand how capture the field "#id" and the field "$" because I cannot create an param in case class named: #id or $.
Thank you for your help.
More correct solution in my opinion is creating own Reads, that is:
case class Program(name: String, id: String, dollar: String)
implicit val programWrites: Reads[Program] = (
(__ \ "name").read[String] ~
(__ \ "#id").read[String] ~
(__ \ "$").read[String]
)(Program.apply _)
Docs: https://www.playframework.com/documentation/2.4.x/ScalaJsonCombinators#Reads
Another solution, i think much worse one, is using backtick sign
case class Program(name: String, `#id`: String, `$`: String)
implicit val programFormat = Json.format[Program]
It allows to write special signs in method names, field names and so on.
More about it: Need clarification on Scala literal identifiers (backticks)

Play framework scala json validation exception

I'm trying to check JsValue object in my Actor using play framework 2.2.2. When I try to use validate method, I receive exception not a result object:
try {
val result = data.validate[EventConfig]
Logger.debug("Result: "+result")
} catch {
case e =>
Logger.error("Exception: "+e)
}
Here is this exception:
Exception: play.api.libs.json.JsResultException: JsResultException(errors:List((,List(ValidationError(error.expected.jsnumber,WrappedArray())))))
Why is this happening, and how should I use validate method?
====== Update
I was using such Reads implementation:
implicit val EventConfig_reads = new Reads[EventConfig] {
def reads(json: JsValue): JsResult[EventConfig] = {
JsSuccess(new
EventConfig((json \ ConfigEventAttrs.PARAM).as[Int],
(json \ ConfigEventAttrs.PERIOD).as[Int],
(json \ ConfigEventAttrs.THRESHOLD).as[Int],
(json \ ConfigEventAttrs.TOGGLE).as[Boolean]))
}
}
The solution is to add catch clause:
implicit val EventConfig_reads = new Reads[EventConfig] {
def reads(json: JsValue): JsResult[EventConfig] = {
try {
JsSuccess(new
EventConfig((json \ ConfigEventAttrs.PARAM).as[Int],
(json \ ConfigEventAttrs.PERIOD).as[Int],
(json \ ConfigEventAttrs.THRESHOLD).as[Int],
(json \ ConfigEventAttrs.TOGGLE).as[Boolean]))
} catch {
case e: JsResultException =>
JsError(e.errors)
}
}
}
That is not the proper way to use validate. I don't think the documentation highlights it's importance as much as it should, but it's explained here, in the section called Using Validation.
data.validate[EventConfig] returns JsResult and not EventConfig. The preferred way to deal with errors is to fold the result:
data.validate[EventConfig].fold(
error => {
// There were validation errors, handle them here.
},
config => {
// `EventConfig` has validated, and is now in the scope as `config`, proceed as usual.
}
)
Let's examine this a bit. The signature if fold on a JsResult is as follows:
fold[X](invalid: (Seq[(JsPath, Seq[ValidationError])]) ⇒ X, valid: (A) ⇒ X): X
It accepts two functions as arguments that both return the same type of result. The first function is a Seq[(JsPath, Seq[ValidationError])]) => X. In my code above, error has the type Seq[(JsPath, Seq[ValidationError])]), which is essentially just a sequence of json paths tupled with their validation errors. Here you can dissect these errors and return the appropriate error messages accordingly, or do whatever else you may need to on failure.
The second function maps A => X, where A is the type JsResult has been validated as, in your case EventConfig. Here, you'll be able to handle your EventConfig type directly.
Causing and catching exceptions is not the way to handle this (and rarely is), as you will lose all of the accumulated validation errors.
Edit: Since the OP has updated his question with additional information regarding his defined Reads.
The problem with the Reads defined there is that they're using as[T]. When calling as, you're trying to force the given json path to type T, which will throw an exception if it cannot. So as soon as you reach the first validation error, an exception is thrown and you will lose all subsequent errors. Your use case is relatively simple though, so I think it would be better to adopt a more modern looking Reads.
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class EventConfig(param: Int, period: Int, threshold: Int, toggle: Boolean)
object EventConfig {
implicit val jsonReads: Reads[EventConfig] = (
(__ \ ConfigEventAttrs.PARAM).read[Int] and
(__ \ ConfigEventAttrs.PERIOD).read[Int] and
(__ \ ConfigEventAttrs.THRESHOLD).read[Int] and
(__ \ ConfigEventAttrs.TOGGLE).read[Boolean]
)(EventConfig.apply _)
}
This is much more compact, and the use of the functional syntax will accumulate all of the validation errors into the JsResult, as opposed to throwing exceptions.
Edit 2: To address the OP's need for a different apply method.
If the parameters you're using the build an object from JSON differ from those of your case class, define a function to use for the JSON Reads instead of EventConfig.apply. Supposing your EventConfig is really like this in JSON:
(time: Long, param: Int)
But instead you want it to be like this:
case class EventConfig(time: Date, param: Int)
Define a function to create an EventConfig from the original parameters:
def buildConfig(time: Long, param: Int) = EventConfig(DateUtils.timeSecToDate(time), param)
Then use buildConfig instead of EventConfig.apply in your Reads:
implicit val jsonReads: Reads[EventConfig] = (
(__ \ "time").read[Long] and
(__ \ "param").read[Int]
)(buildConfig _)
I shortened this example, but buildConfig can be any function that returns EventConfig and parameters match those of the JSON object you're trying to validate.
Validating depends on your Reads method, and I've had an issue there. I should just catch this exception in my reads.