How to parse this such json in Kotlin with GSON? - json

I have JSON object like this
{
"codemap":{
"codeOfItem1":"titleOfItem1",
"codeOfItem2":"titleOfItem2",
"codeOfItem3":"titleOfItem3",
"codeOfItem4":"titleOfItem4"
},
"items":{
"titleOfItem1":{
"attribute1":"value1",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item1",
"subattr2":"value1_of_subattr2_for_item1"
}
},
"titleOfItem2":{
"attribute1":"value2",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item2",
"subattr2":"value1_of_subattr2_for_item2"
}
},
"titleOfItem3":{
"attribute1":"value2",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item3",
"subattr2":"value1_of_subattr2_for_item3"
}
},
"titleOfItem4":{
"attribute1":"value2",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item4",
"subattr2":"value1_of_subattr2_for_item4"
}
}
}
}
How to parse it using GSON in Kotlin ?
(Problem is that strings like titleOfItemXXX is both values in codemap map and key names in items map
I don't really like idea to go fully manual way like in How to parse this Json with no object name
Update:
I don't want to get scheme like this (this is from Kotlin-to-JSON Android Studio Plugin)
import com.google.gson.annotations.SerializedName
data class x1(
#SerializedName("codemap")
val codemap: Codemap,
#SerializedName("items")
val items: Items
) {
data class Codemap(
#SerializedName("codeOfItem1")
val codeOfItem1: String, // titleOfItem1
#SerializedName("codeOfItem2")
val codeOfItem2: String, // titleOfItem2
#SerializedName("codeOfItem3")
val codeOfItem3: String, // titleOfItem3
#SerializedName("codeOfItem4")
val codeOfItem4: String // titleOfItem4
)
data class Items(
#SerializedName("titleOfItem1")
val titleOfItem1: TitleOfItem1,
#SerializedName("titleOfItem2")
val titleOfItem2: TitleOfItem2,
#SerializedName("titleOfItem3")
val titleOfItem3: TitleOfItem3,
#SerializedName("titleOfItem4")
val titleOfItem4: TitleOfItem4
) {
data class TitleOfItem1(
#SerializedName("attribute1")
val attribute1: String, // value1
#SerializedName("atttribute2")
val atttribute2: Atttribute2
) {
data class Atttribute2(
#SerializedName("subattr1")
val subattr1: String, // value1_of_subattr1_for_item1
#SerializedName("subattr2")
val subattr2: String // value1_of_subattr2_for_item1
)
}
data class TitleOfItem2(
#SerializedName("attribute1")
val attribute1: String, // value2
#SerializedName("atttribute2")
val atttribute2: Atttribute2
) {
data class Atttribute2(
#SerializedName("subattr1")
val subattr1: String, // value1_of_subattr1_for_item2
#SerializedName("subattr2")
val subattr2: String // value1_of_subattr2_for_item2
)
}
data class TitleOfItem3(
#SerializedName("attribute1")
val attribute1: String, // value2
#SerializedName("atttribute2")
val atttribute2: Atttribute2
) {
data class Atttribute2(
#SerializedName("subattr1")
val subattr1: String, // value1_of_subattr1_for_item3
#SerializedName("subattr2")
val subattr2: String // value1_of_subattr2_for_item3
)
}
data class TitleOfItem4(
#SerializedName("attribute1")
val attribute1: String, // value2
#SerializedName("atttribute2")
val atttribute2: Atttribute2
) {
data class Atttribute2(
#SerializedName("subattr1")
val subattr1: String, // value1_of_subattr1_for_item4
#SerializedName("subattr2")
val subattr2: String // value1_of_subattr2_for_item4
)
}
}
}
because I don't really known how much items I will have and which names they will use in production.

I think you want dynamic-named property in the schema. In this case, declare it as Map:
data class Schema(val codemap: Map<String, String>, val items: Map<String, Item>) {
data class Item(val attribute1: String, val atttribute2: Attr2) {
data class Attr2(val subattr1: String, val subattr2: String)
}
}
fun gsonDemo() {
val json = """
{
"codemap":{
"codeOfItem1":"titleOfItem1",
"codeOfItem2":"titleOfItem2",
"codeOfItem3":"titleOfItem3",
"codeOfItem4":"titleOfItem4"
},
"items":{
"titleOfItem1":{
"attribute1":"value1",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item1",
"subattr2":"value1_of_subattr2_for_item1"
}
},
"titleOfItem2":{
"attribute1":"value2",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item2",
"subattr2":"value1_of_subattr2_for_item2"
}
},
"titleOfItem3":{
"attribute1":"value2",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item3",
"subattr2":"value1_of_subattr2_for_item3"
}
},
"titleOfItem4":{
"attribute1":"value2",
"atttribute2":{
"subattr1":"value1_of_subattr1_for_item4",
"subattr2":"value1_of_subattr2_for_item4"
}
}
}
}
""".trimIndent()
val obj = Gson().fromJson(json, Schema::class.java)
println(obj.items[obj.codemap["codeOfItem3"]]?.atttribute2?.subattr1) // print value1_of_subattr1_for_item3
}
Note that Gson won't fail if some properties are missing, so all the values of properties may be null, although they are declared as non-nullable

Looks like I forget about simple ways :(
Working answer:
data class TopLevel (
#SerializedName("codemap")
val codemap: Map<String, String>,
#SerializedName("items")
val items: Map<String, Item>
)
data class Item (
#SerializedName("attribute1")
val attribute1: Attribute1,
#SerializedName("attribute2")
val attribute2: Attribute2
)
data class Attribute2 (
#SerializedName("subattr1")
val subattr1: String,
#SerializedName("subattr2")
val subattr1: String
)
enum class Attribute1 {
#SerializedName("DarkSide")
DarkSide,
#SerializedName("LightSide")
LightSide
}
var gson: Gson = Gson()
val str=... //string with source JSON
val result = gson.fromJson(str, TopLeve::class.java)
Everything appears to work correctly.

Related

Kotlinx Deserialization Object or List of Object to List of Object

I am currently implementing an API client with Ktor. The API I am requesting does not return a consistent JSON format.
for Example:
sometimes the JSON looks like this:
{
"description": {
"lang": "en",
"value": "an English description..."
},
...
}
and sometimes like this:
{
"description": [
{
"lang": "en",
"value": "an English description..."
},
{
"lang": "fr",
"value": "a French description..."
}
],
...
}
Now my Question:
How can I implement a Custom Kotlinx Deserializer to Decode an Object of T or a List<T> to a List<T>
My classes look like this:
#Serializable
class ResourceResponse(
#SerialName("description")
val descriptions: List<Description>
) {
#Serializable
data class Description(
#SerialName("value")
val value: String,
#SerialName("lang")
val language: String,
)
}
I want that a Json with only one Description-Object will be deserialized to a List with one Object and not specifically for the description, but in general for classes.
I've found nothing really helpful in the Web.
One solution is to first deserialize it to JsonElement, introspect and then decide how to deserialize it further into ResourceResponse:
fun decode(s: String): ResourceResponse {
val json = Json.parseToJsonElement(s).jsonObject
return when (val desc = json["description"]) {
is JsonArray -> Json.decodeFromJsonElement(json)
is JsonObject -> {
val json2 = json.toMutableMap()
json2["description"] = JsonArray(listOf(desc))
Json.decodeFromJsonElement(JsonObject(json2))
}
else -> throw IllegalArgumentException("Invalid value for \"description\": $desc")
}
}
This solution is definitely not ideal. It may be potentially less performant as we need to deserialize the whole tree into the tree of JsonElement objects only to transform it to the final types (although, maybe the library does this internally anyway). It works only for json and it is tricky to use this solution if ResourceResponse is somewhere deep into the data structure.
You can use a JsonContentPolymorphicSerializer to choose a deserializer based on the form of the JSON.
This one should work:
#Suppress("UNCHECKED_CAST")
class DescriptionsSerializer : JsonContentPolymorphicSerializer<List<ResourceResponse.Description>>(
List::class as KClass<List<ResourceResponse.Description>>
) {
// Here we check the form of the JSON we are decoding, and choose
// the serializer accordingly
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out List<ResourceResponse.Description>> {
return if (element is JsonArray)
ListSerializer(ResourceResponse.Description.serializer())
else
SingleDescriptionAsList()
}
class SingleDescriptionAsList : KSerializer<List<ResourceResponse.Description>> {
override val descriptor: SerialDescriptor
get() = ResourceResponse.Description.serializer().descriptor
override fun deserialize(decoder: Decoder): List<ResourceResponse.Description> {
return listOf(ResourceResponse.Description.serializer().deserialize(decoder))
}
override fun serialize(encoder: Encoder, value: List<ResourceResponse.Description>) {
throw Exception("Not in use")
}
}
}
You must also amend your original class to tell it to use this serializer:
#Serializable
class ResourceResponse(
#SerialName("description")
#Serializable(with = DescriptionsSerializer::class) val descriptions: List<Description>
) {
#Serializable
data class Description(
#SerialName("value")
val value: String,
#SerialName("lang")
val language: String,
)
}
Then you will be able to decode JSON objects with the single key "descriptions" using the ResourceResponse serializer.
For avoidance of doubt, if there are other keys in the JSON (it's not entirely clear from the question) then those should also be written into ResourceResponse definition.
After my research, I have now come up with a solution. For this you need a wrapper class. (here GenericResponse). I hope I can help others who have the same problem.
This is the Wrapper-Class
#Serializable(with = ListOrObjectSerializer::class)
class GenericResponse<T>(
val data: List<T> = emptyList()
) {
private var _isNothing : Boolean = false
val isNothing: Boolean
get() {
return this._isNothing
}
companion object {
fun <T> nothing(): GenericResponse<T> {
val o = GenericResponse(emptyList<T>())
o._isNothing = true
return o
}
}
}
And the Serializer looks like:
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
class ListOrObjectSerializer<T : Any>(private val tSerializer: KSerializer<T>): KSerializer<GenericResponse<T>> {
override val descriptor: SerialDescriptor
get() = tSerializer.descriptor
override fun deserialize(decoder: Decoder): GenericResponse<T> {
val input = decoder as JsonDecoder
val jsonObj = input.decodeJsonElement()
return when(jsonObj) {
is JsonObject -> GenericResponse(listOf(Json.decodeFromJsonElement(tSerializer, jsonObj)))
is JsonArray -> GenericResponse(Json.decodeFromJsonElement(ListSerializer(tSerializer), jsonObj))
else -> return GenericResponse.nothing()
}
}
override fun serialize(encoder: Encoder, value: GenericResponse<T>) {
throw IllegalAccessError("serialize not supported")
}
}
My Data-Class look now like:
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
#Serializable
class ResourceResponse(
#SerialName("description")
val descriptions: GenericResponse<Description>? = null,
) {
#Serializable
data class Description(
#SerialName("value")
val value: String? = null,
#SerialName("lang")
val language: String? = null,
)
}
data class ResourceResponse(
#SerializedName("description") val descriptions: List<Description>,
)
data class Description(
#SerializedName("value") val value: String,
#SerializedName("lang") val language: String,
)
it should be like that

How to make data class of a JSON data if it is not having keys in Kotlin (Android studio)?

{
"results" : [
[ "5bcafde29600021147742e3a",
"ljsdlfj",
"ljdfl",
"url",
"title",
15.2805
],
[ "5bcafde29600021147742e3a",
"ljsdlfj",
"ljdfl",
"url",
"title",
14.2805
],
[ "5bcafde29600021147742e3a",
"ljsdlfj",
"ljdfl",
"url",
"title",
13.2805
]
],
"count": 53,
"total": 53,
"perpage": 10,
"page": 0
}
Although I tried making it using kotlin data class file from JSON - plugin but it resulted
like this -
data class store(
val count: Int,
val page: Int,
val perpage: Int,
val results: List<List<Any>>,
val total: Int
)
See, here it created val results: List<List>, which i don't understand how to use in adapter. Is there any way to solve it?
Hope you understand the question, if you found trouble in getting json data check here
Here's my adapter class, I need url, title and description from results (of Json Data) into pDecp, pTitle and pImg.
class PatentAdapter(val context: Context, val patents: List<List<Any>>) :
RecyclerView.Adapter<PatentAdapter.PatentViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PatentViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.patent_item, parent, false)
return PatentViewHolder(view)
}
override fun onBindViewHolder(holder: PatentViewHolder, position: Int) {
val patentData : List<List<Patent>> = patents as List<List<Patent>>
holder.pDecp.text =
holder.pTitle.text =
}
override fun getItemCount(): Int {
return patents.size
}
class PatentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var pTitle = itemView.findViewById<TextView>(R.id.tv_patentTitle)
var pDecp = itemView.findViewById<TextView>(R.id.tv_patentDescription)
var pImg = itemView.findViewById<ImageView>(R.id.iv_patentImg)
}
}
I figured out the solution, it will be great if you can improve it while it works for me ->
Data class would be like this ->
data class Patent(
val count: Int,
val page: Int,
val perpage: Int,
val results: List<List<String>>,
val total: Int
)
And in adapter class we have to take the postion of array or list if we didn't have keys like this ->
class PatentAdapter(val context: Context, val patents: Patent) :
RecyclerView.Adapter<PatentAdapter.PatentViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PatentViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.patent_item, parent, false)
return PatentViewHolder(view)
}
override fun onBindViewHolder(holder: PatentViewHolder, position: Int) {
val patentData : List<String> = patents.results[position]
holder.pDecp.text = patentData[3]
holder.pTitle.text = patentData[2]
Glide.with(this.context).load(patentData[10].toString()).into(holder.pImg)
}
override fun getItemCount(): Int {
return patents.results.size
}
class PatentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var pTitle = itemView.findViewById<TextView>(R.id.tv_patentTitle)
var pDecp = itemView.findViewById<TextView>(R.id.tv_patentDescription)
var pImg = itemView.findViewById<ImageView>(R.id.iv_patentImg)
}
}

