How can I create a model for this API (Kotlin-Retrofit)? - kotlin

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>
)

Related

How to convert json object with objects to json array with objects

I have a json object that looks like this.
{
"Items": {
"zzzz": {
"id": "zzzz",
"title": "qqqqqqq",
"notifications": []
},
"rrrrr": {
"id": "rrrrr",
"title": "rrrrrrrrrrrrrrrrrr",
"notifications": []
},
"eeeee": {
"id": "eeeee",
"title": "eeeeeeeeeeeeeeeeeeee",
"notifications": []
},
"wwww": null,
"dddddd": {
"id": "dddddd",
"title": "ddddddddddddddddddddddddd",
"notifications": []
},
"qqq": {
"id": "qqq",
"title": "qqqqqqqqqqqqqqqqqqqqqq",
"notifications": []
},
"rrrrrr": null
}
}
My data class:
data class Response(
val Items: List<Notification>
........)
data ckass Notification(
val id : String,
val title: String,
val notifications: List<...>,
I need a List with objects zzzz,rrrr and so on to get into the data class with val items. But I can't figure out how to convert the incoming json object to a json array
I wanted to use my own deserializer, but in my case it won't help because I use one instance of okhttp and retrofit for all requests. And also, a response always comes from the server in the form of:
"Items": {
//other request body
},
.....
}
I am not sure what deserializer you using. Here's a solution assuming Jackson, but maybe you can take the ideas from this if you are using Gson, etc.
The key idea is to use an intermediary object to deserialize into - a Map whose key values you ignore:
// your desired data classes
data class Response(
val items: List<Notification>,
)
data class Notification(
val id: String,
val title: String,
val notifications: List<Any>,
)
// an intermediary object
// I notice that some Notifications are null, hence the `?`
data class ResponseWithObjects(
#JsonProperty("Items") // this is needed for Jackson since I used a conventional variable name Kotlin side
val items: Map<String, Notification?>,
)
fun main(args: Array<String>) {
val actualResponse: ResponseWithObjects = TestUtils.deserialize("/test.json", ResponseWithObjects::class)
println(actualResponse)
val desiredResponse = Response(
items = actualResponse.items
.values.filterNotNull() // assuming you don't want the null notifications in the resultant array
.toList(),
)
println(desiredResponse)
}
To convert the given JSON object to a list of Notification objects, you can iterate over the key-value pairs in the "Items" object and create a Notification object for each non-null value. Here's some sample Kotlin code that demonstrates this:
val json = // the JSON object from your example
val itemsObject = json.getJSONObject("Items")
val notifications = mutableListOf<Notification>()
for (key in itemsObject.keys()) {
val item = itemsObject.getJSONObject(key)
if (item != null) {
val notification = Notification(
item.getString("id"),
item.getString("title"),
// add logic to parse notifications list here
)
notifications.add(notification)
}
}
val response = Response(notifications)
Note that you'll need to fill in the logic to parse the "notifications" list for each Notification object. If it's just an array of strings, you can use item.getJSONArray("notifications").toList() to get a list of strings.

Looking for the simplest way to unwrap root element for certain responses with FeignClient

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.

Polymorphic kotlinx serialization when type is integer, not string

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

flatten list with repeated element in kotlin

I have the following data class as a data transfer object.
data class Continent(
val continent: String,
val countries: List<String>
)
so the JSON response is like below.
{
"content": [
{
"continent": "Europe",
"countries": [
"France",
"Germany"
]
}
]
}
However, what I want to do is this:
{
"content": [
{
"continent": "Europe",
"country": "France"
},
{
"continent": "Europe",
"country": "Germany"
}
]
}
I suppose there must be some kotlin collection functions to apply.
Since the dto i wrote in the first code block is unable to be moderated, i have to apply collection functions on the service layer and make response dto accordingly.
Could you please let me know? Thanks in advance.
data class Country(
val continent: String,
val country: String
)
fun List<ContinentResponse>.toCountries(): List<Country> =
flatMap { r -> r.countries.map { country -> Country(r.continent, country) } }

Type mismatch during Gson deserialize json response with kotlin

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