Retrofit and Moshi: Parsing a JSON Object with Two Arrays - android
In the process of learning how to use Retrofit with Moshi to use APIs with Android, I have run into an issue I cannot get my head around. The goal here is to get a simple array of categories returned from an API. When I make the call to, in this case, the Behance API to list all creativefields, an array is not returned. Instead is is an object with two arrays:
{"fields":[{"id":108,"name":"Advertising"},{"id":3,"name":"Animation"},{"id":4,"name":"Architecture"},{"id":5,"name":"Art Direction"},{"id":130,"name":"Automotive Design"},{"id":109,"name":"Branding"},{"id":133,"name":"Calligraphy"},{"id":9,"name":"Cartooning"},{"id":124,"name":"Character Design"},{"id":12,"name":"Cinematography"},{"id":15,"name":"Computer Animation"},{"id":19,"name":"Copywriting"},{"id":20,"name":"Costume Design"},{"id":21,"name":"Crafts"},{"id":137,"name":"Creative Direction"},{"id":23,"name":"Culinary Arts"},{"id":122,"name":"Digital Art"},{"id":27,"name":"Digital Photography"},{"id":28,"name":"Directing"},{"id":110,"name":"Drawing"},{"id":31,"name":"Editing"},{"id":32,"name":"Editorial Design"},{"id":33,"name":"Engineering"},{"id":35,"name":"Entrepreneurship"},{"id":36,"name":"Exhibition Design"},{"id":37,"name":"Fashion"},{"id":93,"name":"Fashion Styling"},{"id":38,"name":"Film"},{"id":112,"name":"Fine Arts"},{"id":40,"name":"Furniture Design"},{"id":41,"name":"Game Design"},{"id":43,"name":"Graffiti"},{"id":44,"name":"Graphic Design"},{"id":131,"name":"Icon Design"},{"id":48,"name":"Illustration"},{"id":49,"name":"Industrial Design"},{"id":50,"name":"Information Architecture"},{"id":51,"name":"Interaction Design"},{"id":52,"name":"Interior Design"},{"id":53,"name":"Jewelry Design"},{"id":54,"name":"Journalism"},{"id":55,"name":"Landscape Design"},{"id":59,"name":"MakeUp Arts (MUA)"},{"id":63,"name":"Motion Graphics"},{"id":64,"name":"Music"},{"id":66,"name":"Packaging"},{"id":67,"name":"Painting"},{"id":69,"name":"Pattern Design"},{"id":70,"name":"Performing Arts"},{"id":73,"name":"Photography"},{"id":74,"name":"Photojournalism"},{"id":78,"name":"Print Design"},{"id":79,"name":"Product Design"},{"id":123,"name":"Programming"},{"id":136,"name":"Retouching"},{"id":86,"name":"Sculpting"},{"id":87,"name":"Set Design"},{"id":118,"name":"Sound Design"},{"id":91,"name":"Storyboarding"},{"id":135,"name":"Street Art"},{"id":95,"name":"Textile Design"},{"id":126,"name":"Toy Design"},{"id":97,"name":"Typography"},{"id":132,"name":"UI\/UX"},{"id":120,"name":"Visual Effects"},{"id":102,"name":"Web Design"},{"id":103,"name":"Web Development"},{"id":105,"name":"Writing"}],
"popular":[{"id":44,"name":"Graphic Design"},{"id":73,"name":"Photography"},{"id":51,"name":"Interaction Design"},{"id":5,"name":"Art Direction"},{"id":48,"name":"Illustration"},{"id":49,"name":"Industrial Design"},{"id":63,"name":"Motion Graphics"},{"id":37,"name":"Fashion"},{"id":4,"name":"Architecture"},{"id":109,"name":"Branding"},{"id":102,"name":"Web Design"},{"id":132,"name":"UI\/UX"}],"http_code":200}
How do I parse this JSON response to get two arrays of creative fields using Moshi and Retrofit? Below is the setup I had anticipated would work. Now I am aware that the JSON is not a List but more of a FieldList with 2 values of "fields" and "popular", but I can't see how to extract the arrays with Moshi.
Model of a Creative Field
data class Fields(val id: Int, val name: String)
Interface/Service
interface BehanceService{
#GET( "v2/fields")
fun creativeField(#Query("api_key") api_key: String): Call<List<Fields>>
}
The API class
object BehanceAPI {
private val BASE_URL = "http://www.behance.net/"
val retrofittedBuilder: Retrofit by lazy {
Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
val behanceService: BehanceService = retrofittedBuilder.create(BehanceService::class.java)
}
this is how your json looks like as Java Model
data class Response(
val httpCode: Int? = null,
val fields: List<FieldsItem?>? = null,
val popular: List<PopularItem?>? = null)
data class FieldsItem(
val name: String? = null,
val id: Int? = null)
data class PopularItem(
val name: String? = null,
val id: Int? = null)
Your service will be something like this:
interface BehanceService{
#GET("v2/fields")
fun creativeField(#Query("api_key") api_key: String): Call<Response>
}
And your Api class will be something like this:
object BehanceAPI {
private val BASE_URL = "http://www.behance.net/"
val retrofittedBuilder: Retrofit by lazy {
Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
val behanceService: BehanceService = retrofittedBuilder.create(BehanceService::class.java)}
you can call it in this way.
BehanceAPI.behanceService.creativeField("your_key_here").enqueue(new Call<Response>(){
#Override
public void onResponse( response: Call<Response>)
{
// Deal with the response here
val data = response.body();
}
#Override
public void onFailure(Throwable t)
{
// Deal with the error here
}})
Related
Formatting the Nested JSON response from retrofit API in MVVM Architecture - Kotlin
I m new to kotlin and MVVM, I have been working around this issue for a week now, couldn't get any idea even after searching for some code on the internet. I'm trying to edit or modify the retrofit response (to observe a specific type; say "sf") according to my need and neglecting other data which is not needed. I'm using mutable livedata to fetch and update the JSON data from the retrofit response to the recylerview. Here is the link for the JSON data: http://www.nactem.ac.uk/software/acromine/dictionary.py?sf=HMM 3 Data classes based on JSON response: data class sf( #SerializedName("sf") #Expose val sf : String? = null, #SerializedName("lfs") #Expose val lfs : List<lfs>? = null, ) data class lfs( #SerializedName("lf") #Expose var lf : String? = null, #SerializedName("freq") #Expose var freq : Int? = null, #SerializedName("since") #Expose var since : Int? = null, #SerializedName("vars") #Expose var vars : List<vars>? = null, ) : Serializable class vars ( #SerializedName("lf") #Expose var lf : String? = null, #SerializedName("freq") #Expose var freq : Int? = null, #SerializedName("since") #Expose var since : Int? ): Serializable Code in Activity: listUsers = mutableListOf() adapter = WordAdapters(this, listUsers ) recyclerView.adapter = adapter wordViewModel = ViewModelProviders.of(this, WordViewModelFactory(this)).get(WordsViewModel::class.java) wordViewModel!!.getData().observe(this, { t: ArrayList<sf>? -> listUsers.clear() t?.let { listUsers.addAll(it) } adapter.notifyDataSetChanged() }) ViewModel: class WordsViewModel ( context: Context) : ViewModel() { private var listData = MutableLiveData<ArrayList<sf>>() init { val wordRepository: WordsRepository by lazy { WordsRepository } //if (context.isInternetAvailable()) { listData = wordRepository.getMutableLiveData(context) // } } fun getData(): MutableLiveData<ArrayList<sf>> { return listData } } Repository: object WordsRepository { var call: Call<MutableList<sf>>? = null fun getMutableLiveData(context: Context) : MutableLiveData<ArrayList<sf>> { val mutableLiveData = MutableLiveData<ArrayList<sf>>() //context.showProgressBar() call = NetworkApiClient.apiService.getWordsMatching("HMM") call!!.enqueue(object : Callback<MutableList<sf>> { override fun onFailure(call: Call<MutableList<sf>>, t: Throwable) { //hideProgressBar() Log.e("error", t.localizedMessage.toString()) } override fun onResponse(call: Call<MutableList<sf>>, response: Response<MutableList<sf>>) { //hideProgressBar() if (!response.isSuccessful){ Log.e("Code " , response.code().toString()); return } val raw: okhttp3.Response = response.raw() val usersResponse : MutableList<sf>? = response.body() /* if (usersResponse != null) { for( movie in usersResponse[0].lfs!!){ Log.v("MainActivity", movie.vars.toString()) } }*/ Log.e("Output : ", usersResponse.toString()) usersResponse?.let { mutableLiveData.value = it as ArrayList<sf> } } }) return mutableLiveData } } this is the base structure of JSON: here "sf" is a string, lfs is the array, according to this JSON response link provided I get 8 lfs arrays, but currently after parsing the recyclecount is 1 which is the same in the adapter itemcount method, so I get one row displayed in recylerview and rest are ignored. JSON response: [ { "sf":"HMM", "lfs":[ { "lf":"heavy meromyosin", "freq":267, "since":1971, "vars":[ { "lf":"heavy meromyosin", "freq":244, "since":1971 }, { "lf":"Heavy meromyosin", "freq":12, "since":1975 }, { "lf":"H-meromyosin", "freq":5, "since":1975 }, { "lf":"heavy-meromyosin", "freq":4, "since":1977 }, { "lf":"heavy meromyosin", "freq":1, "since":1976 }, { "lf":"H-Meromyosin", "freq":1, "since":1976 } ] }, I want to ignore "sf" string after response and parse the ArrayList which is present under the "sf" which is "lfs", so based on "lfs" I need to display the data. Mutable live data is not accepting any other type other than sf, since I placed the observer on it.
On the json you posted, there is only one parent item ( one sf ), but you are actually trying to pass the 8 lfs children. You have to perform such transformation somewhere, it could be on the network call directly, like this: usersResponse?.let { mutableLiveData.value = it[0].lfs as ArrayList } Take into account two things: It could be better to check if "it" is not empty before going for the first item. This only works if you will always have only one item on the parent array ( this sounds strange since if this is the case then the service should be returning an object, not a list, as the root of the json. If you will receive more than one object you will have to map the response into a single list of lfs. Something like (pseudo code since I'm from my phone): It.map( item -> item.lfs)
How to convert similar Json objects to Json Arrays with Gson and Retrofit
Let's say I'm getting below response from API {"success":true, "base":"EUR", "date":"2021-04-16", "rates": {"AED":4.393943,"AFN":92.83145,"ALL":123.125765,"AMD":621.059723}} and I want the last rates object to be a List of Rate class i.e. Rate(code="AED", value="4.393943"). I am using Retrofit and Gson. I know I need a Deserializer or a Type Adapter. I just don't know how to write one. Rate Class: data class Rate( val code:String, val value:Float ) Currency Class: data class Currency( #SerializedName("success") val success: Boolean?, #SerializedName("base") val base: String?, #SerializedName("date") val date: Date?, #SerializedName("rates") val rates: List<Rate>? ) This is the request: val gson: Gson = GsonBuilder() .registerTypeHierarchyAdapter(Rate::class.java, TypeAdapterOrDeserializer()) .create() val retrofit = Retrofit.Builder() .baseUrl("https://api.exchangerate.host") .addConverterFactory(GsonConverterFactory.create(gson)) .build() val jsonPlaceholderApi = retrofit.create(JsonPlaceholderApi::class.java) val call = jsonPlaceholderApi.currency When I request data from api it gives this error, I know why ;) as I have given List of Rates in Currency class but it is a json rate object. com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT Thank you for the help :)
The data for rates returned by the API is a JSON object, but in your data class you defined it as a List so Gson expects it to be a JSON array. You would need custom parsing for your Currency class in this case. Use this deserializer: class CurrencyDeserializer : JsonDeserializer<Currency> { override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Currency { if (json == null || context == null) { // handle error here throw Exception("Error") } val obj = json.asJsonObject // let Gson handle the other 3 properties val success = context.deserialize<Boolean?>(obj.get("success"), Boolean::class.java) val base = context.deserialize<String?>(obj.get("base"), String::class.java) val date = context.deserialize<Date?>(obj.get("date"), Date::class.java) // create List<Rate> from the rates JsonObject val ratesSet = obj.get("rates").asJsonObject.entrySet() val ratesList = ratesSet.map { val code = it.key val value = it.value.asFloat Rate(code, value) } return Currency(success, base, date, ratesList) } } Add it to Gson like this: val gson: Gson = GsonBuilder() .registerTypeAdapter(Currency::class.java, CurrencyDeserializer()) .create()
How do I include the DOCTYPE declaration when serializing to XML using Retrofit2 and SimpleXmlConverterFactory?
I'm trying to use SimpleXmlConverterFactory with Retrofit to create an XML request to a REST service. However, the service requires the DTD declaration in the request like so. <!DOCTYPE paymentService PUBLIC "-//WorldPay//DTD WorldPay PaymentService v1//EN" "http://dtd.worldpay.com/paymentService_v1.dtd"> But when SimpleXmlConverterFactory serializes the data objects, it leaves off the DOCTYPE declaration: <paymentService merchantCode="..." version="1.4"> <inquiry> <shopperTokenRetrieval> <authenticatedShopperID>...</authenticatedShopperID> </shopperTokenRetrieval> </inquiry> </paymentService> Creating the SimpleXmlConverterFactory isn't anything special: val builder = Retrofit .Builder() .addConverterFactory( SimpleXmlConverterFactory.create() ) .baseUrl(BuildConfig.WORLDPAY_BASE_URL) .build() .create(WorldPayApiService::class.java) And here is the annotated data object #Root(name = "paymentService") data class WorldPayXmlPaymentService( #field:Attribute(name = "version") var version: String = "", #field:Attribute(name = "merchantCode") var merchantCode: String = "", #field:Element(name = "reply", required = false) var reply: WorldPayXmlReply? = null, #field:Element(name = "inquiry", required = false) var inquiry: WorldPayXmlInquiry? = null )
I added an override of a Persister as a parameter to the SimpleXmlConverterFactory.create method, like the following. It looks for the root element and then outputs the DOCTYPE before serializing the rest. val persister = object : Persister() { #Throws(Exception::class) override fun write(source: Any, out: Writer) { (source as? WorldPayXmlPaymentService)?.let { val docType = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<!DOCTYPE paymentService PUBLIC \"-//WorldPay//DTD WorldPay PaymentService v1//EN\" \"http://dtd.worldpay.com/paymentService_v1.dtd\">" out.write(docType) super.write(source, out) } } } val builder = Retrofit .Builder() .addConverterFactory( SimpleXmlConverterFactory.create(persister) ) .baseUrl(BuildConfig.WORLDPAY_BASE_URL) .build() .create(WorldPayApiService::class.java) where WorldPayXmlPaymentService is my root data object. Thanks to #CommonsWare for the guidance.
How to get GSON & Retrofit to serialize and map an array from a JSON response into a String?
I am making an API request which returns some array values. I need to serialize these array values so that I can assign them to their corresponding class attributes (which are String types). Now I know how to use GSON to serialize and deserialize lists, but with Retrofit the mapping is done automatically. This means that if my attribute is of type String, the API call returns the error "Expected a String but received an Array instead". How do I get around this so that I can receive them as arrays without failure, and them store them as strings subsequently? My API Response: { "utterances": [{ "langs": ["eng", "afr", "xho", "zul"], "utts": [ "Have you been here before?", "Was u al hier gewees?", "Ingaba wakhe weza apha ngaphambili?", "Ingabe uke weza lapha ngaphambilini?" ], "responses": [ ["Yes", "No"], ["Ja", "Nee"], ["Ewe", "Hayi"], ["Yebo", "Cha"] ] }, { "langs": ["eng", "afr", "xho", "zul"], "utts": [ "How are you?", "Hoe gaan dit met jou?", "unjani?", "unjani?" ], "responses": [ ["Good", "Bad"], ["Goed", "sleg"], ["ezilungileyo", "ezimbi"], ["kuhle", "kubi"] ] } ] } My UtteranceResponse class: class UtteranceResponse { #SerializedName("status") var status: String? = null #SerializedName("count") var count: Int = 0 #SerializedName("utterances") var utterances: ArrayList<Utterance>? = null } My Utterance class: class Utterance: SugarRecord { #SerializedName ("langs") var langs: String? = null #SerializedName ("utts") var utterances_text: String? = null var utterances_tts: String? = null #SerializedName ("responses") var responses_text: String? = null constructor(){ } } And finally the calling function: fun getUtterancesFromWebservice (){ val apiService = ApiInterface.create() val call = apiService.getUtteranceDetails() call.enqueue(object: Callback<UtteranceResponse> { override fun onResponse(call: Call<UtteranceResponse>, response: retrofit2.Response<UtteranceResponse>?) { if (response != null) { if (response.body()?.utterances != null){ var list: List<Utterance> = response.body()?.utterances!! val utterances: Utterance = list[0] //storeUtterancesFromList(list) } else { Log.d ("Response:", response.body().toString()) } }else{ Log.d ("responseResult", "NULL") } } override fun onFailure(call: Call<UtteranceResponse>, t: Throwable) { Log.e("SHIT", t.toString()) } }) } UPDATE My API Interface as well: #GET("bins/1ahazo") abstract fun getUtteranceDetails():Call<UtteranceResponse> companion object Factory { const val BASE_URL = "https://api.myjson.com/" fun create(): ApiInterface { val gson = GsonBuilder().setPrettyPrinting().create() val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() return retrofit.create(ApiInterface::class.java) } }
You are returning single object not list. Change Call<UtteranceResponse> in ApiInterface to Call<List<Utterance>> and for converting list to string list to string and string to list class Utterance: SugarRecord { #SerializedName ("langs") var langs: List<String?>? = null #SerializedName ("utts") var utterances_text: String? = null var utterances_tts: List<String?>? = null #SerializedName ("responses") var responses_tex:List<List<String?>?>? = null; constructor(){ } }
How to get Json Response in Arraylist from List<Item> using RxJava and Kotlin
Facing Problem on getting Response in ArrayList. I have following Respose on String value var res_message: String = "" res_message = "${result.vehicletypes} " Getting below Value on this String [VehicleType(_id=1, vehicleType=Hatchback, __v=0), VehicleType(_id=2, vehicleType=Maruti, __v=0), VehicleType(_id=3, vehicleType=Honda, __v=0), VehicleType(_id=4, vehicleType=Bike, __v=0)] Retrofit Result is vehicletypes = {ArrayList#6055} size = 4 0 = {Model$VehicleType#6058} "VehicleType(_id=1, vehicleType=Hatchback, __v=0)" 1 = {Model$VehicleType#6059} "VehicleType(_id=2, vehicleType=Maruti, __v=0)" 2 = {Model$VehicleType#6060} "VehicleType(_id=3, vehicleType=Honda, __v=0)" 3 = {Model$VehicleType#6061} "VehicleType(_id=4, vehicleType=Bike, __v=0)" Below Code snippest sending request to API. disposable = apiServices.getVehicle(token) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { result -> res_message = "${result.vehicletypes} " Log.d("Type==", res_message) }, { error -> res_message = "${error.message}" // validateToken() } ) Model Class data class Vehicles(val success: Boolean, val vehicletypes: List<VehicleType>, val message: String) data class VehicleType(val _id: String, val vehicleType: String, val __v: String) I want to get this value on Arralist VehicleType List on below vehicleListArray private var vehicleListArray: ArrayList<Model.VehicleType>? = null How we can achieve this. Thanks in advance.
Assuming that what you are trying to parse is a response from a service that is able to send you propper format for lists (eg Json) than Retrofit can handle parsing lists with ease. In your apiService definition: fun getPeople(token: Whatever): Observable<List<VehicleType>> And if you don't have it already: Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create(gson))
I got solution I have to handle Respose as below code snippet. private fun getVehicleType() { disposable?.add(apiServices.getVehicle(token) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(this::handleResponse, this::handleError)) } private fun handleResponse(vehicles: Model.Vehicles) { VehiclesArrayList = ArrayList(vehicles.vehicletypes) Log.d("type==","n--"+VehiclesArrayList ) mAdapter = DataAdapter(VehiclesArrayList !!, this) v_android_list.adapter = mAdapter } private fun handleError(error: Throwable) { Log.d("type", error.localizedMessage) Toast.makeText(context, "Error ${error.localizedMessage}", Toast.LENGTH_SHORT).show() }