I am trying to parse following JSON using moshi but I am unable to dynamic data like USA or UK. USA and UK are dynamic keys.
{
"USA": {
"name": "United State of America",
"code": "US"
},
"UK": {
"name": "United Kingdom",
"code": "UK"
},
"soft_update": "500",
"hard_update": "500"
}
Data class:
data class ApiAppUpdate(val countryMap: Map<String, ApiCountry>,
#field:Json(name = "hard_update")
val forceUpdateVersion: Int,
#field:Json(name = "soft_update")
val softUpdateVersion: Int)
Following is my json adapter code:
fun getConfig(){
val adapter: JsonAdapter<ApiAppUpdate> = moshi.adapter(ApiAppUpdatee::class.java)
}
I get soft and hard update values but countryMap is always null. I am not sure what's wrong in here. Can someone please help me. Thanks.
The problem is that your model description would match this json and not the one you attached:
{
"countryMap":{
"USA":{
"name":"United State of America",
"code":"US"
},
"UK":{
"name":"United Kingdom",
"code":"UK"
}
},
"soft_update":"500",
"hard_update":"500"
}
In your example "USA" and "UK" are on the same level with "soft_update" and "hard_update".
UPDATE:
This is a working solution:
data class ApiCountry(val name: String, val code: String)
data class ApiAppUpdate(
val countryMap: Map<String, ApiCountry>,
#field:Json(name = "hard_update")
val forceUpdateVersion: Int,
#field:Json(name = "soft_update")
val softUpdateVersion: Int
)
class ApiUpdateAdapter {
#FromJson
fun fromJson(reader: JsonReader): ApiAppUpdate {
var forceUpdateVersion: Int = -1
var softUpdateVersion: Int = -1
val map: MutableMap<String, ApiCountry> = mutableMapOf()
reader.beginObject()
while (reader.hasNext()) {
when (reader.peek()) {
JsonReader.Token.NAME ->
when (val fieldName = reader.nextName()) {
"hard_update" -> forceUpdateVersion = reader.nextInt()
"soft_update" -> softUpdateVersion = reader.nextInt()
else -> {
reader.beginObject()
var name = ""
var code = ""
while (reader.hasNext()) {
when (reader.nextName()) {
"name" -> name = reader.nextString()
"code" -> code = reader.nextString()
else -> reader.skipValue()
}
}
reader.endObject()
map[fieldName] = ApiCountry(name, code)
}
}
else -> reader.skipValue()
}
}
reader.endObject()
return ApiAppUpdate(map, forceUpdateVersion, softUpdateVersion)
}
}
fun main() {
val sourceJson =
"{\"USA\":{\"name\":\"United States of America\",\"code\":\"US\"},\"UK\":{\"name\":\"United Kingdom\",\"code\":\"UK\"}" +
",\"soft_update\":\"200\",\"hard_update\":\"300\"}"
val adapter = Moshi.Builder()
.add(ApiUpdateAdapter())
.build()
.adapter(ApiAppUpdate::class.java)
val apiAppUpdate = adapter.fromJson(sourceJson)
println(apiAppUpdate)
}
Related
I want to get "file" in "media_gallery_entries" JSONArray by Volley for loop
Volley
val item = ArrayList<RecyData>()
val jsonRequest = object : JsonObjectRequest(Request.Method.GET, url, null,
Response.Listener { response ->
try {
val jsonArrayItems = response.getJSONArray("items")
val jsonSize = jsonArrayItems.length()
for (i in 0 until jsonSize) {
val jsonObjectItems = jsonArrayItems.getJSONObject(i)
val pName = jsonObjectItems.getString("name")
val pPrice = jsonObjectItems.getInt("price")
item.add(RecyData(pName, pPrice, pImage))
}
} catch (e: JSONException) {
e.printStackTrace()
}
Data
{
"items": [
{
"id": 1,
"sku": "10-1001",
"name": "item01",
"price": 100,
"media_gallery_entries": [
{
"id": 1,
"file": "//1/0/10-28117_1_1.jpg"
}
]
}
]
}
Instead of doing manual JSON parsing which is always error prone, you can use some parsing library such as Gson which is well tested and very less likely to cause any issue
To use Gson first you need to add dependency in your build.gradle as
implementation 'com.google.code.gson:gson:2.8.7'
Now define kotlin types which maps to your JSON response
class Media(
val id: Int,
val file: String
)
class Entry(
val id: Int,
val sku: String,
val name: String,
val price: Int,
val media_gallery_entries: List<Media>
)
Now in response listener just do
try{
val jsonArrayItems = response.getJSONArray("items")
val token = TypeToken.getParameterized(ArrayList::class.java, Entry::class.java).type
val result:List<Entry> = Gson().fromJson(jsonArrayItems.toString(), token)
// Do something with result
}
catch (e: JSONException) {
e.printStackTrace()
}
Try this code after val pPrice = jsonObjectItems.getInt("price") this line
val mediaEntryArr = jsonObjectItems.getJSONArray("media_gallery_entries")
for(j in 0 until mediaEntryArr.length()){
val mediaEntryObj = mediaEntryArr.getJSONObject(j)
val id = mediaEntryObj.getString("id")
val file = mediaEntryObj.getString("file")
Log.e("mediaEntry----",""+ Gson().toJson(mediaEntryObj))
Log.e("id----",""+ id)
Log.e("file----",""+ file)
}
Background
Using Moshi, I want to create generic adapter for enum that points to a class
I want to use enum type because further down the elements i have complex structure which further boils to different types.
Is it possible to serialize this way via via Moshi?
I tried to make a generic adapter that can handle any type in Attempt but so far I only have the clazz object not the actual T.
Sample Json
{
"items": [
{
"type": "A",
"apple": "123 Apples"
},
{
"type": "B",
"organge": "Banana 12",
"info": {}
},
{
"type": "C",
"grapes": "Green",
"quantity": {
"inStock": "12",
"offShelf": "12"
}
}
]
}
Class Structure
classs FruitResponse(val items: List<FruitTypes>)
#JsonClass(generateAdapter = false)
enum class FruitType(val clazz: Class<*>) {
A(Apple::class.java),
B(Banana::class.java),
C(Grapes::class.java)
}
Attempt
class FruitsAdapter<T : Enum<*>>(enumType: Class<T>) : JsonAdapter<T>() {
private val nameStrings: Array<String?>
private val nameConstantMap: MutableMap<String, T>
init {
try {
val constants = enumType.enumConstants
nameStrings = arrayOfNulls<String>(constants?.size ?: 0)
nameConstantMap = LinkedHashMap()
constants?.forEachIndexed { index, constant ->
val annotation = enumType.getField(constant.name).getAnnotation(Json::class.java)
val name = annotation?.name ?: constant.name
nameConstantMap[name] = constant
nameStrings[index] = name
}
} catch (e: NoSuchFieldException) {
throw AssertionError("Missing field in ${enumType.name}")
}
}
#Throws(IOException::class)
override fun fromJson(reader: JsonReader): T {
val name = reader.nextString()
val constant = nameConstantMap[name]
if (constant != null) return constant
throw JsonDataException(
"Expected one of ${Arrays.asList(*nameStrings)} " +
"but was $name at path ${reader.path}"
)
}
#Throws(IOException::class)
override fun toJson(writer: JsonWriter, value: T?) {
val newValue = nameConstantMap.filter { value == it.value }.map { it.key }.firstOrNull()
if (newValue != null) writer.value(newValue) else writer.nullValue()
}
}
// Usage
val moshiAdapter = Moshi.Builder()
.add(
FruitType::class.java,
FruitsAdapter(FruitType::class.java)
).build()
Take a look at PolymorphicJsonAdapterFactory which can be found in moshi-adapters artifact.
There's also moshi-sealed if you want avoid the boilerplate of manually writing a polymorphic adapter.
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
This is what my json looks like
{
"sub": "9",
"auth_time": 1559381757,
"idp": "idsrv",
"role": [
"Employer",
"Employee",
"Student"
],
"iss": "",
"aud": "",
"exp": 1574933757,
"nbf": 1559381757
}
This is the object I want to convert this Json into.
data class Claims (
#SerializedName("nameid") val nameId: String,
#SerializedName("unique_id") val uniqueId: String,
#SerializedName("sub") val sub: String,
#SerializedName("unifiedNumber") val unifiedNumber: String,
#SerializedName("role") var roleList: List<Role>
)
I wrote a custom Deserializer (which works in Java) for the List type
class RoleDeserializer : JsonDeserializer<List<Role>> {
private var roleId = 0
#Throws(JsonParseException::class)
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): MutableList<Role> {
val resultList = ArrayList<Role>()
if (json.isJsonArray) {
for (e in json.asJsonArray) {
resultList.add(Role(id = roleId++, name = e.asString))
}
} else if (json.isJsonObject) {
resultList.add(Role(id = roleId++, name = json.asString))
} else if (json.isJsonPrimitive) {
if ((json as JsonPrimitive).isString)
resultList.add(Role(id = roleId++, name = json.getAsString()))
} else {
throw RuntimeException("Unexpected JSON type: " + json.javaClass)
}
return resultList
}
}
This is how I register my type adapter
val listType: Type = object : TypeToken<List<Role>>() {}.type
val gson = GsonBuilder().registerTypeAdapter(listType, RoleDeserializer()).create()
val claims = gson.fromJson(stringJson, Claims::class.java)
I still get a parse exception stating that
java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 161 path $.role[0]
and my RoleDeserializer is never called. Am I doing something wrong while registering the type adapter?
Try to replace
val listType: Type = object : TypeToken<List<Role>>() {}.type
with
val listType: Type = object : TypeToken<MutableList<Role>>() {}.type
The role is String array in JSON
Use this
#SerializedName("role") var roleList: List<String>
Instead of this
#SerializedName("role") var roleList: List<Role>
Try this
data class Claims (
#SerializedName("nameid") val nameId: String,
#SerializedName("unique_id") val uniqueId: String,
#SerializedName("sub") val sub: String,
#SerializedName("unifiedNumber") val unifiedNumber: String,
#SerializedName("role") var roleList: List<String>
)
This is my JSON :
{
"cats": [
{
"id": "2",
"uid": "2",
"name": "English",
"date_update": "2019-04-22 15:31:00",
"numCards": 0
}
]
}
I've these two classes:
data class CatModelStr(
val cats: List<Cat>
)
data class Cat(
val date_update: String,
val id: String,
val name: String,
val numCards: Int,
val uid: String
)
I'm using MVVM and android architecture components. This is my model class for getting the data:
class CategoryModel(private val netManager: NetManager) {
var dateChanges: String = "null";
fun getCats(): MutableLiveData<MutableList<CatModelStr>> {
var list = MutableLiveData<MutableList<CatModelStr>>();
if (netManager.isConnected!!) {
list = getCatsOnline();
}
return list
}
private fun getCatsOnline(): MutableLiveData<MutableList<CatModelStr>> {
var list:MutableLiveData<MutableList<CatModelStr>> =MutableLiveData()
val getCats = ApiConnection.client.create(Category::class.java)
getCats.getCats(MyApp().uid, dateChanges)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ success ->
list += success
},{
error->
Log.v("this","ErrorGetCats "+ error.localizedMessage);
}
)
return list;
}
operator fun <T> MutableLiveData<MutableList<T>>.plusAssign(values: List<T>) {
val value = this.value ?: arrayListOf()
value.addAll(values)
this.value = value
}
I have a viewModel and activity for getting the data and it works fine. The problem is this, I want to get cat values (the json properties inside cat) out of my MutableLiveData.
This is my activity code:
vm.getCats().observe(this, Observer {
if(it!=null) {
rc_cats.visibility= View.VISIBLE
pb.visibility=View.GONE
catAdapter.reloadData(it)
}
})
The value is MutableList<CatModelStr> and I need Cat.
How can I get Cat out of MutableList?
What I am getting is that you don't need MutableList list from your response instead you need CatModelStr
you need to make the following changes from starting i guess.
class CategoryModel(private val netManager: NetManager) {
var dateChanges: String = "null";
fun getCats(): MutableLiveData<CatModelStr> {
var list = MutableLiveData<CatModelStr>();
if (netManager.isConnected!!) {
list = getCatsOnline();
}
return list
}
private fun getCatsOnline(): MutableLiveData<CatModelStr> {
var list:MutableLiveData<CatModelStr> = MutableLiveData()
val getCats = ApiConnection.client.create(Category::class.java)
getCats.getCats(MyApp().uid, dateChanges)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ success ->
list.setValue(success)
},{
error->
Log.v("this","ErrorGetCats "+ error.localizedMessage);
list.setValue(null)
}
)
return list;
}
and now if observe this you will get CatModelStr instead of MutableList and the reason is that your JSON is giving you data in format CatModelStr, you are trying to get this unnecessarily in MutableList which also won't work and you can't parse your JSON given the format.