Retrofit + Moshi custom adapter - android

I am struggling to understand how to convert JSON data with Moshi. I am learning Android and Kotlin and my app is supposed to load and display COVID data. The input JSON format is like this:
[
{
"infected": 109782,
"tested": "NA",
"recovered": 75243,
"deceased": 2926,
"country": "Algeria",
"moreData": "https://api.apify.com/v2/key-value-stores/pp4Wo2slUJ78ZnaAi/records/LATEST?disableRedirect=true",
"historyData": "https://api.apify.com/v2/datasets/hi0DJXpcyzDwtg2Fm/items?format=json&clean=1",
"sourceUrl": "https://www.worldometers.info/coronavirus/",
"lastUpdatedApify": "2021-02-11T12:15:00.000Z"
},
{
"infected": 425561,
"tested": 11205451,
"recovered": 407155,
"deceased": 8138,
"country": "Austria",
"moreData": "https://api.apify.com/v2/key-value-stores/RJtyHLXtCepb4aYxB/records/LATEST?disableRedirect=true",
"historyData": "https://api.apify.com/v2/datasets/EFWZ2Q5JAtC6QDSwV/items?format=json&clean=1",
"sourceUrl": "https://www.sozialministerium.at/Informationen-zum-Coronavirus/Neuartiges-Coronavirus-(2019-nCov).html",
"lastUpdatedApify": "2021-02-11T12:15:00.000Z"
},
...
]
As you can see, numbers can also be represented as strings (as in 'tested'), also some URLs can be missing for some countries.
So I followed Moshi documentation and created 2 data classes and a custom adapter.
//desired structure
data class CountryData(
val infected: Int,
val tested: Int,
val recovered: Int,
val deceased: Int,
val country: String,
val moreData: String,
val historyData: String,
val sourceUrl: String,
val lastUpdatedApify: String
)
//actual JSON
data class CountryDataJson(
val infected: String,
val tested: String,
val recovered: String,
val deceased: String,
val country: String,
val moreData: String?,
val historyData: String?,
val sourceUrl: String?,
val lastUpdatedApify: String
)
Custom adapter:
import android.util.Log
import com.example.coronastats.network.CountryData
import com.example.coronastats.network.CountryDataJson
import com.squareup.moshi.FromJson
class CountryJsonAdapter {
#FromJson fun fromJson(countryDataJson: CountryDataJson): CountryData {
val countryData = CountryData(
if (countryDataJson.infected != "NA") countryDataJson.infected.toInt() else -1,
if (countryDataJson.tested != "NA") countryDataJson.tested.toInt() else -1,
if (countryDataJson.recovered != "NA") countryDataJson.recovered.toInt() else -1,
if (countryDataJson.deceased != "NA") countryDataJson.deceased.toInt() else -1,
countryDataJson.country,
countryDataJson.moreData ?: "NA",
countryDataJson.historyData ?: "NA",
countryDataJson.sourceUrl ?: "NA",
countryDataJson.lastUpdatedApify
)
Log.d("adapterLOG", "fromJson triggered")
return countryData
}
}
And my service API:
import com.example.coronastats.CountryJsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
private const val BASE_URL = "https://api.apify.com/"
private val moshi = Moshi.Builder()
.add(CountryJsonAdapter())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
interface CoronaApiService {
#GET("v2/key-value-stores/tVaYRsPHLjNdNBu7S/records/LATEST?disableRedirect=true")
suspend fun getStatistics() : List<CountryData>
}
object CoronaApi {
val retrofitService: CoronaApiService by lazy {
retrofit.create(CoronaApiService::class.java)
}
}
And I'm getting an empty screen as a result. The Log in the adapter is never triggered, so I assume something is wrong and my adapter is never called.
NB: Without all this converter stuff, the app runs ok with the standard KotlinJsonAdapterFactory() and CountryData class as all strings, but I'd like to know how to get the structure that I have here.

I believe that CountryDataJson is missing default values.
Reading the docs I've noticed the following:
In Kotlin, these fields must have a default value if they are in the primary constructor.

Related

Handle random keys with kotlinx serialization

