Home > Enterprise >  Gson deserialize child objects based on parent property
Gson deserialize child objects based on parent property

Time:03-01

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:

  1. Verify that the requested type is ParentItem
  2. Create a map from itemType String to corresponding TypeAdapter, obtained from the Gson instance (in the following called "itemType map")
  3. Get the adapter for JsonObject from the Gson instance (in the following called "JsonObject adapter")
  4. Get a delegate adapter for ParentItem from the Gson instance (in the following called "ParentItem adapter") (a delegate adapter is needed because otherwise Gson would simply use the current ParentItem factory, resulting in infinite recursion)
  5. Create and return an adapter which does the following:
    1. Use the JsonObject adapter to read from the reader
    2. Remove the childProperty value from the parsed JsonObject and store it in a variable childPropertyValue
    3. Remove the itemType value and get the corresponding TypeAdapter from the itemType map (in the following called "child adapter")
    4. Use the ParentItem adapter on the parsed JsonObject (without the childPropertyValue; Gson will not complain about the missing property)
    5. Use the child adapter on childPropertyValue and store its result in the childProperty of the previously read ParentItem object (this requires making ParentItem.childProperty a var)
    6. 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
  • Related