Gson custom deserializer when it contains object or json string - android

How I can I write custom deserializer for same object? Or create 2 Objects (one which contains raw json string and another - Object)?
I'm getting "all device list" from server, and "updated device list" from server. Problem is that when I getting "all device list" I'm getting "Sensor", "Services", "Tail" as objects, but when getting "updated device list" that fields comes as strings (json strings). I will add photos to clarify issue.
All devices response:
[All devices] https://lh3.googleusercontent.com/lzhJXviqtW2l0pLHkJ9-2VBck9hnmREdfTlWTMF8BYTklbXR90jDe2AZbG9sHHPynCXh3HWlFQBsFBiL_oke-AMy-79zIqUsW8HVT7CrulA_KKyItARfwbRxWm8LWBspEriJEsfT4_AgNR7uceyO9pH4VAIm9LBBcuvoFOMJmvSt-y85dgqPld3WJ0RwIlQOBv3rqiw6Ci4QOC983LpiJAKgTgshsQeGNKUWnBIbzkJr87vtF54XOf4PsdAqv3IuRczQSeNLllPTIEaz6rylCotcgANWShMcpm-hQdlrt7E0VBygGTZGLW4hoOQn2F4DWH8p-QH2pEb7JKtEopha0q5cyISbK6AQSYHKMpYb7E8I_FKoxbQIEsvvjDPwGezZlloI1r6HnRHDpAaL8a2eF3yZ3ABTORHS8fa5UY___nvE1cppgzhuK9a1pFFX_A7yZQN_zQNM58i5hW6q0MhawbfFyY8NXBtTOujxz_NIg4_qP3Eu58Eb-wwz17pxYfpAbPRiDXrQDgaXPSmOrJ_txvdzP910UcxHZqFNiW_k3DXUN2tzCY2XSuyyZgc2eX-ZWaE0tevqsvdiCJSd2OvNBC8ihmZUTT0mreyZy-ii8xi2uHjN0KCtvmAkpy-XlKU1lddoxlwj_IrZqbSH_UPdqF4sa5tEahQ=w598-h1336-no
Updated devices response:
[updated devices] https://lh3.googleusercontent.com/-5VlMyTbTCmLcc14oWbaE5QViCio1V3NlcXQrrApZatE8K3CnW4flCGTbpncp_rbFnhjYq49WBqKBBD4g8TBwbHvZe1qsUZSW4JaTr1ZVJFCiLz1fl6SMredI7XnYhbm85q4-RxwxaEP90X6QAR7JOqZICk1QWTS0KxTp3XckHP45ca02LHicNO4TuB72_M1-vx4FMoAZQ42vSrnKZsHBqprJnsthS3ZoG9_v72xgjXPFRb144sRb0PPn6L9VbphZPbBhcg7WU68LK6eZgqyjIByxnkAlh-tsOLsWfMoze1Df93x9OI6OqOao8cphZD_bot-AViErtnc_vyNLHaDniJ6G6gUdDgco6rNCgyPvhGIcPbKf-wWdB8ENQ47HeJVxuJ0E9EE7rGRFhPchR1FxGM2nHewyoicds6uS2eCVpiKjtnen6TuTDfx3aH9NRUzL3thvN_9Jengh_XsyMPpvhGGHAUEcpYraHEXdtC25X9t5zpB1FhLzDiWVIZwhwihOCVI-5J-VBi7JqCOm9eaagYFunZSQI6U3u1qK1SZZfCarzxm_1dCYUECBNAUCdb_HfHM24m9-D8_R77t9DNrMPgisPtOonkqORJzTtHsz-FjyxXAHbp72urUGrHLLlZnRRsrpj4NFdLX_DuXwp5k3SiUlnWTLHk=w2214-h1312-no
[All devices model] https://lh3.googleusercontent.com/lRHdw7nVVz05dG57znq_Vzwwc9zFgEmeS_kQsrqcY2u6OOkcknrpMSytte14jOqmRH_829RAitVc3HVQL2St3g8iSh7STm9XNxuBTJkxAo9OPjbv6l02s02jT35IXWo2qhGy0hiiD4uBfAlYdyy-fjXGCAe4wMLvJYPiDUc9mY73Tf3rxRw5TJ1-XW11hyE3oeHj5OkMzET7_mMoKhro3Bs97HzdDOWO2_5QHAYeqwIuQNJ8NfjU1HTeozdOF_NuAk2nZF8YajHX_VJ4FZtOhovTltZGWJseQ2RzM3gsmd2nt5-obE3msOxPYXf3R9lYlolryRNpBFGHWOrR9xj-6QfuxIOHJOWhQAkkNEAOB7emcvvFzgSQkj0Xkoro2nZCkPH-wagSAJFm-cMhOjapdwhvvlUEjaOh9dw29RBT7jfYH5r2f7i2KELBxksG1uIVo9PcHjg0hvgC0xsw2WMLWFlDnWetngJxmyl6U8pM8NNLghm5CxFaMAKnygnN_4F0aCjfQEeBX-XaC5ss4_1fDQnifw7UcnhBH7n5VZqyYiK1jNqPYDGFJS1J8FJPkXmZsc8uOtAXIa0t58ZOrQ5jHeAKsIlrlj-eM_HP7hM9iQi0VCxjaRmonD_nc6ueKGjyj4xDm9b4uAXnO1B3krVfGAstaJ_RvHw=w552-h652-no
[updated devices model] https://lh3.googleusercontent.com/8uIPKFBfwEfGg7t_KlIB8xJrI1RPJW8WhozKIa7enNlGf7AMZqF0r8qz3FPZ6IzSlyfI3PFk6OFIskPpxzep5_cHudRK5LhpbHdsZkplM8aCoeOSO7-DG8bgTQ__-iEzpmhSG4HORmLauA80L6tEh3SJxbOPoKlconsj7cXWT2uZcPD9xYAjBYHtMp7wrOZXEeAHJPqLoonxIVPsQ-CGTmhVDjQW33YUqhUytDY1FlJcnNCRQVUTRsw5NasxwvCkDVdhFBFlDNyVXX6HN_GL-ZBLNmxrOQyxz_Q7_D7e_sGVp9iw0iQIABr4ogk-9chsY8F0iNjdrD67BxGXaExRN9Nog-TuHgDEf1uLgHUJi6TZRyoalalF-q1Rf6h3Xj3mKj56bSfgk8Z25W3fToOQBMwNEwcYIy8c4bzDjMzvS8san5XkRDIrilN7-iRwJQjH-Q43cgLeSg7wTBCFrqeRPblRCe79A1dVTEnlkHl9aPmp8aW9ih0b5yMcmJjK40BwqQu0sBplzBL_oKDeooVxQ7Yp-sAfa9XKHB-s4WiKbU5fAfURt_jsRO5JtHCeiyjVw9D2RiCZ_fbzTTZGMv4ysJNIYU9oADJLdl-zHTbGzpNggd-KxOWnoDffdHlQkH5nB_FeH11hMbkvdjpFarblpMjh_hhh5dM=w681-h763-no
Json where I'm getting Object
[{"id":"68","title":"testRecall","items":[{"id":992,"alarm":0,"name":"CloudMM","online":"offline","time":"2018-10-28 23:30:58","timestamp":1540806358,"acktimestamp":1540806358,"lat":0,"lng":0,"course":0,"speed":0,"altitude":0,"icon_type":"icon","icon_color":"red","icon_colors":{"moving":"green","stopped":"red","offline":"red","engine":"yellow"},"icon":{"id":10,"user_id":null,"type":"icon","order":3,"width":46,"height":64,"path":"images\/device_icons\/v2\/objects2a_38.png","by_status":"0"},"power":"-","address":"-","protocol":"osmand","driver":"-","driver_data":{"id":null,"user_id":null,"device_id":null,"name":null,"rfid":null,"phone":null,"email":null,"description":null,"created_at":null,"updated_at":null},"sensors":[],"services":[],"tail":[],"distance_unit_hour":"kph","unit_of_distance":"km","unit_of_altitude":"mt","unit_of_capacity":"lt","stop_duration":"0h","moved_timestamp":0,"engine_status":null,"detect_engine":"gps","engine_hours":"gps","total_distance":0,"device_data":{"id":992,"user_id":70,"current_driver_id":null,"timezone_id":null,"traccar_device_id":992,"icon_id":10,"icon_colors":{"moving":"green","stopped":"red","offline":"red","engine":"yellow"},"active":1,"deleted":0,"name":"CloudMM","imei":"665544332211","fuel_measurement_id":1,"fuel_quantity":"0.00","fuel_price":"0.00","fuel_per_km":"0.00","sim_number":"","device_model":"","plate_number":"","vin":"","registration_number":"","object_owner":"","additional_notes":"","expiration_date":null,"sim_expiration_date":"0000-00-00","sim_activation_date":"0000-00-00","installation_date":"0000-00-00","tail_color":"#33cc33","tail_length":5,"engine_hours":"gps","detect_engine":"gps","min_moving_speed":6,"min_fuel_fillings":10,"min_fuel_thefts":10,"snap_to_road":0,"gprs_templates_only":0,"valid_by_avg_speed":"1","parameters":"[\"batterylevel\",\"satellites\",\"hdop\",\"sequence\",\"distance\",\"totaldistance\",\"motion\",\"valid\",\"enginehours\"]","currents":null,"created_at":"2018-10-29 05:34:27","updated_at":"2019-02-27 07:58:31","forward":{"active":"1","ip":"198.121.31.32","port":"6000","protocol":"TCP"},"stop_duration":"0h","pivot":{"user_id":70,"device_id":992,"group_id":68,"current_driver_id":null,"active":1,"timezone_id":null},"traccar":{"id":"992","name":"CloudMM","uniqueId":"665544332211","latestPosition_id":"1","lastValidLatitude":null,"lastValidLongitude":null,"other":"<info><batterylevel>23<\/batterylevel><satellites>0<\/satellites><hdop>0<\/hdop><sequence>6<\/sequence><distance>0<\/distance><totaldistance>0<\/totaldistance><motion>false<\/motion><valid>false<\/valid><enginehours>0<\/enginehours><\/info>","speed":"0.00","time":"2018-10-29 09:45:45","device_time":"2018-10-29 09:45:45","server_time":"2018-10-29 09:45:58","ack_time":"2018-10-29 09:45:58","altitude":null,"course":null,"power":null,"address":null,"protocol":"osmand","latest_positions":null,"moved_at":null},"icon":{"id":10,"user_id":null,"type":"icon","order":3,"width":46,"height":64,"path":"images\/device_icons\/v2\/objects2a_38.png","by_status":"0"},"sensors":[],"services":[],"driver":null,"users":[{"id":70,"email":"tomas#gpswox.com"}],"lastValidLatitude":0,"lastValidLongitude":0,"latest_positions":null,"icon_type":"icon","group_id":68,"user_timezone_id":null,"time":"2018-10-29 09:45:45","course":0,"speed":0}},{"id":1099,"alarm":0,"name":"testRecalObject","online":"offline","time":"Not connected","timestamp":0,"acktimestamp":0,"lat":0,"lng":0,"course":0,"speed":0,"altitude":0,"icon_type":"arrow","icon_color":"red","icon_colors":{"moving":"green","stopped":"yellow","offline":"red","engine":"yellow"},"icon":{"id":0,"user_id":null,"type":"arrow","order":1,"width":25,"height":33,"path":"assets\/images\/arrow-ack.png","by_status":"0"},"power":"-","address":"-","protocol":"-","driver":"-","driver_data":{"id":null,"user_id":null,"device_id":null,"name":null,"rfid":null,"phone":null,"email":null,"description":null,"created_at":null,"updated_at":null},"sensors":[],"services":[],"tail":[],"distance_unit_hour":"kph","unit_of_distance":"km","unit_of_altitude":"mt","unit_of_capacity":"lt","stop_duration":"0h","moved_timestamp":0,"engine_status":null,"detect_engine":"gps","engine_hours":"gps","total_distance":0,"device_data":{"id":1099,"user_id":70,"current_driver_id":null,"timezone_id":null,"traccar_device_id":1099,"icon_id":0,"icon_colors":{"moving":"green","stopped":"yellow","offline":"red","engine":"yellow"},"active":1,"deleted":0,"name":"testRecalObject","imei":"oooopaaa","fuel_measurement_id":1,"fuel_quantity":"0.00","fuel_price":"0.00","fuel_per_km":"0.00","sim_number":"","device_model":"","plate_number":"","vin":"","registration_number":"","object_owner":"","additional_notes":"","expiration_date":null,"sim_expiration_date":"0000-00-00","sim_activation_date":"0000-00-00","installation_date":"0000-00-00","tail_color":"#63f542","tail_length":0,"engine_hours":"gps","detect_engine":"gps","min_moving_speed":1,"min_fuel_fillings":1,"min_fuel_thefts":1,"snap_to_road":0,"gprs_templates_only":0,"valid_by_avg_speed":"1","parameters":null,"currents":null,"created_at":"2019-07-20 21:46:11","updated_at":"2019-07-20 21:46:11","forward":null,"stop_duration":"0h","pivot":{"user_id":70,"device_id":1099,"group_id":68,"current_driver_id":null,"active":1,"timezone_id":null},"traccar":{"id":"1099","name":"testRecalObject","uniqueId":"oooopaaa","latestPosition_id":null,"lastValidLatitude":null,"lastValidLongitude":null,"other":null,"speed":null,"time":null,"device_time":null,"server_time":null,"ack_time":null,"altitude":null,"course":null,"power":null,"address":null,"protocol":null,"latest_positions":null,"moved_at":null},"icon":{"id":0,"user_id":null,"type":"arrow","order":1,"width":25,"height":33,"path":"assets\/images\/arrow-ack.png","by_status":"0"},"sensors":[],"services":[],"driver":null,"users":[{"id":70,"email":"tomas#gpswox.com"}],"lastValidLatitude":0,"lastValidLongitude":0,"latest_positions":null,"icon_type":"arrow","group_id":68,"user_timezone_id":null,"time":null,"course":0,"speed":0}}]}]
And where I'm getting String
{"items":[{"id":124,"alarm":0,"name":"Demo 1","online":"ack","time":"2019-08-22 03:26:02","timestamp":1566481263,"acktimestamp":0,"lat":55.675715,"lng":12.576404,"course":37,"speed":0,"altitude":1,"icon_type":"icon","icon_color":"yellow","icon_colors":{"moving":"green","stopped":"yellow","offline":"red","engine":"yellow"},"icon":{"id":3,"user_id":null,"type":"icon","order":3,"width":46,"height":64,"path":"images/device_icons/v2/objects2a_63_ack.png","by_status":"1"},"power":"-","address":"-","protocol":"homtecs","driver":"Testing","driver_data":{"id":"3","user_id":"1","device_id":"128","device_port":null,"name":"Testing","rfid":"ABC451132","phone":"4","email":"","description":"","created_at":"2017-06-20 19:00:43","updated_at":"2018-10-22 14:38:51"},"sensors":"[{\"id\":\"299\",\"type\":\"satellites\",\"name\":\"Satellites\",\"show_in_popup\":\"0\",\"value\":\"8\",\"val\":\"8\",\"scale_value\":null},{\"id\":\"300\",\"type\":\"satellites\",\"name\":\"Accuracy\",\"show_in_popup\":\"0\",\"value\":\"1.02\",\"val\":\"1.02\",\"scale_value\":null}]","services":"[{\"id\":\"9\",\"name\":\"Service one\",\"value\":\"Days Left (28d.)\",\"expiring\":false},{\"id\":\"19\",\"name\":\"Ignas\",\"value\":\"\\\"Sensor\\\" was not found.\",\"expiring\":true},{\"id\":\"20\",\"name\":\"Ignas w\",\"value\":\"Days Left (1560d.)\",\"expiring\":false},{\"id\":\"21\",\"name\":\"Poiu\",\"value\":\"\\\"Sensor\\\" was not found.\",\"expiring\":true}]","tail":"[{\"lat\":\"55.675024666667\",\"lng\":\"12.5744815\"},{\"lat\":\"55.675158166667\",\"lng\":\"12.574806833333\"},{\"lat\":\"55.675362333333\",\"lng\":\"12.575193666667\"},{\"lat\":\"55.675581666667\",\"lng\":\"12.575788833333\"},{\"lat\":\"55.675698666667\",\"lng\":\"12.5761065\"}]","distance_unit_hour":"kph","unit_of_distance":"km","unit_of_altitude":"mt","unit_of_capacity":"lt","stop_duration":"1min 42s","moved_timestamp":1566481160,"engine_status":null,"detect_engine":"gps","engine_hours":"gps","total_distance":7088342.97,"device_data":{"id":124,"user_id":70,"current_driver_id":3,"timezone_id":null,"traccar_device_id":124,"icon_id":3,"icon_colors":{"moving":"green","stopped":"yellow","offline":"red","engine":"yellow"},"active":1,"deleted":0,"name":"Demo 1","imei":"100000001","fuel_measurement_id":1,"fuel_quantity":"0.00","fuel_price":"0.00","fuel_per_km":"0.00","sim_number":"","device_model":"","plate_number":"","vin":"","registration_number":"","object_owner":"","additional_notes":"","expiration_date":null,"sim_expiration_date":"0000-00-00","sim_activation_date":"0000-00-00","installation_date":"0000-00-00","tail_color":"#33cc33","tail_length":5,"engine_hours":"gps","detect_engine":"gps","min_moving_speed":6,"min_fuel_fillings":10,"min_fuel_thefts":10,"snap_to_road":0,"gprs_templates_only":0,"valid_by_avg_speed":"1","parameters":"[\"sat\",\"hdop\",\"valid\",\"enginehours\",\"rfid\"]","currents":{"geofences":[]},"created_at":"2017-07-04 08:13:53","updated_at":"2018-12-20 08:10:59","forward":null,"stop_duration":"1min 42s","pivot":{"user_id":70,"device_id":124,"group_id":71,"current_driver_id":3,"active":1,"timezone_id":null},"traccar":{"id":"124","name":"Demo 1","uniqueId":"100000001","latestPosition_id":"9275412","lastValidLatitude":"55.675714833333","lastValidLongitude":"12.576404333333","other":"<info><sat>8</sat><hdop>1.02</hdop><valid>true</valid><enginehours>8862443</enginehours><distance>0.46</distance><totaldistance>7088342.97</totaldistance></info>","speed":"0.06","time":"2019-08-22 13:41:02","device_time":"2019-08-22 13:41:02","server_time":"2019-08-22 13:41:03","ack_time":null,"altitude":"1.4","course":"36.8","power":null,"address":null,"protocol":"homtecs","latest_positions":"55.675698666667/12.5761065;55.675581666667/12.575788833333;55.675362333333/12.575193666667;55.675158166667/12.574806833333;55.675024666667/12.5744815;55.6749245/12.574212666667;55.674809333333/12.5739365;55.674687833333/12.5736325;55.674487333333/12.5731665;55.674489666667/12.572678;55.67469/12.5724915;55.6748885/12.572210333333;55.675289/12.571647333333;55.675530666667/12.5713325;55.6757135/12.571215666667","moved_at":"2019-08-22 13:39:20"},"icon":{"id":3,"user_id":null,"type":"icon","order":3,"width":46,"height":64,"path":"images/device_icons/v2/objects2a_63_online.png","by_status":"1"},"sensors":[{"id":"299","user_id":"1","device_id":"124","name":"Satellites","type":"satellites","tag_name":"sat","add_to_history":"0","on_value":null,"off_value":null,"shown_value_by":null,"fuel_tank_name":null,"full_tank":null,"full_tank_value":null,"min_value":null,"max_value":null,"formula":null,"odometer_value_by":null,"odometer_value":null,"odometer_value_unit":"km","temperature_max":null,"temperature_max_value":null,"temperature_min":null,"temperature_min_value":null,"value":"10","value_formula":"0","show_in_popup":"0","unit_of_measurement":"","on_tag_value":null,"off_tag_value":null,"on_type":null,"off_type":null,"calibrations":null,"skip_calibration":null},{"id":"300","user_id":"1","device_id":"124","name":"Accuracy","type":"satellites","tag_name":"hdop","add_to_history":"0","on_value":null,"off_value":null,"shown_value_by":null,"fuel_tank_name":null,"full_tank":null,"full_tank_value":null,"min_value":null,"max_value":null,"formula":null,"odometer_value_by":null,"odometer_value":null,"odometer_value_unit":"km","temperature_max":null,"temperature_max_value":null,"temperature_min":null,"temperature_min_value":null,"value":"0.87","value_formula":"0","show_in_popup":"0","unit_of_measurement":"","on_tag_value":null,"off_tag_value":null,"on_type":null,"off_type":null,"calibrations":null,"skip_calibration":null}],"services":[{"id":"9","user_id":"1","device_id":"124","name":"Service one","expiration_by":"days","interval":"30","last_service":"2019-08-20","trigger_event_left":"5","renew_after_expiration":"1","expires":"0","expires_date":"2019-09-19","remind":"0","remind_date":"2019-09-14","event_sent":"0","expired":"0","email":"","mobile_phone":""},{"id":"19","user_id":"1","device_id":"124","name":"Ignas","expiration_by":"odometer","interval":"123456","last_service":"0","trigger_event_left":"0","renew_after_expiration":"0","expires":"123456","expires_date":null,"remind":"123456","remind_date":null,"event_sent":"0","expired":"0","email":"","mobile_phone":""},{"id":"20","user_id":"1","device_id":"124","name":"Ignas w","expiration_by":"days","interval":"2000","last_service":null,"trigger_event_left":"0","renew_after_expiration":"0","expires":"0","expires_date":"2023-11-29","remind":"0","remind_date":"1970-01-01","event_sent":"1","expired":"0","email":"","mobile_phone":""},{"id":"21","user_id":"1","device_id":"124","name":"Poiu","expiration_by":"odometer","interval":"12","last_service":"0","trigger_event_left":"0","renew_after_expiration":"0","expires":"12","expires_date":null,"remind":"12","remind_date":null,"event_sent":"0","expired":"0","email":"","mobile_phone":""}],"driver":{"id":"3","user_id":"1","device_id":"128","device_port":null,"name":"Testing","rfid":"ABC451132","phone":"4","email":"","description":"","created_at":"2017-06-20 19:00:43","updated_at":"2018-10-22 14:38:51"},"lastValidLatitude":55.675715,"lastValidLongitude":12.576404,"latest_positions":"55.675698666667/12.5761065;55.675581666667/12.575788833333;55.675362333333/12.575193666667;55.675158166667/12.574806833333;55.675024666667/12.5744815;55.6749245/12.574212666667;55.674809333333/12.5739365;55.674687833333/12.5736325;55.674487333333/12.5731665;55.674489666667/12.572678;55.67469/12.5724915;55.6748885/12.572210333333;55.675289/12.571647333333;55.675530666667/12.5713325;55.6757135/12.571215666667","icon_type":"icon","group_id":71,"user_timezone_id":null,"time":"2019-08-22 13:41:02","course":37,"speed":0}}],"events":[],"time":1566481263,"version":"3.4.3.1"}

