How to avoid unnesary naming with Kotlin native serialization? - json

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.

Related

Scala Edit Json Object and Write To a New Json Object

So I have a json string in scala which looks something like this
"""
{
"input": {
"House" :{
"Tile" : "Ceramic"
"Kitchen" : {
"Sink" : "Stainless-Steel"
"Counter-Top" : "Granite"
}
}
}
}
"""
I'm using json4s to parse this and to put it into a Map and then I'm making it mutable so I can edit it, but I don't really know how to take it and modify it to make it a new json object.
import org.json4s.jackson.JsonMethods._
import org.json4s._
import org.json4s.native.Serialization._
import org.json4s.native.Serialization
import scala.collection.mutable
...
val reqJsonMap = parse(reqJson).extract[Map[String,Any]]
val reqJsonMutableMap= collection.mutable.Map[String,Any]()
reqJsonMutableMap ++=reqJsonMap
What I want to do is I want to edit it and make the json of Tile=Marble and change the key of Kitchen to Bathroom
I just don't know how to turn that object into this
"""
{
"input": {
"House" :{
"Tile" : "Marble"
"Bathroom" : {
"Sink" : "Stainless-Steel"
"Counter-Top" : "Granite"
}
}
}
}
"""
I think it is far better to work with case classes than with JSON. I think it is the most convenient to use a Json parsing library such as circe to parse your Json string into a case class, and then do whatever thing you need to do with it:
import io.circe._, io.circe.generic.auto._, io.circe.syntax._, io.circe.parser._
val myJsonString = """
{
"input": {
"house" :{
"tile" : "Ceramic",
"kitchen" : {
"sink" : "Stainless-Steel",
"counterTop" : "Granite"
}
}
}
}
"""
case class Data(input: Input)
case class Input(house: HouseInfo)
case class HouseInfo(tile: String, kitchen: KitchenInfo)
case class KitchenInfo(sink: String, counterTop: String)
val maybeDataJsonObject = parse(myJsonString)
val maybeData = maybeDataJsonObject match {
case Right(dataJsonObject) => dataJsonObject.as[Data]
case Left(failure) => println(failure)
}
maybeData match {
case Right(data) => println(data)
case Left(failure) => println(failure)
}
By the way, the format of your json is not good, because you have capitalized the initials of the field names, which should conventionally be lower cased. Also, you have written it as Counter-Top, instead of let’s say counterTop and the - character would be problematic in field names in Scala.
For applying the transformations you are looking for, you need to write new case classes with desired field names, then you can write a transformer function to transform the obtained Data object into the TransformedData one. Then you would be able to use circe to obtain a new Json string by coding the case class in Json, like in this:
val transformedJsonString = myTransformedData.asJson.toString

How to desgin a class for json when I use Gson in Kotlin?

I'm a beginner of Json and Gson, I know I can map json into a class, and map a class to json via Gson.
"My Json" is a json data, I try to design a class "My Class" to map, but I think that "My Class" is not good. Could you show me some sample code? Thanks!
My Class
data class Setting (
val _id: Long,
val Bluetooth_Stauts: Boolean,
val WiFi_Name,String
val WiFi_Statuse: Boolean
)
My Json
{
"Setting": [
{
"id": "34345",
"Bluetooth": { "Status": "ON" },
"WiFi": { "Name": "MyConnect", "Status": "OFF" }
}
,
{
"id": "16454",
"Bluetooth": { "Status": "OFF" }
}
]
}
Updated
The following is made by Rivu Chakraborty's opinion, it can work well, but it's to complex, is there a simple way?
data class BluetoothDef(val Status:Boolean=false)
data class WiFiDef(val Name:String, val Status:Boolean=false)
data class MDetail (
val _id: Long,
val bluetooth: BluetoothDef,
val wiFi:WiFiDef
)
data class MDetailsList(val mListMetail: MutableList<MDetail>)
var mBluetoothDef1=BluetoothDef()
var mWiFiDef1=WiFiDef("MyConnect 1",true)
var aMDetail1= MDetail(5L,mBluetoothDef1,mWiFiDef1)
var mBluetoothDef2=BluetoothDef(true)
var mWiFiDef2=WiFiDef("MyConnect 2")
var aMDetail2= MDetail(6L,mBluetoothDef2,mWiFiDef2)
val mListMetail:MutableList<MDetail> = mutableListOf(aMDetail1,aMDetail2)
var aMDetailsList=MDetailsList(mListMetail)
val json = Gson().toJson(aMDetailsList)
As per your JSON Structure, I think below class definition should work with Gson
data class Setting (
val id: Long,
val Bluetooth: BluetoothDef,
val WiFi:WiFiDef
)
data class BluetoothDef(val Status:String)
data class WiFiDef(val Name:String, val Status:String)
Explanation -
If you're getting an object in your JSON, you should define a class for that to use with Gson.
Data types should match, use String if you're getting Strings like "ON" and "OFF". You can use Boolean if you're getting true and false (without quotes).
The JSON Element name should match the variable/property name unless you're using #SerializedName to define JSON variable name while using different variable/property name.
*Note You can rename the classes if you want
I think it'll be helpful for you

