Related
I'm looking to parse the below JSON:
{
"list": [
{
"data1": "data1",
"transaction": {
"data2": "data2",
"data3": "data3"
},
"breakdowns": [
{
"data4": "data4",
"data5": "data5"
}
]
}
]
}
I'm using Moshi and okHttpClient to handle this JSON.
My data class is correct
But when I try to parse it as below:
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val type = Types.newParameterizedType(List::class.java,PaymentRequest::class.java)
try{
val q = moshi.adapter(type)
paymentRequest = q.fromJson(response.body!!.source())!!
} catch (e: Exception) {
println(e)
}
I get this error : com.squareup.moshi.JsonDataException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at path $
You can't treat this json as a list. It's not a list itself but actually is a json object that contains a list.
To solve that, first build a class to wrap the "list":
#JsonClass(generateAdapter = true)
data class Wrapper(#Json(name = "list") val list: List<PaymentRequest>)
Then you are good to go:
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val adapter = moshi.adapter<Wrapper>(Wrapper::class.java)
val paymentRequests = adapter.fromJson(response.body!!.source())!!.list
It worked for me, Convert Json to List in Moshi
#TypeConverter
fun toListAWDataItem(json: String): List<Person>? {
val type: Type = Types.newParameterizedType(
List::class.java,
Person::class.java
)
val adapter: JsonAdapter<List<Person>> = moshi.adapter<List<Person>>(type)
return adapter.fromJson(json)!!.map { it }
}
I'm using Retrofit2 for the first time, so I'm confused how to proceed. I have the following code
fun getAddressFromPostCode(postCode: String): List<PXAddress>{
val trimmedPostCode = postCode.replace("\\s".toRegex(),"").trim()
val dataBody = JSONObject("""{"postCode":"$trimmedPostCode"}""").toString()
val hmac = HMAC()
val hmacResult = hmac.sign(RequestConstants.CSSecretKey, dataBody)
val body = JSONObject("""{"data":"$dataBody", "data_signature":"$hmacResult"}""").toString()
val url = RequestConstants.getAddress
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(url)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
val address: PXAddress = retrofit.create(PXAddress::class.java)
}
with the idea that body needs to look like this:
"data":{
"postcode": "WA1 1LD"
},
"data_signature": "{{getSignature}}"
}
and the response should be
"success": 1,
"addresses": [
{
"address1": "47 Museum Street",
"address2": null,
"address3": null,
"town": "WARRINGTON",
"county": "",
"postcode": "WA1 1LD"
},
{
"address1": "49a Museum Street",
"address2": null,
"address3": null,
"town": "WARRINGTON",
"county": "",
"postcode": "WA1 1LD"
},
{
"address1": "Leda Recruitment",
"address2": "49 Museum Street",
"address3": null,
"town": "WARRINGTON",
"county": "",
"postcode": "WA1 1LD"
}
]
}
And I need to convert that response into a list of PXAddress which is
open class PXAddress : RealmObject() {
var addressLine1: String? = null
var addressLine2: String? = null
var addressLine3: String? = null
var town: String? = null
var county: String? = null
var postcode: String? = null
}
Your implementation is wrong for some reasons:
Use an interface to define the web service request, you must define a class like this:
interface ApiService {
#POST("your/webservice/path")
fun getPXAddress(#Body dataBody: YourBodyModel): Call<List<PXAddress>>
}
You must call your webservice with a data class as body, the gson converter will convert your models in json, in your main code you must do that:
fun getAddressFromPostCode(postCode: String): List<PXAddress>{
val trimmedPostCode = postCode.replace("\\s".toRegex(),"").trim()
val dataBody = DataBodyObject(postCode = trimmedPostCode)
val hmac = HMAC()
val hmacResult = hmac.sign(RequestConstants.CSSecretKey, dataBody)
val yourBodyModel = YourBodyModel(data = dataBody, data_signature = hmacResult)
val url = RequestConstants.getUrl() // This address must be only the host, the path is specified in ApiService interface
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.build()
val api: ApiService = retrofit.create(ApiService::class.java) // With this you create a instance of your apiservice
val myCall: Call<List<PXAddress>> = api.getPXAddress(yourBodyModel) //with this you can call your service synchronous
}
One last thing, you must call your method asynchronous mode with rxjava, livedata or coroutines. All of them offers converters to retrofit. By default, retrofit has a call method like the example that I show you, you can complete your code doing this:
myCall.enqueue(object : Callback<List<PXAddress>> {
override fun onFailure(call: Call<List<PXAddress>>?, t: Throwable?) {
// Error response
}
override fun onResponse(call: Call<List<PXAddress>>?, response: Response<List<PXAddress>>?) {
// Success response
val myList : List<PXAddress> = response?.body
}
})
Best regards!
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
Trying some different methods to parse nested Json that is less than user friendly. With the logger I can see the result coming in correctly but the log shows error
com.squareup.moshi.JsonDataException: Expected a string but was BEGIN_OBJECT at path $.capabilities[1]
I cannot for the life of me figure out how to parse the Attribute array. I have tried doing <List<Attribute>> and Attribute and it does not change the result. Is there a way to convert the Attribute array into a list?
Very new at coding in Android so looking for some help.
JSON to parse
{
"id": "65",
"name": "Switch - Kitchen",
"label": "Switch - Kitchen",
"attributes": [
{
"name": "switch",
"currentValue": "off",
"dataType": "ENUM",
"values": [
"on",
"off"
]
}
],
"capabilities": [
"Switch",
{
"attributes": [
{
"name": "switch",
"dataType": null
}
]
},
"Configuration",
"Refresh",
"Actuator"
],
"commands": [
"configure",
"flash",
"off",
"on",
"refresh",
"refresh"
]
}
DeviceDetails
data class DeviceDetails(
#Json(name="CapabilitiesList")
var attributeList: Attribute,
#Json(name="CapabilitiesList")
val capabilities: List<String>,
#Json(name="CommandsList")
val commands: List<String>,
var id: String = "",
var label: String = "",
var name: String = ""
)
data class Attribute(
val currentValue: String,
val dataType: String,
val name: String,
#Json(name="AttributesValues")
val values: List<String>
)
DeviceDetailsAPI
interface DeviceDetailsAPI {
#GET("devices/65")
fun getDeviceDetails(#Query("access_token") access_token: String):
Deferred<DeviceDetails>
companion object{
operator fun invoke(): DeviceDetailsAPI {
//Debugging URL//
val interceptor : HttpLoggingInterceptor = HttpLoggingInterceptor().apply {
this.level = HttpLoggingInterceptor.Level.BODY }
val client : OkHttpClient = OkHttpClient.Builder().apply {
this.addInterceptor(interceptor)}.build()
//Debugging URL//
val okHttpClient = OkHttpClient.Builder()
.build()
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl("http://xxx.xxx.xxx.xxx/apps/api/109/")
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.addConverterFactory(MoshiConverterFactory.create())
.client(client)
.build()
.create(DeviceDetailsAPI::class.java)
}
}
}
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val API_KEY = "xxxxxxxx"
val testapiService = DeviceListAPI()
val testapiDetails = DeviceDetailsAPI()
//GlobalScope.launch (Dispatchers.Main) {
//val DeviceListAPI = testapiService.getAllDevices(access_token = API_KEY).await()
//textViewID.text = DeviceListAPI.toString()
//}
GlobalScope.launch (Dispatchers.Main) {
val DeviceDetailsAPI = testapiDetails.getDeviceDetails(access_token = API_KEY).await()
textViewID.text = DeviceDetailsAPI.toString()
}
}
}
The apparent problem is that the "capabilities": ... in the JSON block is a mixed type list, but you declare it as val capabilities: List<String>. Hence it fails when it hits the
{
"attributes": [
{
"name": "switch",
"dataType": null
}
]
},
item. It's hard to guess how this item relates to the capabilities, but as it currently stands it looks like this will require a pretty complicated custom Moshi adapter to be able to parse this into a meaningful data structure.
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
}})