Here is just proof-of-concept but I think that you'll get the idea.
import com.google.gson.*
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
data class Test(val sensors: List<Sensor>)
data class Sensor(val id: Int, val name: String)
class TestDeserializer : JsonDeserializer<Test> {
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext): Test {
val jsonObject = json.asJsonObject
val sensorsJson = jsonObject["sensors"]
val sensors = if (sensorsJson.isJsonArray) {
sensorsJson.asJsonArray.map { context.deserialize<Sensor>(it, Sensor::class.java) }
} else {
val typeToken = TypeToken.getParameterized(List::class.java, Sensor::class.java)
val sensorsElement = JsonParser().parse(sensorsJson.asString)
context.deserialize(sensorsElement, typeToken.type)
}
return Test(sensors)
}
}
fun main() {
val gson = GsonBuilder()
.registerTypeAdapter(Test::class.java, TestDeserializer())
.create()
val jsonWithObjects = """
{
"sensors": [
{
"id": 1,
"name": "test 1"
},
{
"id": 2,
"name": "test 2"
},
{
"id": 3,
"name": "test 3"
}
]
}
""".trimIndent()
val test1 = gson.fromJson(jsonWithObjects, Test::class.java)
val jsonWithStrings = """
{
"sensors": "[{\"id\": 1,\"name\": \"test 1\"},{\"id\": 2,\"name\": \"test 2\"},{\"id\": 3,\"name\": \"test 3\"}]"
}
""".trimIndent()
val test2 = gson.fromJson(jsonWithStrings, Test::class.java)
println(test1 == test2) // prints true
}

