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
itemTypeString to correspondingTypeAdapter, obtained from theGsoninstance (in the following called "itemTypemap") - Get the adapter for
JsonObjectfrom theGsoninstance (in the following called "JsonObject adapter") - Get a delegate adapter for
ParentItemfrom theGsoninstance (in the following called "ParentItem adapter") (a delegate adapter is needed because otherwise Gson would simply use the currentParentItemfactory, resulting in infinite recursion) - Create and return an adapter which does the following:
- Use the JsonObject adapter to read from the reader
- Remove the
childPropertyvalue from the parsed JsonObject and store it in a variablechildPropertyValue - Remove the
itemTypevalue and get the corresponding TypeAdapter from theitemTypemap (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
childPropertyValueand store its result in thechildPropertyof the previously read ParentItem object (this requires makingParentItem.childPropertyavar) - 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