Dynamic unknow field in JSON

I'm trying to build the right classes from the following URL:
https://api.nasa.gov/neo/rest/v1/feed?start_date=2020-01-01&end_date=2020-01-08&api_key=DEMO_KEY
At this moment, I have the following structure:
data class NearEarthObject (val asteroidObjects : Map<String, DateSelected>)
data class DateSelected (val date: ArrayList<Asteroid>) {
data class Asteroid(
val id: Long,
val codename: String,
val closeApproachDate: String,
val absoluteMagnitude: Double,
val estimatedDiameter: Double,
val relativeVelocity: Double,
val distanceFromEarth: Double,
val isPotentiallyHazardous: Boolean
)
And this is my Java code:
class MainActivity : AppCompatActivity() {
val URLAPI = Constants.BASE_URL
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
asteroidActivityRv.layoutManager = LinearLayoutManager(this)
asteroidActivityRv.adapter = null
val retrofitAsteroids = Retrofit.Builder()
.baseUrl(URLAPI)
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiAsteroid = retrofitAsteroids.create(ApiAsteroids::class.java)
val callAsteroid = apiAsteroid.getAsteroids()
callAsteroid.enqueue(object : Callback<NearEarthObject> {
override fun onFailure(call: Call<NearEarthObject>?, t: Throwable?) {
Log.e("TAG fail", t.toString())
}
override fun onResponse(
call: Call<NearEarthObject>,
response: Response<NearEarthObject>
) {
for (res in response.body().asteroidObjects) {
// Log.e("TAG result", res.value.date)
}
}
})
}
}
But I keep receiving the following error:
java.lang.NullPointerException: Attempt to invoke interface method 'java.util.Set java.util.Map.entrySet()' on a null object reference
at com.example.nasanwsproject.MainActivity$onCreate$1.onResponse(MainActivity.kt:44)
at retrofit2.ExecutorCallAdapterFactory$ExecutorCallbackCall$1$1.run(ExecutorCallAdapterFactory.java:68)
Does anybody know what I'm doing wrong?
Thanks a lot for your help!
it looks response.body().asteroidObjects==null
i think this problem because you cant receive the response use map。
it require a definite class(include the field and value。)