I am trying to serialize the content of a json string that can take the following format:
-723232569: {
"lat": 8.2,
"lon": -90.3,
"schedule": {
"friday": [
{
"date_arr": "friday",
"remarks": " OK",
"time_arr": "07:10",
"time_dep": "06:40",
"trans_name": "C"
}
]
}
However I am struggling with my current serializable class implementation. The top key (-723232569) will vary, it will be generated randomly from one iteration to another. I would like to extract they key and its value with the following class implementation.
#Serializable
data class TimeSlot(val date_arr: String,
val remarks: String,
val time_arr: String,
val time_dep: String,
val trans_link: String,
val trans_name: String,
val trans_tel: String,
val to_lat: String? = null,
val to_lon: String? = null)
#Serializable
data class Schedule(val monday: List<TimeSlot>,
val tuesday: List<TimeSlot>,
val wednesday: List<TimeSlot>,
val thursday: List<TimeSlot>,
val friday: List<TimeSlot>,
val saturday: List<TimeSlot>,
val sunday: List<TimeSlot>)
#Serializable
data class Stop(val lat: Double,
val lon: Double,
val schedule: Schedule)
However when executing the following code I am encountering
try {
val neww = """-723232569: {
"lat": 8.2,
"lon": -90.3,
"schedule": {
"friday": [
{
"date_arr": "friday",
"remarks": " OK",
"time_arr": "07:10",
"time_dep": "06:40",
"trans_name": "C"
}
]
}"""
val res = format.decodeFromString<Stop>(neww)
} catch (ioException: IOException) {
ioException.printStackTrace()
}
Unexpected JSON token at offset 27: Encountered an unknown key '-723232569'.
Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys.

Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path Kotlin Coroutines with MVVM & Retrofit

I'm using retrofit with Kotlin and coroutines using MVVM pattern. This is the first time I'm using retrofit and kotlin. My issue is I'm calling a news api and getting this error even though I've tried solving my problem on my own but didn't get any proper solution.
Json Response:
{
"status": "ok",
"totalResults": 3923,
-"articles": [
-{
-"source": {
"id": null,
"name": "Finextra"
},
"author": "Editorial Team",
"title": "Solaris Digital Assets wins Bitwala as digital asset custody partner",
"description": "Solaris Digital Assets GmbH, a 100% subsidiary of Solarisbank AG, today announced that it has won Bitwala, Germany’s crypto-banking flagship company, as a partner for its digital asset custody solution.",
"url": "https://www.finextra.com/pressarticle/85033/solaris-digital-assets-wins-bitwala-as-digital-asset-custody-partner",
"urlToImage": "https://www.finextra.com/about/finextra-logo-alt-16-9.jpg",
"publishedAt": "2020-11-17T14:28:00Z",
"content": "Solaris Digital Assets GmbH, a 100% subsidiary of Solarisbank AG, today announced that it has won Bitwala, Germanys crypto-banking flagship company, as a partner for its digital asset custody solutio… [+3321 chars]"
},
-{
-"source": {
"id": null,
"name": "Seeking Alpha"
},
"author": "Ophelia Research",
"title": "Power Corporation Of Canada Is Still A Buy",
"description": "Wealthsimple continues to grow through social media platforms and referral incentives. Power Corporation of Canada continues to grow its investments in start-ups.",
"url": "https://seekingalpha.com/article/4389643-power-corporation-of-canada-is-still-buy",
"urlToImage": "https://static2.seekingalpha.com/uploads/2020/11/15/saupload_EWZQEwLYN4dxnan8QPFcRnpuNy_nvcN-PV5mrbjb97co4v9-QgGK8ZN8UqwxzO3oSPoiDkwnvSMFsyqKGu06-S1TGHHydTAz8VkQXaY5-FjSbTa5-qzCROck4sPk2ZeSD6rYIL1P.png",
"publishedAt": "2020-11-17T14:21:26Z",
"content": "Power Corporation of Canada (OTCPK:PWCDF) is a diversified financial services company that pays out solid dividends due to strong established brands and still has the potential for growth given its i… [+6824 chars]"
}]}
Retrofit Builder:
object RetrofitBuilder {
private const val BASE_URL = "http://newsapi.org/v2/"
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build() //Doesn't require the adapter
}
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
Api Interface:
interface ApiService {
#GET("sources/apikey")
suspend fun getTopHeadlines(): Model
}
Api Helper:
suspend fun getTopHeadlines() = apiService.getTopHeadlines()
Main Repository:
suspend fun getTopHeadlines() = apiHelper.getTopHeadlines()
ViewModelFactory:
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(MainRepository(apiHelper)) as T
}
throw IllegalArgumentException("Unknown class name")
}
MainViewModel:
fun getTopHeadlines() = liveData(Dispatchers.IO) {
emit(Resource.loading(data = null))
try {
emit(Resource.success(data = mainRepository.getTopHeadlines()))
} catch (exception: Exception) {
emit(Resource.error(data = null, msg = exception.message ?: "Error Occurred!"))
}
}
Model Class:
data class Model(val status: String,val totalResults: Int,val articles: List<Article>)
Article Class:
data class Article(
val source: Source,
val author: String,
val content: String,
val description: String,
val publishedAt: String,
val title: String,
val url: String,
val urlToImage: String
)
Source Class:
data class Source(
val id: Any,
val name: String
)
Main Activity:
viewModel.getTopHeadlines().observe(this, Observer {
it?.let { resource ->
when (resource.status) {
Status.SUCCESS -> {
Log.e("MainClass","Data caught: "+it.message);
// resource.data?.let { users -> retrieveList(users) }
}
Status.ERROR -> {
Log.e("MainClass","Exception caught: "+it.message);
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
}
Status.LOADING -> {
}
}
}
})
Alright. Judging by this documentation and by your code, there are plenty things wrong here, so I will go into more detail below.
Your Model class that you created:
Here is the POJO structure that I generated for the Json you provided..
Something like this:
data class Base(
#SerializedName("status") val status: String,
#SerializedName("totalResults") val totalResults: Int,
#SerializedName("articles") val articles: List<Articles>
)
data class Articles(
#SerializedName("source") val source: Source,
#SerializedName("author") val author: String,
#SerializedName("title") val title: String,
#SerializedName("description") val description: String,
#SerializedName("url") val url: String,
#SerializedName("urlToImage") val urlToImage: String,
#SerializedName("publishedAt") val publishedAt: String,
#SerializedName("content") val content: String
)
data class Source(
#SerializedName("id") val id: String,
#SerializedName("name") val name: String
)
Model class is done. Moving on to your Retrofit code.
Your Retrofit instance:
#GET(sources/apikey) line is completely wrong. Nothing like that exists in documentation and you won't be able to get anything out of it. In order to get what you need, you need to reference top-headlines or sources or everything.
Your object RetrofitBuilder can be simplified in some ways.
You need to query ApiKey together with your request which you are not doing
You need to query Country together with your request which you are not doing as well
Lets apply the changes then. (I am using top-headlines here since it fits our model class):
// This is much simplified version of what you have written.
interface NewsApi {
// Here we use correct API endpoint.
#GET("top-headlines")
suspend fun getTopHeadlines(
// This is how you Query necessary parameters for APIs
// In our case, we need to query Country + apiKey
#Query("country") country: String = "us",
#Query("apiKey") apiKey: String = "%InsertYourApiKey%"
): Response<Base> // Response<Base> is simply a class that allows you to read API's response codes, body and other details that you might need for processing response information.
companion object {
fun getInstance(): NewsApi {
return Retrofit.Builder()
.baseUrl("http://newsapi.org/v2/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NewsApi::class.java) // Creation doesn't have to be separate, you can have it in here for more concise code.
}
}
}
After these changes, all you have to do:
Insert your API key into interface where I have written %InsertYourApiKey
Use the following code anywhere you need:
...
val newsApi = NewsApi.getInstance()
val response = newsApi.getTopHeadlines()
// Do whatever you need with your response.

android-kotlin JsonObject JsonArray send request data via POST

I want to send data request via post to server I want to know How can I add data in array
data class City(
#SerializedName("cityId")
val cityId: Int?,
#SerializedName("detail")
val detail: List<String?>
)
Request
data class CityRequest(
#SerializedName("listCity")
val listCity: List<City?>
)
Response
data class CityResponse(
#SerializedName("code")
val code: String?,
#SerializedName("status")
val status: Boolean?,
#SerializedName("message")
val message: String?
)
API Server
#Headers("Content-Type: application/json")
#POST("city")
suspend fun sendCityContent(#Body listCity: CityRequest?):
Call<CityResponse?>
Connect Service
I don't know how I can add information to this section in question.
private suspend fun sendDataCity(city: List<city?>) {
val retrofit = clientCity
val sendDataToServer = retrofit?.create(CityService::class.java)
val call = sendDataToServer?.sendCityContent(CityRequest(city))
call?.enqueue(object : Callback<CityResponse?> {
override fun onResponse(
call: Call<CityResponse?>, response: Response<CityResponse?>) {
val getResponse = response.body()
Timber.tag("SALE_CITY: ").d("code: %s", getResponse?.code)
Timber.tag("SALE_CITY: ").d("status: %s", getResponse?.status)
Timber.tag("SALE_CITY: ").d("message: %s", getResponse?.message)
}
override fun onFailure(call: Call<CityResponse?>, t: Throwable?) {
t?.printStackTrace()
}
})
}
JSON Simple
{
"city": [
{
"cityId": 1,
"answer": [
"1"
]
},
{
"questionId": 2,
"answer": [
"2.1",
"2.2"
]
}
]}
What do I have to do next?
Can you have a sample add data in array for me?
Things I want
cityId = 1
detail = "1.1", "1.2"
cityId = 2
detail = "2.1", "2.2"
thank you
One issue i can see with your request is the key is different from what you are sending might be different check that. it should be city not listCity as given.
data class CityRequest(
#SerializedName("city")
val city: List<City?>
)
and your city class should have these keys answer which you have mentioned as details
data class City(
#SerializedName("cityId")
val cityId: Int?,
#SerializedName("answer")
val answer: List<String?>
)
I guess you are just sending with wrong keys that might be the reason the server is not accepting the request. make the above change it should work post if you get error.

Consuming Polymorphic Jsons with Retrofit and Kotlin

My API sends me a polyphonic Json in with the variable addon_item can be either a String or an Array, I have spend days trying to make a CustomDezerializer for it without any success.
Here is the Json response:
({
"code": 1,
"msg": "OK",
"details": {
"merchant_id": "62",
"item_id": "1665",
"item_name": "Burrito",
"item_description": "Delicioso Burrito en base de tortilla de 30 cm",
"discount": "",
"photo": "http:\/\/www.asiderapido.cloud\/upload\/1568249379-KDKQ5789.jpg",
"item_cant": "-1",
"cooking_ref": false,
"cooking_ref_trans": "",
"addon_item": [{
"subcat_id": "144",
"subcat_name": "EXTRA",
"subcat_name_trans": "",
"multi_option": "multiple",
"multi_option_val": "",
"two_flavor_position": "",
"require_addons": "",
"sub_item": [{
"sub_item_id": "697",
"sub_item_name": "Queso cheddar",
"item_description": "Delicioso queso fundido",
"price": "36331.20",
"price_usd": null
}]
}]
}
})
Here is the Custom Dezerializer, which includes BodyConverter that removes two braces that encompassed the Json response:
'''
/**
* This class was created due to 2 issues with the current API responses:
* 1. The API JSON results where encapsulated by parenthesis
* 2. They had dynamic JSON variables, where the Details variable was coming as a String
* or as an Object depending on the error message (werer whe user and password wereh correct.
*
*/
class JsonConverter(private val gson: Gson) : Converter.Factory() {
override fun responseBodyConverter(
type: Type?, annotations: Array<Annotation>?,
retrofit: Retrofit?
): Converter<ResponseBody, *>? {
val adapter = gson.getAdapter(TypeToken.get(type!!))
return GsonResponseBodyConverter(gson, adapter)
}
override fun requestBodyConverter(
type: Type?,
parameterAnnotations: Array<Annotation>?,
methodAnnotations: Array<Annotation>?,
retrofit: Retrofit?
): Converter<*, RequestBody>? {
val adapter = gson.getAdapter(TypeToken.get(type!!))
return GsonRequestBodyConverter(gson, adapter)
}
internal inner class GsonRequestBodyConverter<T>(
private val gson: Gson,
private val adapter: TypeAdapter<T>
) : Converter<T, RequestBody> {
private val MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8")
private val UTF_8 = Charset.forName("UTF-8")
#Throws(IOException::class)
override fun convert(value: T): RequestBody {
val buffer = Buffer()
val writer = OutputStreamWriter(buffer.outputStream(), UTF_8)
val jsonWriter = gson.newJsonWriter(writer)
adapter.write(jsonWriter, value)
jsonWriter.close()
return RequestBody.create(MEDIA_TYPE, buffer.readByteString())
}
}
// Here we remove the parenthesis from the JSON response
internal inner class GsonResponseBodyConverter<T>(
gson: Gson,
private val adapter: TypeAdapter<T>
) : Converter<ResponseBody, T> {
#Throws(IOException::class)
override fun convert(value: ResponseBody): T? {
val dirty = value.string()
val clean = dirty.replace("(", "")
.replace(")", "")
try {
return adapter.fromJson(clean)
} finally {
value.close()
}
}
}
class DetalleDeProductoDeserializer : JsonDeserializer<DetallesDelItemWrapper2> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): DetallesDelItemWrapper2 {
if ((json as JsonObject).get("addon_item") is JsonObject) {
return Gson().fromJson<DetallesDelItemWrapper2>(json, ListaDetalleAddonItem::class.java)
} else {
return Gson().fromJson<DetallesDelItemWrapper2>(json, DetallesDelItemWrapper2.CookingRefItemBoolean::class.java)
}
}
}
companion object {
private val LOG_TAG = JsonConverter::class.java!!.getSimpleName()
fun create(detalleDeProductoDeserializer: DetalleDeProductoDeserializer): JsonConverter {
Log.e("Perfill Adapter = ", "Test5 " + "JsonConverter" )
return create(Gson())
}
fun create(): JsonConverter {
return create(Gson())
}
private fun create(gson: Gson?): JsonConverter {
if (gson == null) throw NullPointerException("gson == null")
return JsonConverter(gson)
}
}
}
Here is the RetrofitClient.class:
class RetrofitClient private constructor(name: String) {
private var retrofit: Retrofit? = null
fun getApi(): Api {
return retrofit!!.create(Api::class.java)
}
init {
if (name == "detalleDelItem") run {
retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(JsonConverterJava.create(JsonConverterJava.DetallesDelItemDeserializer()))
// .addConverterFactory(GsonConverterFactory.create(percentDeserializer))
.client(unsafeOkHttpClient.build())
.build()
Log.e("RetrofitClient ", "Instace: " + "detalle " + name)
}
}
companion object {
//Remember this shit is https for the production server
private val BASE_URL = "http://www.asiderapido.cloud/mobileapp/api/"
private var mInstance: RetrofitClient? = null
#Synchronized
fun getInstance(name: String): RetrofitClient {
mInstance = RetrofitClient(name)
return mInstance!!
}
}
}
Finally my POJO:
open class DetallesDelItemWrapper2 {
#SerializedName("code")
val code: Int? = null
#Expose
#SerializedName("details")
var details: ItemDetails? = null
#SerializedName("msg")
val msg: String? = null
class ItemDetails {
#Expose
#SerializedName("addon_item")
val addonItem: Any? = null
#SerializedName("category_info")
val categoryInfo: CategoryInfo? = null
#SerializedName("cooking_ref")
val cookingRef: Any? = null
#SerializedName("cooking_ref_trans")
val cookingRefTrans: String? = null
}
class ListaDetalleAddonItem: DetallesDelItemWrapper2(){
#SerializedName("addon_item")
val detalleAddonItem: List<DetalleAddonItem>? = null
}
class StringDetalleAddonItem: DetallesDelItemWrapper2(){
#SerializedName("addon_item")
val detalleAddonItem: String? = null
}
I took a shot at this and came up with 2 possible ideas. I don't think they're the only way to achieve this, but I think I can share my thoughts.
First, I've reduced the problem to actually only parsing the items. So I've removed retrofit from the equation and use the following jsons:
val json = """{
"addon_item": [{
"subcat_id": "144",
"subcat_name": "EXTRA",
"subcat_name_trans": "",
"multi_option": "multiple",
"multi_option_val": "",
"two_flavor_position": "",
"require_addons": "",
"sub_item": [{
"sub_item_id": "697",
"sub_item_name": "Queso cheddar",
"item_description": "Delicioso queso fundido",
"price": "36331.20",
"price_usd": null
}]
}]
}
""".trimIndent()
(for when the addon_item is an array)
val jsonString = """{
"addon_item": "foo"
}
""".trimIndent()
(for when the addon_item is a string)
First approach
My first approach was to model addon_item as a generic JsonElement:
data class ItemDetails(
#Expose
#SerializedName("addon_item")
val addonItem: JsonElement? = null
)
(I'm using data classes because I find them more helpful, but you don't have too)
The idea here is to let gson deserialize it as a generic json element and you can then inspect it yourself. So if we add some convenience methods to the class:
data class ItemDetails(
#Expose
#SerializedName("addon_item")
val addonItem: JsonElement? = null
) {
fun isAddOnItemString() =
addonItem?.isJsonPrimitive == true && addonItem.asJsonPrimitive.isString
fun isAddOnItemArray() =
addonItem?.isJsonArray == true
fun addOnItemAsString() =
addonItem?.asString
fun addOnItemAsArray() =
addonItem?.asJsonArray
}
So as you can see, we check the addOnItem for what it contains and according to that, we can obtain its contents. Here's an example of how to use it:
fun main() {
val item = Gson().fromJson(jsonString, ItemDetails::class.java)
println(item.isAddOnItemArray())
println(item.isAddOnItemString())
println(item.addOnItemAsString())
}
I think the biggest advantage of this is that it's fairly simple and you don't require custom logic to deserialize. For me, the huge drawback is the type-safety loss.
You can get the add on as an array, but it will be an array of json elements that have to be "manually" deserialized. Hence, my 2nd approach tries to tackle this.
Second approach
The idea here is to use Kotlin's sealed classes and have 2 types of add ons:
sealed class AddOnItems {
data class StringAddOnItems(
val addOn: String
) : AddOnItems()
data class ArrayAddOnItems(
val addOns: List<SubCategory> = emptyList()
) : AddOnItems()
fun isArray() = this is ArrayAddOnItems
fun isString() = this is StringAddOnItems
}
The SubCategory class is just what was inside the list. Here's a simple version of it:
data class SubCategory(
#SerializedName("subcat_id")
val id: String
)
As you can see the AddOnItems is a sealed class that has the only 2 possible types for your use case.
Now we need a custom deserializer:
class AddOnItemsDeserializer : JsonDeserializer<AddOnItems> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?) =
when {
json?.isJsonArray == true -> {
AddOnItems.ArrayAddOnItems(context!!.deserialize(
json.asJsonArray,
TypeToken.getParameterized(List::class.java, SubCategory::class.java).type))
}
json?.isJsonPrimitive == true && json.asJsonPrimitive.isString ->
AddOnItems.StringAddOnItems(json.asJsonPrimitive.asString)
else -> throw IllegalStateException("Cannot parse $json as addonItems")
}
}
In a nutshell, this checks if add on is an array and creates the respective class and the same for string.
Here's how you can use it:
fun main() {
val item = GsonBuilder()
.registerTypeAdapter(AddOnItems::class.java, AddOnItemsDeserializer())
.create()
.fromJson(jsonString, ItemDetails::class.java)
println(item.addOnItems.isString())
println(item.addOnItemsAsString().addOn)
val item = GsonBuilder()
.registerTypeAdapter(AddOnItems::class.java, AddOnItemsDeserializer())
.create()
.fromJson(json, ItemDetails::class.java)
println(item.addOnItems.isArray())
println(item.addOnItemsAsArray().addOns[0])
}
I think the biggest advantage here is that you get to keep the types. However, you still need to check what it is before calling addOnItemsAs*.
Hope this helps

How to parse JSON indexed dictionnary in Kotlin [duplicate]

I'm receiving a quite deep JSON object string from a service which I must parse to a JSON object and then map it to classes.
How can I transform a JSON string to object in Kotlin?
After that the mapping to the respective classes, I was using StdDeserializer from Jackson. The problem arises at the moment the object had properties that also had to be deserialized into classes. I was not able to get the object mapper, at least I didn't know how, inside another deserializer.
Preferably, natively, I'm trying to reduce the number of dependencies I need so if the answer is only for JSON manipulation and parsing it'd be enough.
There is no question that the future of parsing in Kotlin will be with kotlinx.serialization. It is part of Kotlin libraries. Version kotlinx.serialization 1.0 is finally released
https://github.com/Kotlin/kotlinx.serialization
import kotlinx.serialization.*
import kotlinx.serialization.json.JSON
#Serializable
data class MyModel(val a: Int, #Optional val b: String = "42")
fun main(args: Array<String>) {
// serializing objects
val jsonData = JSON.stringify(MyModel.serializer(), MyModel(42))
println(jsonData) // {"a": 42, "b": "42"}
// serializing lists
val jsonList = JSON.stringify(MyModel.serializer().list, listOf(MyModel(42)))
println(jsonList) // [{"a": 42, "b": "42"}]
// parsing data back
val obj = JSON.parse(MyModel.serializer(), """{"a":42}""")
println(obj) // MyModel(a=42, b="42")
}
You can use this library https://github.com/cbeust/klaxon
Klaxon is a lightweight library to parse JSON in Kotlin.
Without external library (on Android)
To parse this:
val jsonString = """
{
"type":"Foo",
"data":[
{
"id":1,
"title":"Hello"
},
{
"id":2,
"title":"World"
}
]
}
"""
Use these classes:
import org.json.JSONObject
class Response(json: String) : JSONObject(json) {
val type: String? = this.optString("type")
val data = this.optJSONArray("data")
?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } // returns an array of JSONObject
?.map { Foo(it.toString()) } // transforms each JSONObject of the array into Foo
}
class Foo(json: String) : JSONObject(json) {
val id = this.optInt("id")
val title: String? = this.optString("title")
}
Usage:
val foos = Response(jsonString)
You can use Gson .
Example
Step 1
Add compile
compile 'com.google.code.gson:gson:2.8.2'
Step 2
Convert json to Kotlin Bean(use JsonToKotlinClass)
Like this
Json data
{
"timestamp": "2018-02-13 15:45:45",
"code": "OK",
"message": "user info",
"path": "/user/info",
"data": {
"userId": 8,
"avatar": "/uploads/image/20180115/1516009286213053126.jpeg",
"nickname": "",
"gender": 0,
"birthday": 1525968000000,
"age": 0,
"province": "",
"city": "",
"district": "",
"workStatus": "Student",
"userType": 0
},
"errorDetail": null
}
Kotlin Bean
class MineUserEntity {
data class MineUserInfo(
val timestamp: String,
val code: String,
val message: String,
val path: String,
val data: Data,
val errorDetail: Any
)
data class Data(
val userId: Int,
val avatar: String,
val nickname: String,
val gender: Int,
val birthday: Long,
val age: Int,
val province: String,
val city: String,
val district: String,
val workStatus: String,
val userType: Int
)
}
Step 3
Use Gson
var gson = Gson()
var mMineUserEntity = gson?.fromJson(response, MineUserEntity.MineUserInfo::class.java)
Not sure if this is what you need but this is how I did it.
Using import org.json.JSONObject :
val jsonObj = JSONObject(json.substring(json.indexOf("{"), json.lastIndexOf("}") + 1))
val foodJson = jsonObj.getJSONArray("Foods")
for (i in 0..foodJson!!.length() - 1) {
val categories = FoodCategoryObject()
val name = foodJson.getJSONObject(i).getString("FoodName")
categories.name = name
}
Here's a sample of the json :
{"Foods": [{"FoodName": "Apples","Weight": "110" } ]}
I personally use the Jackson module for Kotlin that you can find here: jackson-module-kotlin.
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$version"
As an example, here is the code to parse the JSON of the Path of Exile skilltree which is quite heavy (84k lines when formatted) :
Kotlin code:
package util
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.*
import java.io.File
data class SkillTreeData( val characterData: Map<String, CharacterData>, val groups: Map<String, Group>, val root: Root,
val nodes: List<Node>, val extraImages: Map<String, ExtraImage>, val min_x: Double,
val min_y: Double, val max_x: Double, val max_y: Double,
val assets: Map<String, Map<String, String>>, val constants: Constants, val imageRoot: String,
val skillSprites: SkillSprites, val imageZoomLevels: List<Int> )
data class CharacterData( val base_str: Int, val base_dex: Int, val base_int: Int )
data class Group( val x: Double, val y: Double, val oo: Map<String, Boolean>?, val n: List<Int> )
data class Root( val g: Int, val o: Int, val oidx: Int, val sa: Int, val da: Int, val ia: Int, val out: List<Int> )
data class Node( val id: Int, val icon: String, val ks: Boolean, val not: Boolean, val dn: String, val m: Boolean,
val isJewelSocket: Boolean, val isMultipleChoice: Boolean, val isMultipleChoiceOption: Boolean,
val passivePointsGranted: Int, val flavourText: List<String>?, val ascendancyName: String?,
val isAscendancyStart: Boolean?, val reminderText: List<String>?, val spc: List<Int>, val sd: List<String>,
val g: Int, val o: Int, val oidx: Int, val sa: Int, val da: Int, val ia: Int, val out: List<Int> )
data class ExtraImage( val x: Double, val y: Double, val image: String )
data class Constants( val classes: Map<String, Int>, val characterAttributes: Map<String, Int>,
val PSSCentreInnerRadius: Int )
data class SubSpriteCoords( val x: Int, val y: Int, val w: Int, val h: Int )
data class Sprite( val filename: String, val coords: Map<String, SubSpriteCoords> )
data class SkillSprites( val normalActive: List<Sprite>, val notableActive: List<Sprite>,
val keystoneActive: List<Sprite>, val normalInactive: List<Sprite>,
val notableInactive: List<Sprite>, val keystoneInactive: List<Sprite>,
val mastery: List<Sprite> )
private fun convert( jsonFile: File ) {
val mapper = jacksonObjectMapper()
mapper.configure( DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true )
val skillTreeData = mapper.readValue<SkillTreeData>( jsonFile )
println("Conversion finished !")
}
fun main( args : Array<String> ) {
val jsonFile: File = File( """rawSkilltree.json""" )
convert( jsonFile )
JSON (not-formatted): http://filebin.ca/3B3reNQf3KXJ/rawSkilltree.json
Given your description, I believe it matches your needs.
GSON is a good choice for Android and Web platform to parse JSON in a Kotlin project. This library is developed by Google.
https://github.com/google/gson
1. First, add GSON to your project:
dependencies {
implementation 'com.google.code.gson:gson:2.8.9'
}
2. Now you need to convert your JSON to Kotlin Data class:
Copy your JSON and go to this(https://json2kt.com) website and paste your JSON to Input Json box. Write package(ex: com.example.appName) and Class name(ex: UserData) in proper box. This site will show live preview of your data class below and also you can download all classes at once in a zip file.
After downloading all classes extract the zip file & place them into your project.
3. Now Parse like below:
val myJson = """
{
"user_name": "john123",
"email": "john#example.com",
"name": "John Doe"
}
""".trimIndent()
val gson = Gson()
var mUser = gson.fromJson(myJson, UserData::class.java)
println(mUser.userName)
Done :)
This uses kotlinx.serialization like Elisha's answer. Meanwhile the project is past version 1.0 so the API has changed. Note that e.g. JSON.parse was renamed to Json.decodeFromString. Also it is imported in gradle differently starting in Kotlin 1.4.0:
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0"
}
apply plugin: 'kotlinx-serialization'
Example usage:
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
#Serializable
data class Point(val x: Int, val y: Int)
val pt = Json.decodeFromString<Point>("""{"y": 1, "x": 2}""")
val str = Json.encodeToString(pt) // type can be inferred!
val ilist = Json.decodeFromString<List<Int>>("[-1, -2]")
val ptlist = Json.decodeFromString<List<Point>>(
"""[{"x": 3, "y": 4}, {"x": 5, "y": 6}]"""
)
You can use nullable types (T?) for both nullable and optional fields:
#Serializable
data class Point2(val x: Int, val y: Int? = null)
val nlist = Json.decodeFromString<List<Point2>>(
"""[{"x": 7}, {"x": 8, "y": null}, {"x": 9, "y": 0}]"""
)
Kotlin's data class is a class that mainly holds data and has members, .toString() and other methods (e.g. destructuring declarations) automatically defined.
To convert JSON to Kotlin use http://www.json2kotlin.com/
Also you can use Android Studio plugin. File > Settings, select Plugins in left tree, press "Browse repositories...", search "JsonToKotlinClass", select it and click green button "Install".
After AS restart you can use it. You can create a class with File > New > JSON To Kotlin Class (JsonToKotlinClass). Another way is to press Alt + K.
Then you will see a dialog to paste JSON.
In 2018 I had to add package com.my.package_name at the beginning of a class.
First of all.
You can use JSON to Kotlin Data class converter plugin in Android Studio for JSON mapping to POJO classes (kotlin data class).
This plugin will annotate your Kotlin data class according to JSON.
Then you can use GSON converter to convert JSON to Kotlin.
Follow this Complete tutorial:
Kotlin Android JSON Parsing Tutorial
If you want to parse json manually.
val **sampleJson** = """
[
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio
reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita"
}]
"""
Code to Parse above JSON Array and its object at index 0.
var jsonArray = JSONArray(sampleJson)
for (jsonIndex in 0..(jsonArray.length() - 1)) {
Log.d("JSON", jsonArray.getJSONObject(jsonIndex).getString("title"))
}
Kotlin Serialization
Kotlin specific library by JetBrains for all supported platforms – Android, JVM, JavaScript, Native.
https://github.com/Kotlin/kotlinx.serialization
Moshi
Moshi is a JSON library for Android and Java by Square.
https://github.com/square/moshi
Jackson
https://github.com/FasterXML/jackson
Gson
Most popular but almost deprecated.
https://github.com/google/gson
JSON to Java
http://www.jsonschema2pojo.org/
JSON to Kotlin
IntelliJ plugin - https://plugins.jetbrains.com/plugin/9960-json-to-kotlin-class-jsontokotlinclass-
Parse JSON string to Kotlin object
As others recommend, Gson library is the simplest way!
If the File is in the Asset folder you can do like this, first add
dependencies {
implementation 'com.google.code.gson:gson:2.9.0'
}
then get a file from Asset:
jsonString = context.assets.open(fileName).bufferedReader().use { it.readText() }
then use Gson :
val gson = Gson()
val listPersonType = object : TypeToken<List<Person>>() {}.type
var persons: List<Person> = gson.fromJson(jsonFileString, listPersonType)
persons.forEachIndexed { idx, person -> Log.i("data", "> Item $idx:\n$person") }
Where Person is a Model/Data class, like this
data class Person(val name: String, val age: Int, val messages: List) {
}
If you prefer parsing JSON to JavaScript-like constructs making use of Kotlin syntax, I recommend JSONKraken, of which I am the author.
You can do things like:
val json: JsonValue = JsonKraken.deserialize("""{"getting":{"started":"Hello World"}}""")
println(JsonKraken.serialize(json)) //prints: {"getting":{"started":"Hello World"}}
println(json["getting"]["started"].cast<String>()) //prints: Hello World
Suggestions and opinions on the matter are much apreciated!
I created a simple Extention function to convert JSON string to model class
inline fun <reified T: Any> String.toKotlinObject(): T =
Gson().fromJson(this, T::class.java)
Usage method
stringJson.toKotlinObject<MyModelClass>()
http://www.jsonschema2pojo.org/
Hi you can use this website to convert json to pojo.
control+Alt+shift+k
After that you can manualy convert that model class to kotlin model class. with the help of above shortcut.
Seems like Kotlin does not have any built-in method as in many cases it just imports and implements some tools from Java. After trying lots of packages, finally this one worked reasonably. This fastjson from alibaba, which is very easy to use. Inside build gradle dependencies:
implementation 'com.alibaba:fastjson:1.1.67.android'
Inside your Kotlin code:
import com.alibaba.fastjson.JSON
var jsonDecodedMap: Map<String, String> =
JSON.parse(yourStringValueHere) as Map<String, String>;
Download the source of deme from here(Json parsing in android kotlin)
Add this dependency:
compile 'com.squareup.okhttp3:okhttp:3.8.1'
Call api function:
fun run(url: String) {
dialog.show()
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
dialog.dismiss()
}
override fun onResponse(call: Call, response: Response) {
var str_response = response.body()!!.string()
val json_contact:JSONObject = JSONObject(str_response)
var jsonarray_contacts:JSONArray= json_contact.getJSONArray("contacts")
var i:Int = 0
var size:Int = jsonarray_contacts.length()
al_details= ArrayList();
for (i in 0.. size-1) {
var json_objectdetail:JSONObject=jsonarray_contacts.getJSONObject(i)
var model:Model= Model();
model.id=json_objectdetail.getString("id")
model.name=json_objectdetail.getString("name")
model.email=json_objectdetail.getString("email")
model.address=json_objectdetail.getString("address")
model.gender=json_objectdetail.getString("gender")
al_details.add(model)
}
runOnUiThread {
//stuff that updates ui
val obj_adapter : CustomAdapter
obj_adapter = CustomAdapter(applicationContext,al_details)
lv_details.adapter=obj_adapter
}
dialog.dismiss()
}
})

Categories

Resources