英文:
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.toJsonmethod without explicit type argument uses the runtime type of the argument, soList<?>here, and therefore performs serialization forChild1respectivelyChild2and not forParent GsonBuilder.registerTypeAdapterregisters 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论