More efficient way to model JSON data using retrofit - Kotlin - android

The data that I want to use has this structure:
{
"1": {
"id": 1,
"name": "Bulbasaur"
},
"2": {
"id": 2,
"name": "Ivysaur"
},
"3": {
"id": 3,
"name": "Venusaur"
}
}
Note:
The number labeling each object matches the id of the Pokémon, not the number of Pokémon
My problem is that when I try to create data classes for this it ends up creating a data class for each object. Not one data class that fits each object. I believe this is due to the number labeling the object(Pokémon) being different for each object.
Is there a way I can format this data in maybe one or two data classes and not over 800?
Ideally I would like the data to be structured like this but it does not work when run.
data class ReleasedPokemonModel(
val id: Int,
val name: String
)

When parsing Json to Object with this special case, you should custom Json Deserializer yourself.
Here I use Gson library to parse Json to Object.
First, create a custom Json Deserializer with Gson. As follows:
PokemonResponse.kt
data class PokemonResponse(
val pokemonMap: List<StringReleasedPokemonModel>
)
data class ReleasedPokemonModel(
val id: Int,
val name: String
)
GsonHelper.kt
object GsonHelper {
fun create(): Gson = GsonBuilder().apply {
registerTypeAdapter(PokemonResponse::class.java, PokemonType())
setLenient()
}.create()
private class PokemonType : JsonDeserializer<PokemonResponse> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): PokemonResponse {
val list = mutableListOf<ReleasedPokemonModel>()
// Get your all key
val keys = json?.asJsonObject?.keySet()
keys?.forEach { key ->
// Get your item with key
val item = Gson().fromJson<ReleasedPokemonModel>(
json.asJsonObject[key],
object : TypeToken<ReleasedPokemonModel>() {}.type
)
list.add(item)
}
return PokemonResponse(list)
}
}
}
Next I will create a GsonConverterFactory so that I can addConvertFactory to Retrofit.
val gsonConverterFactory = GsonConverterFactory.create(GsonHelper.create())
And now I will add retrofit.
val retrofit = Retrofit.Builder()
// Custom your Retrofit
.addConverterFactory(gsonConverterFactory) // Add GsonConverterFactoty
.build()
Finally in ApiService, your response will now return type PokemonResponse.
interface ApiService {
#GET("your_link")
suspend fun getGenres(): PokemonResponse
}

The problem is that there's no JSON array there. it's literally one JSON object with each Pokemon listed as a property. I would recommend that you reformat the JSON beforehand to look like this:
[
{
"id": 1,
"name": "Bulbasaur"
},
{
"id": 2,
"name": "Ivysaur"
},
{
"id": 3,
"name": "Venusaur"
}
]
And then you could model it like this:
data class ReleasedPokemonModel(
val id: Int,
val name: String
)
data class Response(
val items: List<ReleasedPokemonModel>
)
See more here.
And see here for discussion about reformatting the data before handing it to Retrofit.

You can use Map to store the key like the following
data class PokemonResponse(
val pokemonMap:Map<String,ReleasedPokemonModel>
)
data class ReleasedPokemonModel(
val id: Int,
val name: String
)

Related

Graphql get direct requested model