Related

Android parse JSON using JacksonAnnotation

I'm trying to parse a JSON without results.
I've a response like following:
{
"0": {
"id": "4",
"nome": "Zero Gravity",
"id_stop": "0"
},
"1": {
"id": "540",
"nome": "First Name",
"id_stop": "111"
}
}
The problem is that it's not a list with a defined id, but the id is a progressive integer that varies according to the possible answers. How do I do proper parsing? I tried building a response like this:
here my service
#GET("{path}")
suspend fun getResponse(
#Path(value = "path", encoded = true) path: String,
#Query(value = "cmd") alias: String? = "cmd",
#Query(value = "id_where") idWhere: String,
): Response<ArrayList<APIResponse>>
Here APIResponse
data class APIResponse(
#JsonProperty("id")
val id: String?,
#JsonProperty("nome")
val nome: String?,
#JsonProperty("id_stop")
val idStop: String?,
) {
fun toDomain(): API {
return API( id, nome, idStop
)
}
}
here my repository
suspend fun getAPIResponse(from: String) : API {
val response = service.getResponse(path = basePath, idWhere = where)
return response.body()?.map {
it.toDomain()
}
}
This isn't the solution though, because I can't get a complete answer but I always have a single item with all fields null. Should I use a HashMap? how could i solve?

