I have a JSON I am trying to deserialize. I am trying to accommodate the following structure with the following constraints
1) Both rectangle and Square are of type Shape and can instead have a list of Shapes.
2) Their ordering can be switched a Square can have a list of Rectangles (list of shapes)
3) Both Square and Rectangle could also have a list of Circle which does not inherit anything from Shape and is basically composed within Shape
4) I want to uniquely identify square and rectangle class and not treat them as the same Shape object after deserialization within my model structure(wondering if there is way without parsing the type property so I know the object type is Square or Rectangle).
{
"type": "rectangle",
"x": "3",
"y": "3",
"children": [
{
"type": "square",
"x": "3",
"y": "4",
"children": [
{
"type": "circle",
"radius": "3"
},
{
"type": "circle",
"radius": "4"
},
{
"type": "circle",
"radius": "5"
}
]
}
]
}
The model objects I created so far
abstract class Shape {
abstract val x: String
abstract val y: String
abstract val type: String
abstract val children: List<Shape>
}
data class Square(override val x: String, override val y: String, override val type: String,
override val children: List<Shape>) : Shape()
data class Rectangle(override val x: String, override val y: String, override val type:
String, override val children: List<Shape>) : Shape()
data class Circle(val radius: String, val type: String)
Related
I'm using https://www.api-football.com to get football countries and leagues, Json response for countries is like this:
{
"get": "countries",
"parameters": [],
"errors": [],
"results": 164,
"paging": {
"current": 1,
"total": 1
},
"response": [
{
"name": "Albania",
"code": "AL",
"flag": "https://media.api-sports.io/flags/al.svg"
},
{
"name": "Algeria",
"code": "DZ",
"flag": "https://media.api-sports.io/flags/dz.svg"
},
{
"name": "Andorra",
"code": "AD",
"flag": "https://media.api-sports.io/flags/ad.svg"
},
...
I tried to create a model like this:
Here is the CountryData data class:
data class CountryData (
val code: String,
val flag: String,
val name: String
)
And here is the Country data class which uses CountryData and use them in a list:
data class Country(
val results : List<CountryData>
)
But those models give me some errors: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was NUMBER at line 1 column 61 path $.results
And I think that's because of the api response type is not a list of CountryData.
So if that's the problem, I want to get only the part after "response": [ ... But I don't know how to do this.
If you think another problem causes this error please let me know.
By the way, here is the api interface:
interface FootballApi {
#Headers("X-RapidAPI-Key: $API_KEY")
#GET(GET_COUNTRIES)
suspend fun getCountries() : Response<Country>
}
And here is my Retrofit Instance :
object RetrofitInstance {
val api: FootballApi by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(FootballApi::class.java)
}
}
I solved this problem by adding other data models to my project.
Here is the Country data class:
data class Country(
val code: String,
val flag: String,
val name: String
)
Here is the CountryResponse data class:
data class CountryResponse (
val get: String,
val parameters: List<Any>,
val errors: List<Any>,
val results: Int,
val paging: Paging,
val response: List<Country>
)
Here is the Paging data class:
data class Paging(
val current: Int,
val total: Int
)
And now I can get the country list by response.body()!!.response
The problem you had in the first place is that parameter names in your model do matter! At first you tried to unmarshal the JSON data into the wrong model:
data class Country(
val results : List<CountryData>
)
The JSON field with all the countries in it has the name response like this:
"response": [
{
"name": "Albania",
"code": "AL",
"flag": "https://media.api-sports.io/flags/al.svg"
},
...
So if you use the same attribute in your model it should work without the need of adding all the other attributes (which you probably don't need) to your model:
data class Country(
val response: List<CountryData>
)
I have a FeignClient which communicates with an external service, looks like below:
interface JiraClient {
\\ ...
#RequestMapping(method = [RequestMethod.GET], value = ["\${jira.api.agile}/sprint/{sprintId}/issue"])
fun getIssuesForASprint(
#PathVariable("sprintId") sprintId: Int,
#RequestParam(name = "startAt", required = false) startAt: Int = 0,
#RequestParam(name = "maxResults", required = false) maxResults: Int = MAX_RESULTS
): IssueHolder
#RequestMapping(method = [RequestMethod.GET], value = ["\${jira.api.insight}/objectschema/list"])
fun getObjectSchemaList(): List<ObjectSchema>
}
And here is my POJO:
#JsonIgnoreProperties(ignoreUnknown = true)
data class ObjectSchema(
#JsonProperty("id") val id: Int,
#JsonProperty("name") val name: String?,
#JsonProperty("objectSchemaKey") val objectSchemaKey: String?,
#JsonProperty("status") val status: String?,
#JsonProperty("created") #JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
) val created: Date?,
#JsonProperty("updated") #JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
) val updated: Date?,
#JsonProperty("objectCount") val objectCount: Int?,
#JsonProperty("objectTypeCount") val objectTypeCount: Int?
)
It won't work because the response has an additional root element:
{
"objectschemas": [
{
"id": 1,
"name": "APPS CATALOG",
"objectSchemaKey": "AC",
"status": "Ok",
"created": "2020-01-01T11:11:11.000Z",
"updated": "2020-01-01T11:11:11.000Z",
"objectCount": 123,
"objectTypeCount": 10
},
{
"id": 2,
"name": "ORG CATALOG",
"objectSchemaKey": "OC",
"status": "Ok",
"created": "2020-01-01T11:11:11.000Z",
"updated": "2020-01-01T11:11:11.000Z",
"objectCount": 456,
"objectTypeCount": 20
}
]
}
For the other responses, I have created holder data classes, because they have pagination info, that makes sense:
{
"expand": "schema,names",
"startAt": 0,
"maxResults": 200,
"total": 1024,
"issues": [
...
]
}
Goal:
I'd like to get it work, with an easy way to add some annotations (e.g.: # JsonRootName ) to certain data classes (but not all), so that I can unwrap the root value objectschemas out of the box. Please note, that the data class used here is only for deserialization purpose, I will not need it for serialization. And I don't wanna add a bunch of logic into fun feignDecoder(): Decoder. For the Feign Client interface, there is no chance to add any logic like DeserializationFeature.UNWRAP_ROOT_VALUE.
Does anyone know the easiest way to achieve this? Thank you very much.
The complete code snippets can be found here.
I am trying to consume and emit JSON which contains a polymorphic list of items. The problem is: the items contain type key with integer values (not strings). The API endpoint produces and expects JSON similar to this:
{
"startTime": "2022-07-27T13:32:57.379Z",
"items": [
{
"type": 0,
"results": "string",
"restBetweenRounds": "string"
},
{
"type": 1,
"results": "string",
"setCount": 0
},
{
"type": 2,
"items": [
{
"type": 0,
"results": "string",
"restBetweenRounds": "string"
},
{
"type": 1,
"results": "string",
"setCount": 0
}
],
"results": "string"
}
],
"status": 0,
"clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
As described in the article on polymorphism, I created an hierarchy of classes. I also try to convert type value before deserialization.
object MyTransformingDeserializer : JsonTransformingSerializer<BaseItem>(PolymorphicSerializer(BaseItem::class)) {
override fun transformDeserialize(element: JsonElement): JsonElement {
val type = element.jsonObject["type"]!!
val newType = JsonPrimitive(value = type.toString())
return JsonObject(element.jsonObject.toMutableMap().also { it["type"] = newType })
}
}
#Serializable(with = MyTransformingDeserializer::class)
sealed class BaseItem {
abstract val type: String
}
#Serializable
#SerialName("0")
class ItemType0(
override val type: String,
// ...
) : BaseItem()
#Serializable
#SerialName("1")
class ItemType1(
override val type: String,
// ...
) : BaseItem()
#Serializable
#SerialName("2")
class ItemType2(
override val type: String,
// ...
) : BaseItem()
But all I get is this error:
kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic
serializer was not found for class discriminator '0'
Given that I can not change the format of the JSON, what can be done to successfully serialize/desereialize it?
Handling polymorphism in Kotlinx Serialization is difficult, especially when you don't have control over the format of the source. But KxS does give a lot of low-level tools to manually handle almost anything.
You were close in choosing JsonTransformingSerializer! It seems that it doesn't transform the JSON before KxS selects a serializer. Because discriminators can only be strings, and so deserialization fails.
JsonContentPolymorphicSerializer
Instead of JsonTransformingSerializer, you can use JsonContentPolymorphicSerializer.
Kotlinx Serialization will first deserialize the JSON to a JsonObject. It will then provide that object to the serializer for BaseItem, and you can parse and select the correct subclass.
import kotlinx.serialization.*
import kotlinx.serialization.json.*
object BaseItemSerializer : JsonContentPolymorphicSerializer<BaseItem>(BaseItem::class) {
override fun selectDeserializer(
element: JsonElement
): DeserializationStrategy<out BaseItem> {
return when (val type = element.jsonObject["type"]?.jsonPrimitive?.intOrNull) {
0 -> ItemType0.serializer()
1 -> ItemType1.serializer()
2 -> ItemType2.serializer()
else -> error("unknown Item type $type")
}
}
}
Including type
Since this is manually performing polymorphic discrimination, there's no need to include type in your classes.
import kotlinx.serialization.Serializable
#Serializable(with = BaseItemSerializer::class)
sealed class BaseItem
#Serializable
data class ItemType0(
// ...
) : BaseItem()
#Serializable
class ItemType1(
// ...
) : BaseItem()
#Serializable
class ItemType2(
// ...
) : BaseItem()
However you might like to include it, for completeness, and so it's included when serializing. For that, you must use #EncodeDefault
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable
#Serializable(with = BaseItemSerializer::class)
sealed class BaseItem {
abstract val type: Int
}
#Serializable
class ItemType0(
// ...
) : BaseItem() {
#EncodeDefault
override val type: Int = 0
}
// ...
Complete example
Bringing it all together, here's a complete example.
import kotlinx.serialization.*
import kotlinx.serialization.json.*
val mapper = Json {
prettyPrint = true
prettyPrintIndent = " "
}
fun main() {
val json = """
{
"startTime": "2022-07-27T13:32:57.379Z",
"status": 0,
"clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"items": [
{
"type": 0,
"results": "string",
"restBetweenRounds": "string"
},
{
"type": 1,
"results": "string",
"setCount": 0
},
{
"type": 2,
"items": [
{
"type": 0,
"results": "string",
"restBetweenRounds": "string"
},
{
"type": 1,
"results": "string",
"setCount": 0
}
],
"results": "string"
}
]
}
""".trimIndent()
val itemHolder: ItemHolder = mapper.decodeFromString(json)
println(itemHolder)
println(mapper.encodeToString(itemHolder))
}
#Serializable
data class ItemHolder(
val startTime: String,
val clientId: String,
val status: Int,
val items: List<BaseItem>,
)
#Serializable(with = BaseItem.Serializer::class)
sealed class BaseItem {
abstract val type: Int
object Serializer : JsonContentPolymorphicSerializer<BaseItem>(BaseItem::class) {
override fun selectDeserializer(
element: JsonElement
): DeserializationStrategy<out BaseItem> {
return when (val type = element.jsonObject["type"]?.jsonPrimitive?.intOrNull) {
0 -> ItemType0.serializer()
1 -> ItemType1.serializer()
2 -> ItemType2.serializer()
else -> error("unknown Item type $type")
}
}
}
}
#Serializable
data class ItemType0(
val results: String,
val restBetweenRounds: String,
) : BaseItem() {
#EncodeDefault
override val type: Int = 0
}
#Serializable
data class ItemType1(
val results: String,
val setCount: Int,
) : BaseItem() {
#EncodeDefault
override val type: Int = 1
}
#Serializable
data class ItemType2(
val results: String,
val items: List<BaseItem>,
) : BaseItem() {
#EncodeDefault
override val type: Int = 2
}
This prints
ItemHolder(
startTime=2022-07-27T13:32:57.379Z,
clientId=3fa85f64-5717-4562-b3fc-2c963f66afa6,
status=0,
items=[
ItemType0(results=string, restBetweenRounds=string),
ItemType1(results=string, setCount=0),
ItemType2(
results=string,
items=[
ItemType0(results=string, restBetweenRounds=string),
ItemType1(results=string, setCount=0)
]
)
]
)
and
{
"startTime": "2022-07-27T13:32:57.379Z",
"clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": 0,
"items": [
{
"results": "string",
"restBetweenRounds": "string",
"type": 0
},
{
"results": "string",
"setCount": 0,
"type": 1
},
{
"results": "string",
"items": [
{
"results": "string",
"restBetweenRounds": "string",
"type": 0
},
{
"results": "string",
"setCount": 0,
"type": 1
}
],
"type": 2
}
]
}
which matches the input!
Versions
Kotlin 1.7.10
Kotlinx Serialization 1.3.4
I want to group by id.
[
{
"id": 1,
"name": "a"
},
{
"id": 2,
"name": "b"
},
{
"id": 3,
"name": "c"
}
]
The results should be as follows:
{
"1": "a",
"2": "b",
"3": "c"
}
What is the most idiomatic way of doing this in Kotlin?
Your question only shows JSON, so I'm not sure if this is about JSON serialization or Kotlin. Since it's tagged kotlin, I'm assuming you're already deserializing the initial list to Kotlin with something like this
data class NamedThing(val id: Int, val name: String)
val list: List<NamedThing> = TODO("somehow you're getting a list of those here")
If you already have this, you can easily create a map from this list using:
val map = list.associate { it.id to it.name }
I am trying to serialize the json response below, but I am unsure how to do it.
This is the Json my backend returns:
[
{
"title": "Dummy section, should not be seen",
"type": "dummy_test",
"metadata": []
},
{
"title": "Title1",
"type": "categories_products",
"metadata": [
{
"id": "1272"
}
]
},
{
"title": "Title2",
"type": "categories_products",
"metadata": [
{
"id": "996"
}
]
}
]
This is my ExploreItem class:
data class ExploreItem(
#SerializedName("metadata") val metadata: List<Metadata> = listOf(),
#SerializedName("title") val title: String = "",
#SerializedName("type") val type: String = ""
) {
enum class ExploreItemType(val value: String) {
#SerializedName("unknown")
UNKNOWN("unknown"),
#SerializedName("other_companies")
OTHER_COMPANIES("other_companies"),
#SerializedName("categories_products")
CATEGORIES_PRODUCTS("categories_products"),
#SerializedName("popular_categories")
POPULAR_CATEGORIES("popular_categories")
}
}
data class Metadata(
#SerializedName("id") val id: String = ""
)
And now I am trying to serialize it in the repository like this:
Serializer.defaultJsonParser.fromJson(response.body!!.string(),ExploreItem::class.java )
but it doesn't work because it's expecting a list of ExploreItem. How can I rewrite the serializer expression to parse it into a list?
From your error
Type mismatch. Required:List Found:ExploreItem!
Post errors is very important, Gson is telling you that it wants a List and not an object of ExploreItem.
In other words, you are telling to Gson with the call Serializer.defaultJsonParser.fromJson(response.body!!.string(),ExploreItem::class.java )
"Hey Gson, from the string I want an object ExploreItem", and Gson is telling you "Hey my friend, you string start with [ ] for sure it is a list of something and not a single object."
You need to pass in the Serializer.defaultJsonParser.fromJson(response.body!!.string(),List<ExploreItem>::class.java)
P.s: I'm not sure about the Kotlin syntax