自定义密封类的JsonSerializer不起作用

huangapple go评论74阅读模式
英文:

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&lt;Parent&gt;, JsonDeserializer&lt;Parent&gt; {
  companion object {
    const val CLASSNAME = &quot;CLASSNAME&quot;
    const val DATA  = &quot;DATA&quot;
  }

  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(&quot;first&quot;), Parent.Child2(&quot;second&quot;))
    
    val gson = GsonBuilder()
        .registerTypeAdapter(Parent::class.java, ParentSerializer())
        .create()

    val jsonString = gson.toJson(list)
    println(jsonString) // here serialize isn&#39;t called

    val type = object : TypeToken&lt;List&lt;Parent&gt;&gt;() {}.type
    val deserializedList = gson.fromJson&lt;List&lt;Parent&gt;&gt;(jsonString, type) // here deserialize is called correctly
    assertThat(deserializedList).isEqualTo(list) 
  }

答案1

得分: 1

这里有两个事情是相互关联的:

  • Gson.toJson 方法在没有显式类型参数的情况下使用参数的运行时类型,所以在这里是 List&lt;?&gt;,因此对 Child1Child2 进行序列化,而不是 Parent
  • GsonBuilder.registerTypeAdapter 为特定类注册了适配器,但不包括其子类。

在调用 toJson 时,你可能可以为 List&lt;Parent&gt; 添加一个额外的类型参数(例如 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, so List&lt;?&gt; here, and therefore performs serialization for Child1 respectively Child2 and not for Parent
  • GsonBuilder.registerTypeAdapter registers an adapter for that specific class, but not subclasses

You could probably either add an additional type argument for List&lt;Parent&gt; 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.

huangapple
  • 本文由 发表于 2023年5月25日 03:05:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/76326686.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定