how to flatten nested JSON into single class using retrofit and gson converter?

I have a nested JSON like this from Server, as you can see there is a nested data in location
{
"id": "18941862",
"name": "Pizza Maru",
"url": "https://www.zomato.com/jakarta/pizza-maru-1-thamrin?utm_source=api_basic_user&utm_medium=api&utm_campaign=v2.1",
"location": {
"address": "Grand Indonesia Mall, East Mall, Lantai 3A, Jl. M.H. Thamrin No. 1, Thamrin, Jakarta",
"locality": "Grand Indonesia Mall, Thamrin",
"city": "Jakarta",
"city_id": 74,
"latitude": "-6.1954467635",
"longitude": "106.8216102943",
"zipcode": "",
"country_id": 94,
"locality_verbose": "Grand Indonesia Mall, Thamrin, Jakarta"
},
"currency": "IDR"
}
I am using retrofit and using gson converter. usually I need to make 2 data class for something like this to map JSON into POJO. so I need to make Restaurant class and also Location class, but I need to flatten that json object into single Restaurant class, like this
data class Restaurant : {
var id: String
var name: String
var url: String
var city: String
var latitude: Double
var longitude: Double
var zipcode: String
var currency: String
}
how to do that if I am using retrofit and gson converter ?
java or kotlin are ok
This solution is a silver bullet for this problem and cannot be appreciated enough.
Take this Kotlin file first:
/**
* credits to https://github.com/Tishka17/gson-flatten for inspiration
* Author: A$CE
*/
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FIELD)
annotation class Flatten(val path: String)
class FlattenTypeAdapterFactory(
private val pathDelimiter: String = "."
): TypeAdapterFactory {
override fun <T: Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> {
val delegateAdapter = gson.getDelegateAdapter(this, type)
val defaultAdapter = gson.getAdapter(JsonElement::class.java)
val flattenedFieldsCache = buildFlattenedFieldsCache(type.rawType)
return object: TypeAdapter<T>() {
#Throws(IOException::class)
override fun read(reader: JsonReader): T {
// if this class has no flattened fields, parse it with regular adapter
if(flattenedFieldsCache.isEmpty())
return delegateAdapter.read(reader)
// read the whole json string into a jsonElement
val rootElement = defaultAdapter.read(reader)
// if not a json object (array, string, number, etc.), parse it
if(!rootElement.isJsonObject)
return delegateAdapter.fromJsonTree(rootElement)
// it's a json object of type T, let's deal with it
val root = rootElement.asJsonObject
// parse each field
for(field in flattenedFieldsCache) {
var element: JsonElement? = root
// dive down the path to find the right element
for(node in field.path) {
// can't dive down null elements, break
if(element == null) break
// reassign element to next node down
element = when {
element.isJsonObject -> element.asJsonObject[node]
element.isJsonArray -> try {
element.asJsonArray[node.toInt()]
} catch(e: Exception) { // NumberFormatException | IndexOutOfBoundsException
null
}
else -> null
}
}
// lift deep element to root element level
root.add(field.name, element)
// this keeps nested element un-removed (i suppose for speed)
}
// now parse flattened json
return delegateAdapter.fromJsonTree(root)
}
override fun write(out: JsonWriter, value: T) {
throw UnsupportedOperationException()
}
}.nullSafe()
}
// build a cache for flattened fields's paths and names (reflection happens only here)
private fun buildFlattenedFieldsCache(root: Class<*>): Array<FlattenedField> {
// get all flattened fields of this class
var clazz: Class<*>? = root
val flattenedFields = ArrayList<Field>()
while(clazz != null) {
clazz.declaredFields.filterTo(flattenedFields) {
it.isAnnotationPresent(Flatten::class.java)
}
clazz = clazz.superclass
}
if(flattenedFields.isEmpty()) {
return emptyArray()
}
val delimiter = pathDelimiter
return Array(flattenedFields.size) { i ->
val ff = flattenedFields[i]
val a = ff.getAnnotation(Flatten::class.java)!!
val nodes = a.path.split(delimiter)
.filterNot { it.isEmpty() } // ignore multiple or trailing dots
.toTypedArray()
FlattenedField(ff.name, nodes)
}
}
private class FlattenedField(val name: String, val path: Array<String>)
}
Then add it to Gson like this:
val gson = GsonBuilder()
.registerTypeAdapterFactory(FlattenTypeAdapterFactory())
.create()
Retrofit.Builder()
.baseUrl(baseUrl)
...
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
Using your example, you can get the pojo parsed like this:
// prefer constructor properties
// prefer val over var
// prefer added #SerializedName annotation even to same-name properties:
// to future proof and for easier proguard rule config
data class Restaurant(
#SerializedName("id") val id: String,
#SerializedName("name") val name: String,
#SerializedName("url") val url: String,
#Flatten("location.city") val city: String,
#Flatten("location.latitude") val latitude: Double,
#Flatten("location.longitude") val longitude: Double,
#Flatten("location.zipcode") val zipcode: String,
#SerializedName("currency") var currency: String
)
You can even write a path down an array, e.g #Flatten("friends.0.name") = get first friend's name.
For more info, visit this repo
Note, however, that I stripped down the TypeAdapter to only read/consume json objects. You can implement write() if you want to use it to write json too.
You are welcome.

