Jackson-databind中Kotlin泛型列表反序列化异常问题
我在应用中使用jackson-databind将Kotlin对象序列化为JSON,通过HTTP传输后在另一端反序列化为Kotlin对象,但特定场景下列表反序列化出现异常:deserializedResponse.result被反序列化为ArrayList<Map<String, String>>,原本的TotalStationPoint对象变成了属性映射。我希望它能还原为List<TotalStationPoint>,想先了解Jackson的惯用解决方式,再考虑自定义方案。
相关代码
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "type") interface ServerInteraction data class GetJobResultResponse<T>( @JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.CLASS) val result: T? ) : ServerInteraction data class TotalStationPoint(val pointName: String, val location: Point3, val timestamp: Instant) class LoadTotalStationDataSerializationTest { @Test fun loadTotalStationDataIsSerializedCorrectly() { val result: List<TotalStationPoint> = loadTotalStationData() val getJobResultResponse: GetJobResultResponse<List<TotalStationPoint>> = GetJobResultResponse(result) val serializedResponse: String = JsonSerializer.serialize(getJobResultResponse) /* Serializes to: { "type": "de.uni_freiburg.inatech.streem.image_converter.batch_job_executor.common.responses.GetJobResultResponse", "result": { "java.util.ArrayList": [ { "pointName": "V1_22_0000000956", "location": { "x": 10012.21984, "y": 10002.558907, "z": 2.342047000000001 }, "timestamp": 1670328663.04 }, { "pointName": "V1_22_0000000955", "location": { "x": 10012.219816, "y": 10002.559031, "z": 2.3418279999999996 }, "timestamp": 1670328662.91 } // ... ] } } */ val deserializedResponse: GetJobResultResponse<*> = JsonSerializer.deserialize<GetJobResultResponse<*>>(serializedResponse) assertTrue((deserializedResponse.result as List<*>).first() is TotalStationPoint) assertEquals(getJobResultResponse, deserializedResponse) } private fun loadTotalStationData(): List<TotalStationPoint> { // Loads and parses a CSV file to List<TotalStationData> // ... } }
惯用解决办法
1. 为列表元素添加类型标识
当前@JsonTypeInfo仅标记了外层容器(java.util.ArrayList)的类型,但没有为列表内的TotalStationPoint元素提供类型信息。给TotalStationPoint添加注解,让Jackson序列化时写入类型标识:
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "type") data class TotalStationPoint(val pointName: String, val location: Point3, val timestamp: Instant)
这样序列化后的每个元素都会带上type字段,反序列化时Jackson就能识别出元素的具体类型。
2. 用TypeReference明确泛型类型
反序列化时,避免使用通配符GetJobResultResponse<*>,改用TypeReference指定完整的泛型参数类型:
val deserializedResponse = JsonSerializer.deserialize( serializedResponse, object : TypeReference<GetJobResultResponse<List<TotalStationPoint>>>() {} )
这种方式能让Jackson精准知晓result字段对应的是List<TotalStationPoint>,而非模糊的泛型类型。
3. 启用泛型类型ID特性
启用Jackson的ENABLE_USE_OF_GENERIC_TYPE_IDS特性,让序列化时写入完整的泛型参数类型信息:
val mapper = ObjectMapper().registerModule(KotlinModule()) mapper.enable(MapperFeature.ENABLE_USE_OF_GENERIC_TYPE_IDS)
同时调整GetJobResultResponse的注解配置:
data class GetJobResultResponse<T>( @JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.CLASS, visible = true) val result: T? ) : ServerInteraction
该特性会让Jackson在处理泛型类型时,把完整的泛型参数信息写入JSON,反序列化时即可准确还原。
4. 用@JsonSubTypes声明子类(适用于有父类场景)
如果TotalStationPoint是某个父类的子类,可在父类中通过@JsonSubTypes声明所有可能的子类:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonSubTypes( JsonSubTypes.Type(value = TotalStationPoint::class, name = "totalStationPoint") ) interface PointData data class TotalStationPoint(val pointName: String, val location: Point3, val timestamp: Instant) : PointData
之后将GetJobResultResponse的泛型参数改为PointData相关类型,Jackson就能根据type字段自动匹配对应的子类类型。
内容的提问来源于stack exchange,提问作者vatbub

