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"
}
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'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
I have a json model, where contents of certain attribute depend on the other attribute. Something like this:
"paymentMethod": "CREDIT_CARD",
"metaData": {
"cardType": "VISA",
"panPrefix": "",
"panSuffix": "",
"cardHolder": "",
"expiryDate": ""
}
So when paymentMethod equals to CREDIT_CARD, the metadata object will contain attributes as described. In case of other payment method, there'll be different metadata.
I want to handle this situation in a future-proof way. What I'm trying to do is to not parse the metadata field right away, but keep it somehow "unparsed" until I've parsed the paymentMethod field. Then I'd take the metadata and applied appropriate parsing approach.
However I don't know which type to use for a Scala class field for such "late parsed" attributes. I've tried String, JsonInput, JObject, and they all are not suitable (either don't compile or can't be parsed). Any ideas which type can I use? Or, in other words:
case class CreditCardMetadata(
cardType: String,
panPrefix: String,
panSuffix: String,
cardHolder: String,
expiryDate: String)
case class PaypalMetadata(...) // etc.
case class PaymentGatewayResponse(
paymentMethod: String,
metadata: ???)
You could create a CustomSerializer to parse the metadata directly. Something like :
case class PaymentResponse(payment: Payment, otherField: String)
sealed trait Payment
case class CreditCardPayment(cardType: String, expiryDate: String) extends Payment
case class PayPalPayment(email: String) extends Payment
object PaymentResponseSerializer extends CustomSerializer[PaymentResponse]( format => (
{
case JObject(List(
JField("paymentMethod", JString(method)),
JField("metaData", metadata),
JField("otherField", JString(otherField))
)) =>
implicit val formats = DefaultFormats
val payment = method match {
case "CREDIT_CARD" => metadata.extract[CreditCardPayment]
case "PAYPAL" => metadata.extract[PayPalPayment]
}
PaymentResponse(payment, otherField)
},
{ case _ => throw new UnsupportedOperationException } // no serialization to json
))
Which can be used as:
implicit val formats = DefaultFormats + PaymentResponseSerializer
val json = parse("""
{
"paymentMethod": "CREDIT_CARD",
"metaData": {
"cardType": "VISA",
"expiryDate": "2015"
},
"otherField": "hello"
}
""")
val json2 = parse("""
{
"paymentMethod": "PAYPAL",
"metaData": {
"email": "foo#bar.com"
},
"otherField": "world"
}
""")
val cc = json.extract[PaymentResponse]
// PaymentResponse(CreditCardPayment(VISA,2015),hello)
val pp = json2.extract[PaymentResponse]
// PaymentResponse(PayPalPayment(foo#bar.com),world)
You can use a Map[String, String].
It will contain anything you may need.
The answer by Peter Neyens has inspired me to implement my own solution. It's not as generic as his, but in my case I needed something really simple and ad-hoc. Here's what I've done:
It's possible to define a case class with the field of unknown type is represented by a JObject type. Something like this:
case class PaymentGatewayResponse(
default: Boolean,
paymentMethod: String,
visibleForCustomer: Boolean,
active: Boolean,
metaData: JObject)
When such json is parsed into such case class, this field is not parsed immediately, but contains all the necessary information. Then it's possible parse it in a separate step:
case class CreditCardMetadata(
cardType: String,
cardObfuscatedNumber: String,
cardHolder: String,
expiryDate: String)
val response: PaymentGatewayResponse = doRequest(...)
response.map { r =>
r.paymentMethod match {
case "CREDIT_CARD" => r.metaData.extract[CreditCardMetadata]
case unsupportedType: String => throw new UnsupportedPaymentMethodException(unsupportedType)
}
}