Error in parsing json with class Kotlin Android

hi i am trying to parse JSON with kotlin
below is my json code
[{
"module":"1",
"books":[{"name":"bookname1","authors":"author1, author 2"},
{"name":"bookname2","authors":"author1, author 2"},
{"name":"bookname3","authors":"author1, author 2"}]
},
{
"module":"2",
"books":[{"name":"bookname1","authors":"author1, author 2"},
{"name":"bookname2","authors":"author1, author 2"},
{"name":"bookname3","authors":"author1, author 2"}]
},
{
"module":"3",
"books":[{"name":"bookname1","authors":"author1, author 2"},
{"name":"bookname2","authors":"author1, author 2"},
{"name":"bookname3","authors":"author1, author 2"}]
},
{
"module":"4",
"books":[{"name":"bookname1","authors":"author1, author 2"},
{"name":"bookname2","authors":"author1, author 2"},
{"name":"bookname3","authors":"author1, author 2"}]
},
{
"module":"5",
"books":[{"name":"bookname1","authors":"author1, author 2"},
{"name":"bookname2","authors":"author1, author 2"},
{"name":"bookname3","authors":"author1, author 2"}]
}]
please note that this json response starts with array
here is my class to parse it
class SemdetailsPArser {
#SerializedName("module")
#Expose
var module: String? = null
#SerializedName("books")
#Expose
var books: List<Book>? = null
}
class Book {
#SerializedName("name")
#Expose
var name: String? = null
#SerializedName("authors")
#Expose
var authors: String? = null
}
And here is my code
//interface
interface SemdetailsFetcher {
#GET("test/json/sub1.json")
fun getCurrentSemData(): Call<SemdetailsPArser>
}
here is my code in activity
fun getCurrentData() {
val retrofit = Retrofit.Builder()
.baseUrl(BaseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(SemdetailsFetcher::class.java)
val call = service.getCurrentSemData()
call.enqueue(object : Callback, retrofit2.Callback<SemdetailsPArser> {
override fun onResponse(
call: retrofit2.Call<SemdetailsPArser>?,
response: retrofit2.Response<SemdetailsPArser>?
) {
// val thisthig = response?.body();
println("here 1 ${response?.body().toString()}")
}
override fun onFailure(call: Call?, e: IOException?) {
println("here 2")
}
override fun onFailure(call: retrofit2.Call<SemdetailsPArser>?, t: Throwable?) {
println("here 3 $t")
}
override fun onResponse(call: Call, response: Response) {
if (response.code() == 200) {
println("secodn success")
val sampleResp = response.body()!!
println(sampleResp)
}
}
})
}
and i am getting this error
here 3 com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 2 path $
I understood that this might be related to my parsing class
here i am getting an array of json info, i tried the same code with another json
{
"person": {
"name": "Don",
"age": 35
},
"books": [{
"id": 800,
"name": "book 1",
"description": "clear sky",
"icon": "01n"
},
{
"id": 801,
"name": "book 2",
"description": "clear sky 1",
"icon": "01N"
}
],
"city": "bgvnslsl",
"id": 1851632,
"bname": "abcd",
"code": 200
}
this was working perfectly when i changed the parsing class and interface
My problem is that i dont know how to write a class to parse a json response starting with an array
You are expecting list of SemdetailsPArser , so you should define return type as List of SemdetailsPArser
This should fix problem.
interface SemdetailsFetcher {
#GET("test/json/sub1.json")
fun getCurrentSemData(): Call<List<SemdetailsPArser>>
}
You also need to change it in other parts of code.
The error you are getting means that you're trying to parse JSON array, thinking it should be JSON object. JSON array is the thing between these [], while JSON object is in curly brackets like these {}. So your first JSON corresponds to something like List<Module>, it's not an object, but a list of them. Each module has a list of books in it.
So all said, it should be like this
interface SemdetailsFetcher {
#GET("test/json/sub1.json")
fun getCurrentSemData(): Call<List<SemdetailsPArser>>
}
By the way, if you define your POJOs right, you won't need all the annotations.
Create SemdetailsPArser class
data class SemdetailsPArser(
val books: List<Book>,
val module: String
)
Next create Book class
data class Book(
val authors: String,
val name: String
)
next in the interface (SemdetailsFetcher)
interface SemdetailsFetcher {
#GET("test/json/sub1.json")
fun getCurrentSemData(): Call<List<SemdetailsPArser>>
}

Kotlin Json Question Expected a string but was BEGIN_OBJECT at path

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.

How to properly make reusable class to catch response data using retrofit?

I am new in Android development, and I am trying to get data from server. the general JSON response structure will be like this
{
"success": "1",
"data": [
{
"customers_id": 4,
"customers_gender": "0",
"customers_firstname": "TES IOS",
"customers_lastname": "TES IOS",
"customers_dob": "2018-12-27",
"email": "TES002#email.com",
"user_name": "TES002",
"customers_default_address_id": 0,
"customers_telephone
},
"message": "Successfully get user data from server"
}
the "success" and "message" field will be the same (will always be string). but the "data" can be different for other request call. It can send user data, store data or product data, or even Array/List of Products.
so I want to make general reusable class to catch that JSON response. the class will be like this, I set the "data" to be Any, and then later it will be casted back to User object:
class ServerData(successStatus: Int, data: Any, message: String) {
val isSuccessfull : Boolean
val data : Any
val message : String
init {
isSuccessfull = successStatus != 0
this.data = data
this.message = message
}
}
the interface is like this:
interface LakuinAPI {
#FormUrlEncoded
#POST("processlogin")
fun performLogin(
#Field("kode_customer") outletCode: String,
#Field("password") password: String
): Call<ServerData>
}
and then I use it in the activity, like the code below:
private fun sendLoginDataToServer(outletCode: String, password: String) {
val call = lakuinAPI.performLogin(outletCode,password)
call.enqueue(object: Callback<ServerData> {
override fun onFailure(call: Call<ServerData>, t: Throwable) {
Toast.makeText(this#LoginActivity,t.localizedMessage,Toast.LENGTH_LONG).show()
}
override fun onResponse(call: Call<ServerData>, response: Response<ServerData>) {
if (!response.isSuccessful) {
Toast.makeText(this#LoginActivity,"Code: " + response.code(),Toast.LENGTH_LONG).show()
return
}
val lakuinServerData = response.body()
val userList = lakuinServerData?.data as List<User> // the error in here
val userData = userList.first() // the error in here
println(userData)
}
})
}
but I get error message:
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap
cannot be cast to com.ssss.lakuinkotlin.Model.User
I give comment in the code above the location of the error. I don't why it happened.
to be honest, I am not if this is the correct way to catch user data from general response JSON like the the JSON above. is there a better way ?
You can use generics to achieve it
class Response<Data> constructor() : ResponseSimple() {
#SerializedName(FIELD_DATA)
var data: Data? = null
private constructor(data: Data) : this() {
this.data = data
}
companion object {
const val FIELD_SUCCESS = "success"
const val FIELD_ERROR = "error"
const val FIELD_DATA = "data"
const val FIELD_MESSAGE = "message"
#JvmStatic
fun <Data> create(data: Data): Response<Data> {
return Response(data)
}
}
}
And ResponseSimple is
open class ResponseSimple {
#SerializedName(Response.FIELD_ERROR)
var error: String = ""
#SerializedName(Response.FIELD_SUCCESS)
var succes: Boolean = false
#SerializedName(Response.FIELD_MESSAGE)
var message:String = ""
}
Then api response should be Call<Response<ServerData>>.
And about ClassCastException, you can't convert ServerData to User just using as.
You need to use Call<Response<ArrayList<User>>> or create class converter.
Try replacing this line :
val userList = lakuinServerData?.data as List<User>
with:
val userList = lakuinServerData?.data as new TypeToken<List<User>>(){}.getType()

Categories

Resources