How to edit existing JSON object with sprayJSON

I am using akka with spray json support for which I need to edit value in the recieved json.
import akka.http.scaladsl.server.Directives
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import spray.json._
final case class Item(name: String, id: Long)
final case class Order(items: List[Item],orderTag:String)
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
implicit val itemFormat = jsonFormat2(Item)
implicit val orderFormat = jsonFormat2(Order)
}
In my use case I recieve the json with orderTag value as null, all I need to do is edit the orderTag value with and then use it as entity value.Is it possible to write/edit jsonObject and How to do that ?
class MyJsonService extends Directives with JsonSupport {
// format: OFF
val route =
get {
pathSingleSlash {
complete(Item("thing", 42)) // will render as JSON
}
} ~
post {
entity(as[Order]) { order => // will unmarshal JSON to Order
val itemsCount = order.items.size
val itemNames = order.items.map(_.name).mkString(", ")
complete(s"Ordered $itemsCount items: $itemNames")
}
}
}
You can just edit the json AST like ..
val json = """{"orderTag":null}"""
val jsVal = json.parseJson
val updatedJs = if (jsObj.fields.get("orderTag") == Some(JsNull)) {
JsObject(jsObj.fields + ("orderTag" -> JsString("new tag")))
} else {
jsObj
}
updatedJs.compactPrint
res26: String = """
{"orderTag":"new tag"}
"""

