Reputation: 13843
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
Upvotes: 2
Views: 1078
Reputation: 2218
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.
Upvotes: 1