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)
}
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 want to parse nested json using retrofit moshi. The json data i'm having is an array, inside array first element is string & second is again an array.
I don't want to parse first element in the array ("list"), just want to parse second element from the array (i.e. Inner array).
But i'm facing challenges exactly here with the data object to be use.
Obviously we can use Any type in kotlin with list, but again will loose type of the object inside the list.
Json Format:
{
"results": [
"list",
[
{
//jsonobj
},
{
//jsonobj
}
]
]
}
I want to parse json with this Data class that i have created without parsing first element & parse directly second element.
#JsonClass(generateAdapter = true)
data class ResponseModel(
#Json(name = "results")
val results: List<Results?>?,
) {
#JsonClass(generateAdapter = true)
data class Results(
#Json(name = "reference")
val reference: String?,
#Json(name = "enabled")
val enabled: Boolean,
)
}
fromJson overridden function
#Throws(IOException::class)
override fun fromJson(reader: JsonReader): C {
val result = newCollection()
reader.beginArray()
while (reader.hasNext()) {
result?.add(elementAdapter.fromJson(reader)!!)
}
reader.endArray()
return result
}
This is throwing an exception saying EXPECTED_OBJECT but it was STRING when trying to parse first element ("list").
i'm stuck here & not able to proceed further.
So can somebody plz help me to get out from here? any help will be appreciated.
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 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
I am receiving a JSON data model that has a map wrapper Table. I'm trying to use generics to pass in the type that is beyond the wrapper but it's not translating well at runtime. Here's an example of my JSON file:
{
"Table": [
{
"paymentmethod_id": 1,
"paymentmethod_description": "Cash",
"paymentmethod_code": "Cash",
"paymentmethod_is_ach_onfile": false,
"paymentmethod_is_element": false,
"paymentmethod_is_reward": false,
"paymentmethod_is_openedgeswipe": false,
"paymentmethod_update_user_id": 1,
"paymentmethod_insert_user_id": 1,
"paymentmethod_insertdate": "2014-10-07 14:53:16",
"paymentmethod_deleted": false,
"paymentmethod_is_mobile_visible": true
}
]
}
The wrapper class I'm using is called Table.
data class Table<T>(
#SerializedName("Table") val models : Array<T>
)
The actual model class is PaymentMethod.
data class PaymentMethod(
#SerializedName("paymentmethod_id") val idNumber : Int = -1
)
I have created a generic data manager class that takes < T > type. I think use subclasses of the data manager to localize the input and results (such as declaring the model class PaymentMethod.
open class NXDataManager<T>(manager: NXNetworkManager? = null, rpc : String?, parameters: List<Pair<String, String>>? = null, method : String = "get")
{
...
open fun sendRequest(completionHandler: (models:Array<T>) -> Unit, errorHandler: (error:FuelError) -> Unit) {
val request = NXNetworkRequest(rpc, parameters, method)
request.send(manager, completionHandler = { s: String ->
val table: Table<T> = Gson().fromJson(s)
completionHandler(table.models)
}, errorHandler = errorHandler)
}
inline fun <reified T> Gson.fromJson(json: String) = this.fromJson<T>(json, object: TypeToken<T>() {}.type)
}
My subclassed data manager specifies the model to parse into.
final public class PaymentMethodsDataManager : NXDataManager<PaymentMethod>
{
constructor () : super("genGetPaymentMethods")
}
When I run the code as:
val table: Table<T> = Gson().fromJson(s)
I get an error message java.lang.ClassCastException: java.lang.Object[] cannot be cast to Networking.PaymentMethod[]. However, when I pass in an explicit type it works as expected--parsing the array into PaymentMethod models:
val table: Table<PaymentMethod> = Gson().fromJson(s)
Any ideas of how I can still use the generic type T?
Data Class :
data class Table<T>(
#SerializedName("Table") val models : Array<T>
)
to JSON:
val gson = Gson()
val json = gson.toJson(table)
from JSON:
val json = getJson()
val table = gson.fromJson(json, Table::class.java)
Method fromJson is generic, so when you call it for Table<T> variable it creates Array<Any> as most suitable. You need to notice that PaymentMethod class extends T generic, but I don't know is it even possible. If you find out how to make it, use something like following:
val table: Table<T> = Gson().fromJson<Table<PaymentMethod>>(s)
In your case I'm using gson adapters. Following function creates object with specified type parameter:
fun getObjectFromString(type: Type, string: String) =
Gson().getAdapter(TypeToken.get(type)).fromJson(string)
To use it write something following:
val table: Table<T> = getObjectFromString(Table<PaymentMethod>::class.java, s) as Table<PaymentMethod>
Update
To avoid spare class cast you can use reified generic function:
inline fun <reified T> getObjectFromString(string: String): T =
getGsonConverter().getAdapter(TypeToken.get(T::class.java)).fromJson(string)!!
In that case using would be easier:
val table: Table<T> = getObjectFromString<Table<PaymentMethod>>(s)
I used first solution in cases where I don't know what type the object would be - I've got only Type variable with information about that object.
java.lang.ClassCastException: java.lang.Object[] cannot be cast to
Networking.PaymentMethod[]
Your JSON is
{
"Table": [
{
"paymentmethod_id": 1,
"paymentmethod_description": "Cash",
"paymentmethod_code": "Cash",
"paymentmethod_is_ach_onfile": false,
"paymentmethod_is_element": false,
"paymentmethod_is_reward": false,
"paymentmethod_is_openedgeswipe": false,
"paymentmethod_update_user_id": 1,
"paymentmethod_insert_user_id": 1,
"paymentmethod_insertdate": "2014-10-07 14:53:16",
"paymentmethod_deleted": false,
"paymentmethod_is_mobile_visible": true
}
]
}
Create a data class, PaymentMethod.
We frequently create classes whose main purpose is to hold data. In
such a class some standard functionality and utility functions are
often mechanically derivable from the data.
data class PaymentMethod(#SerializedName("Table") val table:ArrayList<PaymentData> )
data class PaymentData
(
#SerializedName("paymentmethod_id") val paymentmethod_id: Int,
#SerializedName("paymentmethod_description") val paymentmethod_description: String,
#SerializedName("paymentmethod_code") val paymentmethod_code:String,
#SerializedName("paymentmethod_is_ach_onfile") val paidStatus:Boolean,
#SerializedName("paymentmethod_is_element") val paymentmethod_is_element:Boolean,
#SerializedName("paymentmethod_is_reward") val paymentmethod_is_reward:Boolean,
#SerializedName("paymentmethod_is_openedgeswipe") val paymentmethod_is_openedgeswipe:Boolean,
#SerializedName("paymentmethod_update_user_id") val paymentmethod_update_user_id:Int,
#SerializedName("paymentmethod_insert_user_id") val paymentmethod_insert_user_id:Int,
#SerializedName("paymentmethod_insertdate") val paymentmethod_insertdate:String,
#SerializedName("paymentmethod_deleted") val paymentmethod_deleted:Boolean),
#SerializedName("paymentmethod_is_mobile_visible") val paymentmethod_is_mobile_visible:Boolean
)
You can call this way
val paymentDATA = Gson().fromJson<PaymentMethod>("JSON_RESPONSE", PaymentMethod::class.java)
val _adapterPaymentHistory = paymentDATA.table