Gson根据父属性反序列化子对象

发布于 2025-01-10 10:42:11 字数 2738 浏览 0 评论 0原文

我正在尝试反序列化一个 json 响应,该响应包含具有子对象的对象,这些子对象可以根据父类中的属性更改类型。我已经看到了如何使用类型适配器工厂在定义子对象自己的属性类型时反序列化子对象的示例,但无法弄清楚如何在父对象中定义类型的位置执行此操作。这可能吗?

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
}

在上面的示例中,childPropertyThatChanges 应根据 itemType 的值反序列化为不同的类型。


给定下面的序列化类:

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

类型适配器为:

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)

在上面的示例中,childProperty 永远不会反序列化 - 它保持为 null,因为它无法推断类型,因为 itemType存在于父对象中。

但是,如果我将 json 架构更改为下面的 itemType 位于子对象内的内容,则一切都可以正常反序列化。

{
    "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
}

我无法更改收到的 json,因此我试图弄清楚如何创建类型适配器,以便它能够与父对象和子对象中定义的类型一起使用。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

梦里泪两行 2025-01-17 10:42:11

使用 Gson,您可以通过实现自定义 TypeAdapterFactory 执行以下操作:

  1. 验证请求的类型是否为 ParentItem
  2. 创建地图itemType String 到对应的TypeAdapter,从Gson实例获取(下文中称为“itemType映射”)
  3. 获取来自 Gson 实例的 JsonObject 适配器(在下文中称为“JsonObject 适配器”)
  4. 获取一个 ParentItem 代理适配器 Gson 实例(在下文中称为“ParentItem 适配器”)(需要一个委托适配器,否则 Gson 将仅使用当前的 ParentItem 工厂,导致无限递归)
  5. 创建并返回一个执行以下操作的适配器:
    1. 使用 JsonObject 适配器从阅读器读取
    2. 从解析的 JsonObject 中删除 childProperty 值并将其存储在变量 childPropertyValue
    3. 移除itemType值并从itemType映射中获取对应的TypeAdapter(以下称为“子适配器”)
    4. 在解析的 JsonObject 上使用 ParentItem 适配器(不带 childPropertyValue;Gson 不会抱怨缺少属性)
    5. childPropertyValue 上使用子适配器,并将其结果存储在先前读取的 ParentItem 对象的 childProperty 中(这需要创建 ParentItem.childProperty一个var)
    6. 返回 ParentItem 对象

然后只需要使用 GsonBuilder 注册 TypeAdapterFactory (以及可选的任何自定义适配器ChildPropertyFooChildPropertyBar)。

以下是 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>
    }
}

请注意,其他 JSON 框架提供了开箱即用的此功能,例如 Jackson 有 JsonTypeInfo.As.EXTERNAL_PROPERTY

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.

纵情客 2025-01-17 10:42:11

一种方法是为 ParentItem 类创建一个类型适配器,并在 JsonDeserializer 子类中根据 itemType 属性的值反序列化子类具有正确类的对象(ChildPropertyFooChildPropertyBar)。然后,您只需将反序列化的对象分配给 ChildProperty 属性即可。但是,这需要将 childProperty 更改为 ParentItem 中的 var,因为需要重新分配它。或者,可以构造一个完整的 ParentItem

代码可能看起来像这样:

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
    }

}

当然可以通过将 itemTypechildProperty 等细节注入到 ItemDeserializer 中来概括整个事情。例如,但我更想展示基本方法。

无论如何,要获得一个用于快速测试的独立示例,调用仍然丢失,可能如下所示:

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

然后调试控制台将输出以下内容,您可以看到反序列化代码给出了所需的结果:

This property will be here if itemType is 'foo'
def
This property will be here if itemType is 'bar'
jkl

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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文