Accessing inner JSON field in Scala

I have JSON string as
{
"whatsNew" : {
"oldNotificationClass" : "WhatsNewNotification",
"notificationEvent" : { ..... },
"result" : {
"notificationCount" : 10
.....
}
},
......
"someEmpty": { },
......
}
I am trying to get notificationCount field using json4s in Scala as follow but notificationCount is coming as empty for all. Any help?
UPDATE
Also if some pair is empty how can I handle empty condition and continue to loop?
Function returning JSON string from file
def getData(): Map[String, AnyRef] = {
val jsonString = scala.io.Source.fromInputStream(this.getClass.getResourceAsStream("/sample.json")).getLines.mkString
val jsonObject = parse( s""" $jsonString """)
jsonObject.values.asInstanceOf[Map[String, AnyRef]]
}
Code to get fields
val myMap: Map[String, AnyRef] = MyDataLoader.getData
for((key, value) <- myMap) {
val id = key
val eventJsonStr: String = write(value.asInstanceOf[Map[String, String]] get "notificationEvent")
val resultJsonStr: String = write(value.asInstanceOf[Map[String, String]] get "result")
//val notificationCount: String = write(value.asInstanceOf[Map[String, Map[String, String]]] get "notificationCount")
}
You can use path and extract like so:
val count: Int = (parse(jsonString) \ "whatsNew" \ "result" \ "notificationCount").extract[Int]
you will need this import for the .extract[Int] to work:
implicit val formats = DefaultFormats
To do it in a loop:
parse(jsonString).children.map { child =>
val count: Int = (child \ "result" \ "notificationCount").extract[Int]
...
}