I'm trying to deserialize a json response that contains objects which have child objects that can change type based on a property in the parent class. I've seen examples of how to use the type adapter factory for deserializing a child when it's own property type is defined, but cannot figure out how to do it where the defining type is in the parent object. Is this possible?
Example JSON
{
"items": [
{
"someProperty": "here",
"anotherProperty": "there",
"childProperty": {
"foo": "This property will be here if itemType is 'foo'"
"abc": "def"
},
"itemType": "foo",
},
{
"someProperty": "here",
"anotherProperty": "there",
"childProperty": {
"bar": "This property will be here if itemType is 'bar'"
"ghi": "jkl"
},
"itemType": "bar",
}
],
"limit": 25,
"nextCursor": null
}
In the above example, the childPropertyThatChanges
should get deserialized to a different type depending on the value of itemType
.
Given the classes for serialization below:
data class FooBarWrapper(
val items: List<ParentItem>,
val limit: Int,
val nextCursor: String?
) : Serializable
data class ParentItem(
val someProperty: String,
val anotherProperty: String,
val childProperty: ChildProperty
)
open class ChildProperty
data class ChildPropertyFoo(
val foo: String,
val abc: String
) : ChildProperty()
data class ChildPropertyBar(
val bar: String,
val ghi: String
) : ChildProperty()
And the type adapters as:
val exampleTypeAdapter = RuntimeTypeAdapterFactory
.of(ChildProperty::class.java, "itemType")
.registerSubtype(ChildPropertyFoo::class.java, "foo")
.registerSubtype(ChildPropertyBar::class.java, "bar")
val exampleGson = GsonBuilder()
.registerTypeAdapterFactory(exampleTypeAdapter)
.create()
val deserialized = exampleGson.fromJson(exampleJson, FooBarWrapper::class.java)
In the above example, the childProperty
is never deserialized - it remains null since it cannot infer the type because the itemType
lives in the parent object.
If I however change the json schema to the below where the itemType is inside the child object, everything deserializes fine.
{
"items": [{
"someProperty": "here",
"anotherProperty": "there",
"childPropertyThatChanges": {
"foo": "here when itemType is foo",
"abc": "def",
"itemType": "foo"
}
},
{
"someProperty": "here",
"anotherProperty": "there",
"childPropertyThatChanges": {
"bar": "here when itemType is bar",
"ghi": "jkl",
"itemType": "bar"
}
}
],
"limit": 25,
"nextCursor": null
}
I can't change the json that I'm receiving, so I'm trying to figure out how to create the type adapter so that it works with the type being defined in the parent vs the child object.
CodePudding user response:
With Gson you could possibly solve this by implementing a custom TypeAdapterFactory
which does the following:
- Verify that the requested type is
ParentItem
- Create a map from
itemType
String to correspondingTypeAdapter
, obtained from theGson
instance (in the following called "itemType
map") - Get the adapter for
JsonObject
from theGson
instance (in the following called "JsonObject adapter") - Get a delegate adapter for
ParentItem
from theGson
instance (in the following called "ParentItem adapter") (a delegate adapter is needed because otherwise Gson would simply use the currentParentItem
factory, resulting in infinite recursion) - Create and return an adapter which does the following:
- Use the JsonObject adapter to read from the reader
- Remove the
childProperty
value from the parsed JsonObject and store it in a variablechildPropertyValue
- Remove the
itemType
value and get the corresponding TypeAdapter from theitemType
map (in the following called "child adapter") - Use the ParentItem adapter on the parsed JsonObject (without the
childPropertyValue
; Gson will not complain about the missing property) - Use the child adapter on
childPropertyValue
and store its result in thechildProperty
of the previously read ParentItem object (this requires makingParentItem.childProperty
avar
) - Return the ParentItem object
Then you only need to register that TypeAdapterFactory
with a GsonBuilder
(and optionally any custom adapters for ChildPropertyFoo
or ChildPropertyBar
).
Here is a sample implementation of the TypeAdapterFactory
:
object ParentItemTypeAdapterFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
// Only support ParentItem and subtypes
if (!ParentItem::class.java.isAssignableFrom(type.rawType)) {
return null
}
// Safe cast due to check at beginning of function
@Suppress("UNCHECKED_CAST")
val delegateAdapter = gson.getDelegateAdapter(this, type) as TypeAdapter<ParentItem>
val jsonObjectAdapter = gson.getAdapter(JsonObject::class.java)
val itemTypeMap = mapOf(
"foo" to gson.getAdapter(ChildPropertyFoo::class.java),
"bar" to gson.getAdapter(ChildPropertyBar::class.java),
)
// Safe cast due to check at beginning of function
@Suppress("UNCHECKED_CAST")
return object : TypeAdapter<ParentItem>() {
override fun read(reader: JsonReader): ParentItem? {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return null
}
val parentItemValue = jsonObjectAdapter.read(reader)
val itemType = parentItemValue.remove("itemType").asString
val childAdapter = itemTypeMap[itemType]
?: throw JsonParseException("Invalid item type: $itemType")
val childPropertyValue = parentItemValue.remove("childProperty")
val itemObject = delegateAdapter.fromJsonTree(parentItemValue)
val childObject = childAdapter.fromJsonTree(childPropertyValue)
itemObject.childProperty = childObject
return itemObject
}
override fun write(writer: JsonWriter, value: ParentItem?) {
throw UnsupportedOperationException()
}
} as TypeAdapter<T>
}
}
Note that other JSON frameworks provide this functionality out of the box, for example Jackson has JsonTypeInfo.As.EXTERNAL_PROPERTY
.
CodePudding user response:
One way would be to create a type adapter for the ParentItem
class and within the JsonDeserializer
subclass based on the value of the itemType
property deserialize the child object with the correct class (ChildPropertyFoo
or ChildPropertyBar
). Then you can simply assign the deserialized object to the ChildProperty
property. However, this would require childProperty
to be changed to var in ParentItem
since it needs to be reassigned. Alternatively, one could construct a complete ParentItem
.
The code might look something like this:
import com.google.gson.*
import java.lang.reflect.Type
internal class ItemDeserializer : JsonDeserializer<ParentItem> {
override fun deserialize(
json: JsonElement,
t: Type,
jsonDeserializationContext: JsonDeserializationContext
)
: ParentItem? {
val type = (json as JsonObject)["itemType"].asString
val gson = Gson()
val childJson = json["childProperty"]
val childClass = if (type == "foo") ChildPropertyFoo::class.java else ChildPropertyBar::class.java
val childObject = gson.fromJson<ChildProperty>(childJson, childClass)
val parent = gson.fromJson(json, ParentItem::class.java) as ParentItem
parent.childProperty = childObject
return parent
}
}
The whole thing could of course be generalized by injecting the details like itemType
, childProperty
etc. into the ItemDeserializer
instance, but I rather wanted to show the basic approach.
Anyway, to get a self-contained example for a quick test the call is still missing, which could look like this:
import com.google.gson.GsonBuilder
fun main() {
val deserializer = ItemDeserializer()
val gson = GsonBuilder().registerTypeAdapter(ParentItem::class.java, deserializer).create()
val deserializedTest = gson.fromJson(json, FooBarWrapper::class.java)
for (item in deserializedTest.items) {
when (val childProperty = item.childProperty) {
is ChildPropertyFoo -> {
println(childProperty.foo)
println(childProperty.abc)
}
is ChildPropertyBar -> {
println(childProperty.bar)
println(childProperty.ghi)
}
}
}
}
The debug console will then output the following, you can see that the deserialization code gives the desired result:
This property will be here if itemType is 'foo'
def
This property will be here if itemType is 'bar'
jkl