I have a json object that I need to update. The original object is a list that looks like this:
[
{
"firstName":"Jane",
"lastName":"Smith"
},
{
"firstName":"Jack",
"lastName":"Brown"
}
]
For each element in the list, we have an extra field, "age", that needs to be added at run-time, so the result should look like the following:
[
{
"firstName":"Jane",
"lastName":"Smith",
"age": "21"
},
{
"firstName":"Jack",
"lastName":"Brown",
"age": "34"
}
]
Any suggestions how to do this so the result is still json?
Thanks.
request.body.asJson.map {
jm => (jm.as[JsObject] ++ Json.obj("age" -> 123))
}
I would recommended deserializing the JSON array you receive into a List of case classes, then having some function fill in the missing attributes based on the current attributes of the case class, and finally serializing them as JSON and serving the response.
Let's make a Person case class with the fields that will be missing as Option:
import play.api.libs.json.Json
case class Person(firstName: String, lastName: String, age: Option[Int])
object Person {
implicit val format: Format[Person] = Json.format[Person]
def addAge(person: Person): Person = {
val age = ... // however you determine the age
person.copy(age = Some(age))
}
}
Within the companion object for Person I've also defined a JSON serializer/deserializer using the format macro, and a stub for a function that will find a person's age then copy it back into the person and return it.
Deep within the web service call you might then have something like this:
val jsArray = ... // The JsValue from somewhere
jsArray.validate[List[Person]].fold(
// Handle the case for invalid incoming JSON
error => InternalServerError("Received invalid JSON response from remote service."),
// Handle a deserialized array of List[Person]
people => {
Ok(
// Serialize as JSON, requires the implicit `format` defined earlier.
Json.toJson(
// Map each Person to themselves, adding the age
people.map(person => Person.addAge(person))
)
)
}
)
This method is much safer, otherwise you'll have to extract values from the array one by one and concatenate objects, which is very awkward. This will also allow you to easily handle errors when the JSON you receive is missing fields you're expecting.
Related
Situation
Say I have this JSON, which I get from some server:
{
"useless_info": "useless info",
"data": {
"useless_info2": "useless info 2",
"children": [
{
"kind": "Car",
"data": {
"id": 1,
"transmission": "manual"
}
},
{
"kind": "Boat",
"data": {
"id": 2,
"isDocked": true
}
}
]
}
}
This JSON is an example. It represent a much larger and complex one, but with similar structure. I have no say in it's structure, so I must adapt to it.
What I Want
The JSON has deep inside it an array of vehicle objects. Say I only care about the ID.
I could model it like this:
#Serializable
data class VehicleResponse(
#Serializable(with = VehicleResponseSerializer::class)
#SerialName("data")
val vehicles: List<Vehicle>
)
#Serializable
data class Vehicle(val id: Int)
In order to avoid modeling the response exactly by writing many neste data classes (which would be awful with the real one, which is more nested and complex), I can use a JsonTransformingSerializer to cut right to part I want, like this:
object VehicleResponseSerializer : JsonTransformingSerializer<List<Vehicle>>(ListSerializer(Vehicle.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
val vehicles = mutableListOf<JsonElement>()
val vehicleArray = element.jsonObject["children"]!!.jsonArray
// equals: [{"kind":"Car","data":{"id":1,"totalWheels":"4"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]
vehicleArray.forEach { vehicle ->
val vehicleData = vehicle.jsonObject["data"]!!
// equals: {"id":1,"totalWheels":"4"}}
vehicles.add(vehicleData)
}
return JsonArray(vehicles.toList())
}
}
Calling it from main:
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
fun main() {
val vehicles: VehicleResponse = configuredJson.decodeFromString(vehicleJson)
println(vehicles)
}
val configuredJson = Json {
ignoreUnknownKeys = true
}
Prints:
VehicleResponse(vehicles=[Vehicle(id=1), Vehicle(id=2)])
So if it works perfectly, what's even the problem
It's this part:
#Serializable
data class VehicleResponse(
#Serializable(with = VehicleResponseSerializer::class)
#SerialName("data")
val vehicles: List<Vehicle>
)
I have a problem with the #SerialName. It feels like a hack to me. All this accomplishes is give the custom serializer the data object rather than the full original object from the response. If you remember from the custom serializer, I cut right to the array I want by walking down the structure. It would be trivial to get to the data object. So, instead of
val vehicleArray = element.jsonObject["children"]!!.jsonArray
I would have to do:
val vehicleArray = element.jsonObject["data"]!!.jsonObject["children"]!!.jsonArray
Currently I feel like what I'm doing doesn't really describe the code. In my VehicleResponse data class, I write that my vehicles list does has a serial name of "data", when it doesn't at all. This could lead me to misinterpret the structure of JSON in the future, or even confuse me as to what I'm trying to do. With this simple example it's OK, but what about more complex ones?
What I Tried
Changing VehicleResponse to this:
#Serializable(with = VehicleResponseSerializer::class)
data class VehicleResponse(
val vehicles: List<Vehicle>
)
And altering the line that gets the array inside the custom serializer to this:
override fun transformDeserialize(element: JsonElement): JsonElement {
...
val vehicleArray = element.jsonObject["data"]!!.jsonObject["children"]!!.jsonArray
...
}
Running this code gives the following error:
Exception in thread "main" java.lang.ClassCastException: class java.util.ArrayList cannot be cast to class VehicleResponse (java.util.ArrayList is in module java.base of loader 'bootstrap'; VehicleResponse is in unnamed module of loader 'app')
at MainKt.main(Main.kt:5)
at MainKt.main(Main.kt)
Process finished with exit code 1
For reference, I tried to print the vehicles array inside the serializer, to see it by any chance I messed up navigating the JSON.
override fun transformDeserialize(element: JsonElement): JsonElement {
...
println(vehicles)
return JsonArray(vehicles.toList())
}
It prints:
[{"id":1,"transmission":"manual"}, {"id":2,"isDocked":true}]
So everything seems to be fine. Not sure what I'm doing wrong.
I am trying to familiarize myself with the PlayJSON library. I have a JSON formatted file like this:
{
"Person": [
{
"name": "Jonathon",
"age": 24,
"job": "Accountant"
}
]
}
However, I'm having difficulty with parsing it properly due to the file having different types (name is a String but age is an Int). I could technically make it so the age is a String and call .toInt on it later but for my purposes, it is by default an integer.
I know how to parse some of it:
import play.api.libs.json.{JsValue, Json}
val parsed: JsValue = Json.parse(jsonFile) //assuming jsonFile is that entire JSON String as shown above
val person: List[Map[String, String]] = (parsed \ "Person").as[List[Map[String, String]]]
Creating that person value throws an error. I know Scala is a strongly-typed language but I'm sure there is something I am missing here. I feel like this is an obvious fix too but I'm not quite sure.
The error produced is:
JsResultException(errors:List(((0)/age,List(JsonValidationError(List(error.expected.jsstring),WrappedArray())))
The error you are having, as explained in the error you are getting, is in casting to the map of string to string. The data you provided does not align with it, because the age is a string. If you want to keep in with this approach, you need to parse it into a type that will handle both strings and ints. For example:
(parsed \ "Person").validate[List[Map[String, Any]]]
Having said that, as #Luis wrote in a comment, you can just use case class to parse it. Lets declare 2 case classes:
case class JsonParsingExample(Person: Seq[Person])
case class Person(name: String, age: Int, job: String)
Now we will create a formatter for each of them on their corresponding companion object:
object Person {
implicit val format: OFormat[Person] = Json.format[Person]
}
object JsonParsingExample {
implicit val format: OFormat[JsonParsingExample] = Json.format[JsonParsingExample]
}
Now we can just do:
Json.parse(jsonFile).validate[JsonParsingExample]
Code run at Scastie.
I am using Scala with play JSON library for parsing JSON. We are the facing the problem using JSON parsing is, we have same JSON structure, but some JSON files contain different with values structure with the same key name. Let's take an example:
json-1
{
"id": "123456",
"name": "james",
"company": {
"name": "knoldus"
}
}
json-2
{
"id": "123456",
"name": "james",
"company": [
"knoldus"
]
}
my case classes
case class Company(name: String)
object Company{
implicit val _ = Json.format[Company]
}
case class User(id: String, name: String, company: Company)
object User{
implicit val _ = Json.format[Company]
}
while JSON contains company with JSON document, we are getting successfully parsing, but if company contains an array, we are getting parsing exception. Our requirements, are is there anyway, we can use play JSON library and ignore the fields if getting parsing error rather that, ignore whole JSON file. If I am getting, company array values, ignore company field and parse rest of them and map corresponding case class.
I would do a pre-parse function that will rename the 'bad' company.
See the tutorial for inspiration: Traversing-a-JsValue-structure
So your parsing will work, with this little change:
case class User(id: String, name: String, company: Option[Company])
The company needs to be an Option.
Final we found the answer to resolving this issue, as we know, we have different company structure within JSON, so what we need to do, we need to declare company as a JsValue because in any case, whatever the company structure is, it is easily assigned to JsValue type. After that, our requirements are, we need to use object structure, and if JSON contains array structure, ignore it. After that, we used pattern matching with our company JsValue type and one basis of success and failure, we parse or JSON. The solution with code is given below:
case class Company(name: String)
object Company{
implicit val _ = Json.format[Company]
}
case class User(id: String, name: String, company: JsValue)
object User{
implicit val _ = Json.format[Company]
}
Json.parse("{ --- whatevery json--string --- }").validate[User].asOpt match {
case Some(obj: JsObject) => obj.as[Company]
case _ => Company("no-name")
}
I've seen many examples with pretty much everything one can need to understand how to validate json representation of a given simple model.
Official play documentation is very specific on the subject, covering from simple validations to transformers and combinators.
I'm just wondering how can I check json againts a given data type structure, like:
{
title : "title1",
tags : ["funny", "others"],
props : [
{ prop1: "val1" },
{ prop2: "val2" }
]
}
I'd like to check the previous json example, to validate if it has this structure:
title: String
tags: Array[String]
props: Array[(String->String] // ???
This is of course a simplification, since a case class would be as simple as:
case class Example1(title: String, tags: Array[String], props: Array[???])
As you can see, my questions has two parts:
- first, I want to properly use validation/transformation/reads or whatever API Play 2.3.x is ment to perform that kind of json validation without a model
- second, how to specify an array of simple key/value objects
Validation with Reads does not require a model class.
val json = { ... }
val titleMustBeString: Reads[String] = (JsPath \ "title").read[String]
json.validate[String](titleMustBeString) match {
case title: JsSuccess[String] => println(s"Title: $title")
case err: JsError => println("Invalid json")
}
i'm writing a Play 2.3.2 application in Scala.
I'm writing a Statistic controller that queries my mongodb database for obtain some informations.
Now i'm trying to implement a method that returns all the tags associate to a user.
I get an http request in json format as the following:
{
"user": "example#example.com"
}
I want to parse the Json request and get the String associate to the "user" field of the Json, if the Json is correctly i want to do some work with the String object, otherwise i want to return a BadRequest response.
My method implementation is something like this:
def userEmail = Action.async { request =>
val userEmail: String = request.body.asJson.map { json =>
json.validate[String].map {
//if the json congaing the "user tag" return the instance, get the instance, otherwise return a BadRequestInstance
}
}
def elaborate(user: String)= {
//some work
}
elaborate(userEmail)
}
How can i make that??
As johanandren mentioned reading playframework documentation should solve your problem.
Hint: I would define case class and implicit reads to convert json data into case class type.
case class User(email: String)
implicit val userReads = (
(JsPath \ "email").read[String]
)(User.apply _)