How to create a JSON object in Scala?

First, I searched a lot on Google and StackOverflow for questions like that, but I didn't find any useful answers (to my big surprise).
I saw something about Play Framework, how to create JSON array in Java and how to create JSON objects in Java, but I don't want to use Play Framework and I don't know if the creation of JSON objects differ from Scala to Java.
Following is the JSON I want to create. Later I'll convert the object into a string to send it via a POST request (through an API call).
{
"start_relative": {
"value": "5",
"unit": "years"
},
"metrics": [
{
"name": "DP_391366" # S-Temperature - Celsius
},
{
"name": "DP_812682" # Sensor-A4 Luminosity
}
]
}
How can I do something like that in Scala?
You should use a library that handles serialization/deserialization.
I would consider choosing between Spray Json and Play Json.
I will explain to you how the process works with Play first, and it's very similar to that in Spray.
Let's say you have a class, and an object with an instance and a json as string:
case class MyClass(id: Int,
name: String,
description: String)
object Data {
val obj: MyClass = MyClass(1, "me", "awesome")
val str: String =
"""
|{
| "id": 1,
| "name": "me",
| "description": "awesome"
|}
""".stripMargin
}
For MyClass to be serialized/deserialized, you will need an implicit formatter, specific for this, so you will create an object that contains this formatter using Play.
trait MyClassPlayProtocol {
implicit val formatAbility = Json.format[Ability]
}
object MyClassPlayProtocol extends MyClassPlayProtocol
The serialization/deserialization will look something like this:
object PlayData {
import play.api.libs.json.JsValue
import play.api.libs.json.Json
import MyClassPlayProtocol._
import General._
val str2Json: JsValue = Json.parse(str)
val obj2Json: JsValue = Json.toJson(obj)
val json2Str: String = Json.stringify(str2Json)
val json2Obj: MyClass = obj2Json.as[MyClass]
}
In Spray, the protocol will look like this:
trait MyClassSprayProtocol extends DefaultJsonProtocol {
implicit val myClassFormat = jsonFormat3(MyClass)
}
object MyClassSprayProtocol extends MyClassSprayProtocol
and the serialization/deserialization:
object SprayData {
import spray.json._
import MyClassSprayProtocol._
import General._
val str2Json: JsValue = str.parseJson
val obj2Json: JsValue = obj.toJson
val json2Str: String = str2Json.compactPrint
val json2Obj: MyClass = obj2Json.convertTo[MyClass]
}
As you can see, it's mostly a matter of choice between this two. Both are still improved and probably will be in the near future.
Depending on the benchmark, you will find that one is better than the other by a few miliseconds (usually Spray).
I for one am using Spray at work and Play in some personal projects, and I can't say I found something fundamentally different from one to another.
EDIT:
And to finally answer your question, to go from MyClass to String (serialization), you will do something like this:
PLAY: Json.stringify(Json.toJson(myClass))
SPRAY: myClass.toJson.compactPrint
And the deserialization:
PLAY: Json.parse(string).as[MyClass]
SPRAY: myClass.parseJson.convertTo[MyClass]
You need to use a library if you dont want it to do by yourself there are serveral:
Spray Json - https://github.com/spray/spray-json
Lift Json - https://github.com/lift/lift/tree/master/framework/lift-base/lift-json/
Jerkson - https://github.com/codahale/jerkson
Jackson - You can use Jackson with the scala Module https://github.com/FasterXML/jackson-module-scala
Note: The cool Java Gson LIbrary looses a lot of Magic if you want to use it with Scala, because they dont know Collections. So if you want to use this library you have to convert the "Scala List" to java.util.List and after that use Gson.

updating Json object

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.

Ignoring fields with Jerkson Parsing, expected a valid value error

Say I have JSON as such:
{
"field":{
"nested":{
"foo":"foo val",
"bar":"bar val",
},
"toignore1":{
},
"toignore2":{
}
}
}
I can't seem to parse this correctly, and since it's possible I don't know all the fields to ingore, e.g. toignore3..., I don't want to call them out in the models. I just need a few values from the whole response. If JSON_STRING represents the JSON above, why can't I do this when parsing with Jerkson?
case class JsonModel(val field: FieldModel)
case class FieldModel(val nested: NestedModel) // ignoring other stuff here
case class NestedModel(val foo: String, bar: String)
val parsed = parse[JsonModel](JSON_STRING)
You can do this one of two ways:
case class CaseClassWithIgnoredField(id: Long) {
#JsonIgnore
val uncomfortable = "Bad Touch"
}
#JsonIgnoreProperties(Array("uncomfortable", "unpleasant"))
case class CaseClassWithIgnoredFields(id: Long) {
val uncomfortable = "Bad Touch"
val unpleasant = "The Creeps"
}