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

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.

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.

Ktor handle empty response for arrays

I have recently started using Ktor and got stuck at the very beginning itself.
I have a very simple response, which could have content like below -
{
"result": true,
"data": [
{
"Name": "Danish",
"Credit": "80"
},
{
"Name": "Kumar",
"Credit": "310"
}
]
}
Or it could be like this -
{
"result": false,
"data": [],
"message": "No data available, use default user",
"default": [
{
"Name": "Default User",
"Credit": "100"
}
]
}
And my response class is like -
#Serializable
data class UserResponse(
#SerialName("result") var result: Boolean? = null,
#SerialName("data") var data: ArrayList<User?>? = null,
#SerialName("message") var message: String? = null,
#SerialName("default") var default: ArrayList<User?>? = null
)
#Serializable
data class UserResponse(
#SerialName("Name") var name: String? = null,
#SerialName("Credit") var credit: String? = null,
)
io.ktor.client.call.NoTransformationFoundException: No transformation found: class io.ktor.utils.io.ByteBufferChannel
And I am getting NoTransformationFoundException, I think it could be due to data object being empty, but how to fix this?
According to this, we can catch this exception, but I can't use this as I need other data to be used.
Exception looks like you haven't install Json content negotiation plugin, when creating ktor client. It should be like this:
val httpClient = HttpClient {
install(ContentNegotiation) {
json()
}
}
Then you can use this client like this:
val response: UserResponse = httpClient.get("URL").body()

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

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

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

How to use Retrofit 2 to send object with list of another object with image

I need somebody help for this case:
Here the format of my object :
{
"transport": true,
"fraisTransport": "string"
"cars": [
{
"id": "string",
"prix": "string",
"photo_url1" : "string",
"photo_url2" : "string"
},
{
"id": "string",
"prix": "string",
"photo_url1" : "string",
"photo_url2" : "string"
}
]
}
Here is my Api interface
#Multipart
#POST("declaration")
fun addDeclaration( #Part carsImage: Array<MultipartBody.Part> ,
#Part propertyCars: MultipartBody.Part,
#Part dataDeclaration: RequestBody): Observable<Response>
here how i set the variable for dataDEclaration
val jsonObject = JSONObject()
jsonObject.put("transport", declaration.transport)
jsonObject.put("frais_transport", declaration.fraisTransport)
val bodyDeclarationInfo = jsonObject.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
Now my problem is how to set the variable for cars objects. Please any suggestions will welcome. Thanks
I adivise you to either send data as a class following this methode : https://futurestud.io/tutorials/retrofit-send-objects-in-request-body
Or you can create your json and send it like this : How to POST raw whole JSON in the body of a Retrofit request?
After long search i got a way to solve my problem. Perhaps that can help somebody.
#Multipart
#POST("declaration")
fun addDeclaration(#Part carsImage: MutableList<MultipartBody.Part>, #PartMap
declaration: HashMap<String?, RequestBody?>?): Observable<Response>
val declarationInfo: HashMap<String?, RequestBody?>? = HashMap()
declarationInfo["transport"] = createPartFromString("" + declarationVente.transport)
task["fraisTransport"] = createPartFromString("" +declarationVente.fraisTransport)
val body: MutableList<MultipartBody.Part> = mutableListOf()
val listA: List<Cars> = mutableListOf<Cars>().apply {
for (aD in 0 until it.size) {
declarationInfo["cars[$aD][id]"] = createPartFromString(it[aD].animalId.toString())
declarationInfo["cars[$aD][prix]"] = createPartFromString(it[aD].prixDeclaration.toString())
val photoFile1 = File(it[aD].photo1)
val reqFile = photoFile1.asRequestBody("image/*".toMediaTypeOrNull())
val imageFile1 = MultipartBody.Part.createFormData("cars[$aD][uploadFile1]", photoFile1.name, reqFile)
body.add(imageFile1)
}
}