I implemented a sample of Graphql by Retrofit. I have a response like this:
if (response.isSuccessful) {
Log.e("response", response.body().toString())
Also, this is my interface class:
suspend fun postDynamicQuery(#Body body: String): Response<String>
Now I want to change my method by giving a direct model. this is the servers' answer.
{
"data": {
"getCityByName": {
"id": "112931",
"name": "Tehran",
"country": "IR",
"coord": {
"lon": 51.4215,
"lat": 35.6944
}
}
}
To give a model answer, I should have a model like this:
data class CityModel(
val data: Data
)
data class Data(
val getCityByName: GetCityByName
)
data class GetCityByName(
val id: String,
val name: String,
val country: String,
val coord: Coord
)
data class Coord(
val lon: Double,
val lat: Double
)
And these two changes:
if (response.isSuccessful) {
cityModel = response.body()}
and
suspend fun postDynamicQuery(#Body body: String): Response<CityModel>
PROBLEM: I want a city model without creating a Data model and a CityModel model. this is a boilerplate to make two extra models for each API.
I used GsonConverterFactory for converting to model:
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
I searched for solving this problem. The only way is using the Appolo library.
It has some benefits that I'll mention.
Type-safety: All models are created automatically by the schema. Also, you can use the models in your application.
Less-code: this library provides some code via using the GraphQL schematic.
Error-free: your queries and model are created automatically by the schematic, and as a result, you don't have any mistakes.
Some tools provided for you IDE for easy using
For adding this library you need to add these dependencies on your build.gradle file.
implementation("com.apollographql.apollo3:apollo-runtime:3.3.2")
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.0'

How To Upload Multiple Image URL String Within Array

{
"image_url": [
{
"image_url": "url1"
},
{
"image_url": "url1"
},
{
"image_url": "url1"
}
]
}
I Want To Pass image_url As An Array To API With Multiple Image URL [String] How Can I Pass Like This Using Retrofit [Android - Kotlin]
val jsResult = JSONObject()
val jsArray = JSONArray()
for (i in 0 until imgOnlineList.size) {
val jGroup = JSONObject()
jGroup.put("imageURL", imgOnlineList[i])
jsArray.put(jGroup)
}
jsResult.put("productCategory", jsArray)
Constant.logD(mTAG, "productCategoryOnlineList : ", jsResult.toString())
Since you are using Retrofit with Kotlin, you don't need to write plain Json objects. You can set Gson converter in Retrofit Builder.
You can create two data classes
data class Image(val image_url: String)
data class ImageList(val image_url: List<Image>)
And then pass them to API call as #Body payload or however is required by API provider. e.g.,
#POST("{api_end_point}")
fun sendImages(#Body imageList: ImageList)

Retrofit with custom JsonDeserializer in Kotlin

I need to parse this json. It works fine by default, but I need to add timestamp, so i use custom deserialize factory.
[
{
"ccy": "USD",
"base_ccy": "UAH",
"buy": "26.60000",
"sale": "26.96000"
},
{
"ccy": "EUR",
"base_ccy": "UAH",
"buy": "28.95000",
"sale": "29.60000"
},
{
"ccy": "RUR",
"base_ccy": "UAH",
"buy": "0.35000",
"sale": "0.38500"
},
{
"ccy": "BTC",
"base_ccy": "USD",
"buy": "8610.8989",
"sale": "9517.3093"
}
]
But json: JsonElement? parametr in MyDeserializer never come with ArrayList, always in single object. How i can read remote json for ArrayList, аnd modify it?
class MyDeserializer : JsonDeserializer<ArrayList<CurrencyItem>> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): ArrayList<CurrencyItem> {
//Clean array save result
var currrencyList = ArrayList<CurrencyItem>()
// Get remote json
val itemsJsonArray = jsonObject.asJsonArray
//Modify remote json to custom object with timestamp
for (item in itemsJsonArray) {
var JsonObject = item.asJsonObject
var ccy = JsonObject.get("ccy").asString
var base_ccy = JsonObject.get("base_ccy").asString
var buy = JsonObject.get("buy").asString
var sale = JsonObject.get("sale").asString
var timestamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
currrencyList.add(CurrencyItem(1, ccy, base_ccy, buy, sale, time))
}
return currrencyList
}
}
Add custom converter to retrofit
//Add converter to retrofit
val retrofit =
Retrofit.Builder().baseUrl("https://api.privatbank.ua/")
//My custom converter
.addConverterFactory(GsonConverterFactory.create(customGson))
.client(okkHttpclient)
.build()
The problem is you're registering your deserializer with the wrong type:
JsonDeserializer<ArrayList<CurrencyItem>>
The type for your deserializer is
ArrayList<CurrentItem>
Yet, you're registering it with
CurrencyItem::class.java
Now, registering generic types is not straightforward as plain types.
In your case you need:
Type currencyItemListType = new TypeToken<ArrayList<CurrencyItem>>() {}.getType();
registerTypeAdapter(currencyItemListType, MyDeserializer())code here
With that, when de retrofit call returns with a success code (2xx) it shoud automatically try to deserialize the json with your deserializer

Android App crashes as Json element is empty String ("") and not Object

I am working on an android project and using RxAndroid, Retrofit to make API call and retrieve json. The json looks something like following :
{
"result": [
{
"parent": "jhasj",
"u_deviation": "skasks",
"caused_by": "ksks",
"u_owner_mi": {
"link": "https://gddhdd.service-now.com/api/now/v1/table/sys_user/ghtytu",
"value": "ghtytu"
},
"impact": "",
}
]
}
I am using gson to parse the Json. The problem is "u_owner_mi" sometimes reruns empty string "" when there is no value assigned to it. I don't have access to change the return type to null. This is making my app crash as I am expecting an object here.
I get the following error :
Expected BEGIN_OBJECT but was STRING
If you can't modify the server, try replacing the offending line in the server response before passing it to the Gson parser. Something like:
String safeResponse = serverResponse.replace("\"u_owner_mi\": \"\"", "\"u_owner_mi\": null");
Your app (client) code is expecting an object according to a contract specified in the class that you pass to GSON. Your app behaves as it should and crashes loudly. You should consider having your server return "u_owner_mi" : null instead of an empty string, assuming you have control over that. The u_owner_mi field on the client side would have to be a nullable type.
If you don't have the ability to fix the api, you could also write a custom deserializer.
Suppose your result class and sub-object are:
data class Result(
val parent: String,
val owner: Any?
)
data class Owner(
val link: String,
val value: String
)
The deserializer could be:
class ResultDeserializer : JsonDeserializer<Result> {
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): Result {
val jsonObject = json.asJsonObject
val ownerProperty = jsonObject.get("owner")
return Result(
parent = jsonObject.get("parent").asString,
owner = if (ownerProperty.isJsonObject) context?.deserialize<Owner>(ownerProperty.asJsonObject, Owner::class.java)
else ownerProperty.asString
)
}
}
Finally, to add the deserializer:
#Test
fun deserialization() {
val gson = GsonBuilder().registerTypeAdapter(Result::class.java, ResultDeserializer()).create()
val result1 = gson.fromJson<Result>(jsonWithObject, Result::class.java)
val result2 = gson.fromJson<Result>(jsonWithEmpty, Result::class.java)
}

How to parse a json list of multiple type in Moshi

Got a json list mixed with plain String and Image object like this:
{
"mixList": [
"string",
{
"imageUrl": "http://...",
"height": 320,
"width": 480
}
]
}
How to parse with Moshi?
I would expect to have a List<Data>,
where StringData extends Data
and ImageData extends Data
I've solved this problem using Moshi with a CustomAdaptor. After a lot of research I wasn't able to find a better "out-of-the-box" solution. This solutions is in kotlin but can easily be ported to Java as well.
First let's define the types we are trying to parse here. I'll call the wrapper type, that contains the mixList, Response:
#JsonClass(generateAdapter = true)
data class Response(val mix_types: Data)
And the 2 different types that can be inside the list StringData and ImageData:
sealed class Data {
data class StringData(val value: String) : Data()
#JsonClass(generateAdapter = true)
data class ImageData(
val imageUrl: String,
val height: Int,
val width: Int
) : Data()
}
As I'm using Moshi code-gen, so I have annotated Response and ImageData with #JsonClass(generateAdapter = true) so that Moshi will generate the adapters for these types(and I'll leverage this in my custom adapter).
I want to provide my own custom adapter to the Data type, and I won't also want the adapter Moshi would generate for the StringData type, since this is precisely what I want to serialise/deserialise into a String, so I won't annotate these classes.
Now I'm gonna write my custom adapter like this:
class DataCustomAdapter {
#FromJson
fun fromJson(jsonReader: JsonReader, delegate: JsonAdapter<ImageData>): Data? {
return if (jsonReader.peek() == BEGIN_OBJECT) {
delegate.fromJson(jsonReader)
} else {
StringData(jsonReader.nextString())
}
}
#ToJson
fun toJson(jsonWriter: JsonWriter, data: Data, delegate: JsonAdapter<ImageData>) {
when (data) {
is ImageData -> delegate.toJson(jsonWriter, data)
is StringData -> jsonWriter.value(data.value)
}
}
}
all it's missing now is to register the custom adapter with Moshi:
private val moshi = Moshi.Builder()
.add(DataCustomAdapter())
.build()

Categories

Resources