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()