英文:
Custom JsonSerializer for sealed class not working
问题
I'm trying to write a custom JsonSerializer for a sealed class but the serialize method is not being called.
For simplicity, I created a sample sealed class
sealed class Parent {
data class Child1(val name: String): Parent()
data class Child2(val name: String): Parent()
}
and then created a JsonSerializer for this like this:
class ParentSerializer : JsonSerializer<Parent>, JsonDeserializer<Parent> {
companion object {
const val CLASSNAME = "CLASSNAME"
const val DATA = "DATA"
}
override fun serialize(src: Parent, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
val jsonObject = JsonObject()
jsonObject.addProperty(CLASSNAME, src.javaClass.name)
jsonObject.add(DATA, context.serialize(src))
return jsonObject
}
override fun deserialize(jsonElement: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Parent {
val jsonObject = jsonElement.asJsonObject
val className = jsonObject.get(CLASSNAME) as JsonPrimitive
val objectClass = Class.forName(className.asString)
return context.deserialize(jsonObject.get(DATA), objectClass)
}
}
and I tried serializing a list using this, but the serialize method is not called, but interestingly enough deserialize method is getting called (though it's currently erroring out that it's not able to find CLASSNAME, since serialize wasn't called first).
I created a test for this
@Test
fun check() {
val list = listOf(Parent.Child1("first"), Parent.Child2("second"))
val gson = GsonBuilder()
.registerTypeAdapter(Parent::class.java, ParentSerializer())
.create()
val jsonString = gson.toJson(list)
println(jsonString) // here serialize isn't called
val type = object : TypeToken<List<Parent>>() {}.type
val deserializedList = gson.fromJson<List<Parent>>(jsonString, type) // here deserialize is called correctly
assertThat(deserializedList).isEqualTo(list)
}
英文:
I'm trying to write a custom JsonSerializer for a sealed class but the serialize method is not being called.
For simplicity, I created a sample sealed class
sealed class Parent {
data class Child1(val name: String): Parent()
data class Child2(val name: String): Parent()
}
and then created a JsonSerializer for this like this:
class ParentSerializer : JsonSerializer<Parent>, JsonDeserializer<Parent> {
companion object {
const val CLASSNAME = "CLASSNAME"
const val DATA = "DATA"
}
override fun serialize(src: Parent, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
val jsonObject = JsonObject()
jsonObject.addProperty(CLASSNAME, src.javaClass.name)
jsonObject.add(DATA, context.serialize(src))
return jsonObject
}
override fun deserialize(jsonElement: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Parent {
val jsonObject = jsonElement.asJsonObject
val className = jsonObject.get(CLASSNAME) as JsonPrimitive
val objectClass = Class.forName(className.asString)
return context.deserialize(jsonObject.get(DATA), objectClass)
}
}
and I tried serializing a list using this, but the serialize method is not called, but interestingly enough deserialize method is getting called(though it's currently erroring out that it's not able to find CLASSNAME, since serialize wasn't called first).
I created a test for this
@Test
fun check() {
val list = listOf(Parent.Child1("first"), Parent.Child2("second"))
val gson = GsonBuilder()
.registerTypeAdapter(Parent::class.java, ParentSerializer())
.create()
val jsonString = gson.toJson(list)
println(jsonString) // here serialize isn't called
val type = object : TypeToken<List<Parent>>() {}.type
val deserializedList = gson.fromJson<List<Parent>>(jsonString, type) // here deserialize is called correctly
assertThat(deserializedList).isEqualTo(list)
}
答案1
得分: 1
这里有两个事情是相互关联的:
Gson.toJson
方法在没有显式类型参数的情况下使用参数的运行时类型,所以在这里是List<?>
,因此对Child1
和Child2
进行序列化,而不是Parent
。GsonBuilder.registerTypeAdapter
为特定类注册了适配器,但不包括其子类。
在调用 toJson
时,你可能可以为 List<Parent>
添加一个额外的类型参数(例如 toJson(list, type)
),或者使用 GsonBuilder.registerTypeHierarchyAdapter
而不是 registerTypeAdapter
。但是,使用 registerTypeHierarchyAdapter
对你的代码不会直接起作用,因为它调用了 context.serialize(src)
,这会导致无限递归,这是方法 JsonSerializer.serialize
警告的情况。你需要使用 TypeAdapterFactory
而不是 JsonSerializer
来解决这个问题。
然而,你的代码存在一个主要的安全问题:通过使用 Class.forName
,你实际上允许提供 JSON 数据的用户从你的类路径加载任意类,然后通过 Gson 调用其构造函数并为其设置任意字段值。在最坏的情况下,这可能导致远程代码执行漏洞。
因为你知道要加载的类是 Parent
的子类,所以在使用 Class.forName
时,你的代码应该明确强制执行,例如像这样:
val objectClass = Class.forName(className.asString, false, this.javaClass.classLoader)
.asSubclass(Parent::class.java)
这不会直接初始化请求的类(false
参数),并使用 asSubclass
确保该类实际上是 Parent
或其子类,然后才执行其他操作。
但是,似乎你尝试解决的一般问题是多态反序列化。已经有多个关于这个问题的答案(例如 这个),你可能会在那里找到其他解决方案的想法。
作为附注:自从 Gson 2.10 版本以后,有带有 TypeToken
参数的 Gson.fromJson
重载,例如 fromJson(String, TypeToken)
。你应该优先使用这些重载,而不是你使用的带有 Type
参数的 fromJson(..., Type)
重载,因为它们提供了类型安全性。
英文:
There are two things which coincide here:
- The
Gson.toJson
method without explicit type argument uses the runtime type of the argument, soList<?>
here, and therefore performs serialization forChild1
respectivelyChild2
and not forParent
GsonBuilder.registerTypeAdapter
registers an adapter for that specific class, but not subclasses
You could probably either add an additional type argument for List<Parent>
when calling toJson
(e.g. toJson(list, type)
), or use GsonBuilder.registerTypeHierarchyAdapter
instead of registerTypeAdapter
. However, using registerTypeHierarchyAdapter
won't directly work for your code because it calls context.serialize(src)
which leads to infinite recursion, something the method JsonSerializer.serialize
warns against. You would have to use a TypeAdapterFactory
instead of JsonSerializer
to solve this.
However, there is a major security issue with your code: By using Class.forName
you essentially let the user who provides the JSON data load an arbitrary class from your classpath and then through Gson call its constructor and set arbitrary field values for it. In the worst case this can lead to a remote code execution vulnerability.
Since you know that the class you want to load is a subclass of Parent
, your code should explicitly enforce that when using Class.forName
, for example like this:
val objectClass = Class.forName(className.asString, false, this.javaClass.classLoader)
.asSubclass(Parent::class.java)
This does not directly initialize the requested class (false
argument) and with asSubclass
makes sure the class is actually Parent
or a subclass of it before doing anything else with it.
However, it appears the general problem you are trying to solve is polymorphic deserialization. There are already multiple answers about this (such as this one) where you might find ideas for other solutions.
As side note: Since Gson 2.10 there are Gson.fromJson
overloads with TypeToken
parameter, for example fromJson(String, TypeToken)
. You should prefer those over the fromJson(..., Type)
overload you used because they provide type safety.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论