I want my android app to read data from Google Sheets.
I have tried that and got below as Response:
{
"items": [
{
"Company": "Tata",
"Car": "Nexon",
"Model": "XZ",
"Price": 1400000,
"Range": 320,
"BatterySize": 30
},
{
"Company": "Tata",
"Car": "Nexon",
"Model": "XZ+",
"Price": 1500000,
"Range": 320,
"BatterySize": 30
}
]
}
The first thing that I want to do is get value using Key like "Company" or other.
I also want to filter and only display all items where the price is less than 1500000.
Here is what I am doing to get data from the URL
val url = URL("My_URL")
val client = OkHttpClient()
val request = okhttp3.Request.Builder()
.url(url)
.get()
.build()
val response = client.newCall(request).execute()
val responseBody = response.body!!.string()
val jsonArray = JSONObject(responseBody)
val name = jsonArray.getString("items")
println(name)
It prints:
[
{
"Company": "Tata",
"Car": "Nexon",
"Model": "XZ",
"Price": 1400000,
"Range": 320,
"BatterySize": 30
},
{
"Company": "Tata",
"Car": "Nexon",
"Model": "XZ+",
"Price": 1500000,
"Range": 320,
"BatterySize": 30
}
]
Which is not List but Sting I guess. Not able to understand what to do with this String.
Let me know if you need more information.
You might want to try kotlinx.serialization which is a part of the Kotlin language. To use it in your Android project (Groovy syntax, if you use Kotlin DSL in your Gradle files it will be slightly different):
Add this to your top-level build.gradle: classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
In your module-level build.gradle, add apply plugin: 'kotlinx-serialization' OR if you're using newer syntax, add kotlinx-serialization to your plugins {} block
To (de)serialize your data, consider adding the following helper extensions (note: this example uses the default Json; see below to get an idea of how to configure you own instance):
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
inline fun <reified T> T.toJson(): String = Json.encodeToString(this)
inline fun <reified T> String.fromJson(): T = Json.decodeFromString(this)
WARNING: using the default Json, you will get into trouble if the data actually provided by your API does not match your declared format, so you may want to configure a Json instance to use in your (de)serializer (and also assign default values to your data fields):
val json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
// more config if needed
}
This article provides a good explanation of that config.
Q: When do I need to configure Json myself?
A: If you are sure that your API will always return the data you expect, you are good to go with the default one. In all other cases it would be wise to use a custom config.
Based on you example, a single item from your list would look like that:
#Serializable
data class Item(
#SerialName("Company") val company: String,
#SerialName("Car") val car: String,
#SerialName("Model") val model: String,
#SerialName("Price") val price: Float,
#SerialName("Range") val range: Int,
#SerialName("BatterySize") val batterySize: Int
)
You also need a "holder" for your parsed items:
#Serializable
data class Items(#SerialName("items") val data: List<Item>)
Here's how you can get your data:
val url = URL("My_URL")
val client = OkHttpClient()
val request = okhttp3.Request.Builder()
.url(url)
.get()
.build()
val response = client.newCall(request).execute()
val items = response.body!!.string().fromJson<Items>().data
I also want to filter and only display all items where the price is
less than 1500000.
Sure, you can now apply any List operation to your data:
items.filter { it.price < 1500000 }.forEach { item ->
println("item $item passed the price filter")
}
A couple of things to note:
On Android, consider using Retrofit for your API calls. There is a converter factory by Jake Wharton which allows you to use kotlinx.serialization with Retrofit
avoid using not-null assertions (!!) in production code, always you the safe call (?) operator
Related
I don't understand what is problem clearly. When I searched it in google, I don't decide my reponse model is problem or the json response is problem and should change. Which one? I can't find solution for Kotlin. How I should solve this?
response JSON:
"data":{
"productInfo":{
"data":{
"toBarcode":"2704439285463",
"productJson":{
"p_no":"28420000",
"p_name":"ASA"
}
}
},
"moves":{
"data":[
{
"fisAcik":"MALVERENDEN",
"toBarcode":"2704439285463",
"toJson":{
"to_Hks_Adi":"DAĞITIM MERKEZİ"
},
"movementJson":{
"isleme_Tarihi":"21/12/2022 02:19:30"
}
}
]
}
}
Data.kt
data class Data(
val productInfo: ProductInfo,
val moves: Moves
)
data class Moves (
val data: List<MovesItem>
)
data class MovesItem (
#SerializedName("fisAcik")
val receiptExplanation: String,
val toBarcode: String,
val toJson: ToJson,
val movementJson: MovementJson
)
data class MovementJson (
#SerializedName("isleme_Tarihi")
val processDate: String
)
data class ToJson (
#SerializedName("to_Hks_Adi")
val toUnitHksName: String
)
data class ProductInfo (
val data: ProductInfoItems
)
data class ProductInfoItems (
val toBarcode: String,
val productJson: ProductJson
)
data class ProductJson (
#SerializedName("p_No")
val migrosProductNo: String,
#SerializedName("p_Name")
val migrosProductName: String
)
method that using to call request.
suspend fun dataGetInfo(#Body request: DataRequest): NetworkResult<BaseResponse<Data>>
The framework you are using for this:
...fun dataGetInfo(#Body request: DataRequest)...
is implicitly taking a JSON request and deserializing.
The annotation #SerializedName is a from the Gson library, so I guessed that your framework must be using Gson. From that I was able to test using:
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
println(Gson().fromJson(src, Data::class.java))
which produces
Data(productInfo=ProductInfo(data=ProductInfoItems(toBarcode=2704439285463, productJson=ProductJson(migrosProductNo=null, migrosProductName=null))), moves=Moves(data=[MovesItem(receiptExplanation=MALVERENDEN, toBarcode=2704439285463, toJson=ToJson(toUnitHksName=DAĞITIM MERKEZİ), movementJson=MovementJson(processDate=21/12/2022 02:19:30))]))
So fundamentally your code is ok, but I think the problem is how the source JSON is "topped and tailed". To get that parse work, I was using
val src = """
{
"productInfo": {
"data": {
"toBarcode": "2704439285463",
"productJson": {
"p_no": "28420000",
"p_name": "ASA"
}
}
},
"moves": {
"data": [
{
"fisAcik": "MALVERENDEN",
"toBarcode": "2704439285463",
"toJson": {
"to_Hks_Adi": "DAĞITIM MERKEZİ"
},
"movementJson": {
"isleme_Tarihi": "21/12/2022 02:19:30"
}
}
]
}
}
"""
Notice how I removed, from your source, "data": since what you pasted is obviously not a JSON document. I guess, therefore, that this is where the problem occurs - something to do with the top or bottom of the JSON document or you need a container object around the JSON for Data
This error was from my wrong request. I saw Ios has same error also when request with wrong value. So, for who will look this quesiton, they should understand it's not from response or kotlin. Check your value it is clearly what request need.
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'
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
)
I'm using the 1.0.0 version of kotlin serialization but I'm stuck when I try to deserialize a "flexible" array.
From the Backend API that I don't control I get back an JSON Array that holds different types of objects. How would you deserialize them using kotlin serialization?
Example
This is the API's response
[
{
"id": "test",
"person": "person",
"lastTime": "lastTime",
"expert": "pro"
},
{
"id": "test",
"person": "person",
"period": "period",
"value": 1
}
]
#Serializable
sealed class Base {
#SerialName("id")
abstract val id: String
#SerialName("person")
abstract val person: String
}
#Serializable
data class ObjectA (
#SerialName("id") override val id: String,
#SerialName("title") override val title: String,
#SerialName("lastTime") val lastTime: String,
#SerialName("expert") val expert: String
) : Base()
#Serializable
data class ObjectB (
#SerialName("id") override val id: String,
#SerialName("title") override val title: String,
#SerialName("period") val period: String,
#SerialName("value") val value: Int
) : Base()
Performing the following code result in an error
println(Json.decodeFromString<List<Base>>(json))
error Polymorphic serializer was not found for class discriminator
When you say you don't control the API, is that JSON being generated from your code by the Kotlin serialization library? Or is it something else you want to wrangle into your own types?
By default sealed classes are handled by adding a type field to the JSON, which you have in your objects, but it's a property in your Base class. In the next example it shows you how you can add a #SerialName("owned") annotation to say what type value each class corresponds to, which might help you if you can add the right one to your classes? Although in your JSON example both objects have "type" as their type...
If you can't nudge the API response into the right places, you might have to write a custom serializer (it's the deserialize part you care about) to parse things and identify what each object looks like, and construct the appropriate one.
(I don't know a huge amount about the library or anything, just trying to give you some stuff to look at, see if it helps!)
#cactustictacs solution came very close. He said that "By default sealed classes are handled by adding a type field to the JSON"
But because I didn't had a type property I needed a other field that decides which subclass it should be.
In Kotlin Serializer you can do that by
val format = Json {
classDiscriminator = "PROPERTY_THAT_DEFINES_THE_SUBCLASS"
}
val contentType = MediaType.get("application/json")
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(format.asConverterFactory(contentType))
.build()
where in classDiscriminator you can enter the property that you want. Hope this helps other people in the future.
Note: Newbie here, please let me know if i need to provide more information or clarify on anything.
To give you some context: I am practising building a Messenger-clone application with lots of Retrofit methods. For that purpose, i am using a small local JSON server, with which the application communicates.
When a user of the application creates an account, the application creates a profile object in the JSON server using the following method:
#FormUrlEncoded
#POST("profiles")
suspend fun createProfile(#Field("username") username: String?,
#Field("picture") picture: String?,
#Field(value = "nickname") nickname: String?,
#Field(value = "contacts") contacts: ArrayList<String?>,
#Field(value = "status") status: Int?): Response<Profile>
Initially, the contacts ArrayList is empty, because the user has not yet added any contacts. Creating a random profile with an empty ArrayList() for the contacts parameter, this is the result inside the JSON server:
{
"username": "username.example",
"picture": "picture's URL",
"nickname": "Nikola",
"status": 1,
"id": 4
}
The class that represents the Profile model inside the application is this:
class Profile(
val username: String? = "",
var picture: String? = "",
var nickname: String? = "",
var contacts: ArrayList<String?>? = ArrayList(),
var status: Int? = 1,
val id: Int? = 0
)
Once the profile is created, naturally the user can add new contacts, which happens using the following method:
#FormUrlEncoded
#PATCH("profiles/{id}")
suspend fun addContact(#Path("id") id: Int?,
#Field("contacts") contacts: ArrayList<String?>?): Response<Profile>
And here is where the problem occurs, on the very first contact added. The ArrayList, which is sent to server contains just one item and the result inside the JSON server looks like this:
{
"username": "username.example",
"picture": "picture's URL",
"nickname": "Nikola",
"status": 1,
"id": 4,
"contacts": "first.contact"
}
Basically, because the arraylist contains just one item, it saves it as a String. This creates all kinds of problems later on because, once the application uses a #GET method for that profile, it expects an ArrayList for the contacts attribute, but it receives a String.
What can i do to make the the JSON profile look like this:
{
"username": "username.example",
"picture": "picture's URL",
"nickname": "Nikola",
"status": 1,
"id": 4,
"contacts": ["first.contact"]
}
The contacts parameter needs to be an array, even when there is only one item in it.
Use #Body instead of #Form and #FormUrlEncoded:
data class ProfileContacts(val contacts: List<String>)
#PATCH("profiles/{id}")
suspend fun addContact(#Path("id") id: Int?, #Body contacts: ProfileContacts): Response<Profile>
and add a converter, if you haven't already had one, a Gson one for example:
// build.gradle
dependencies {
implementation 'com.squareup.retrofit2:converter-gson:2.6.1' // latest version
}
// Retrofit Builder
val retrofit = Retrofit.Builder()
... // other methods
.addConverterFactory(GsonConverterFactory.create())
.build()
#Body lets you define the request body as a Kotlin class, which will eventually get serialized using the provided Converter (in case of Gson, it will be converted to JSON). #Field on the other hand is used for sending data as application/x-www-form-urlencoded (as the required #FormUrlEncoded annotation also suggests). This means that the body of your request will be encoded into a list of key-value pairs, separated by '&', e.g. (based on the createProfile method):
username=username.example&picture=picture%27s%20URL&nickname=Nikola&status=1&id=4
You can POST an array as application/x-www-form-urlencoded by using the same key more than once. That's what basically happens when you annotate a list with the Retrofit #Field annotation - every element from the list is paired with the common key, e.g.:
#FormUrlEncoded
#PATCH("profiles/{id}")
suspend fun addContact(#Path("id") id: Int?,
#Field("contacts") contacts: ArrayList<String?>?): Response<Profile>
// ...
addContact(1, arrayListOf("first.contact", "second.contact"))
// request body:
contacts=first.contact&contacts=second.contact
So when you try to update the profile using only one element contacts list, a single "contacts" pair gets created (contacts=first.contact), and it's treated like a string value.