RPC with Kotlinx Serialization
因为将项目从Retrofit切换到Ktor和gson切换到kotlinx serialize。而kotlinx serialize和gson的实现差异导致在rpc接口中对于rpc 请求参数类型不一致的情况下,序列化出现了异常报错,比如类似一下的请求Body
{
"id": 0,
"jsonrpc": "2.0",
"method": "eth_call",
"params": [
{
"data": "0x5ec88c7900000000000000000000000081080a7e991bcdddba8c2302a70f45d6bd369ab5",
"from": "0x81080a7e991bcDdDBA8C2302A70f45d6Bd369Ab5",
"to": "0xb3831584acb95ED9cCb0C11f677B5AD01DeaeEc0"
},
"latest"
]
}
List<Any>
+ @Contextual
本来以为在Kotlinx Serialize上会像Gson一样轻松解决,一开始便对于原来的数据结构定义成
@Serializable
data class TestBaseRpc(
val jsonrpc: String,
val method: String,
val params: List<@Contextual Any>,
val id: Int
)
并在Json 配置的时候将 Contextual配置上
Json {
useArrayPolymorphism = true
prettyPrint = true
allowStructuredMapKeys = true
serializersModule = SerializersModule {
contextual(Any::class) {
TestCaller.serializer()
String.serializer()
}
}
}
将单元测试代码跑起来后发现,contextual 只使用了最后一个serializer来解析Any类型,所以这个方向无法实现想要的结果
报错如下:
***.TestCaller cannot be cast to class java.lang.String
Note: 如果将两个serializer调换顺序就会发现报错也会两个类型互换
通过 Sealed Interface + Polymorphic
polymorphic多态的意思,顾名思义,可以根据实际情况进行选择对应的Serializer,所以需要有一个基类(可以interface,sealed…)
- 首先定义一个interface
internal interface Parameter
- 定义subclass
internal sealed interface Parameter { @Serializable data class CallParameter( val data: String, val from: String, val to: String ) : Parameter @Serializable(with = IntListParameterSerializer::class) data class IntListParameter( val items: List<Int> ) : Parameter @Serializable(with = StringParameterSerializer::class) data class StringParameter( val content: String ) : Parameter ... }
- 配置Json模块,绑定subclass的Serializer
Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true allowStructuredMapKeys = true serializersModule = SerializersModule { polymorphic(List::class) { ListSerializer(ParameterSerialize) } polymorphic(Parameter::class) { subclass(Parameter.CallParameter::class, Parameter.CallParameter.serializer()) subclass(Parameter.StringParameter::class, Parameter.StringParameter.serializer()) subclass(Parameter.IntListParameter::class, Parameter.IntListParameter.serializer()) } } }
- 针对不同结构定义不同的Serializer
internal object IntListParameterSerializer : KSerializer<Parameter.IntListParameter> { override fun deserialize(decoder: Decoder): Parameter.IntListParameter { val items = decoder.decodeSerializableValue(ListSerializer(Int.serializer())) return Parameter.IntListParameter( items = items ) } override val descriptor: SerialDescriptor get() = buildClassSerialDescriptor("IntListParameter") { this.element<List<Int>>("items") } override fun serialize(encoder: Encoder, value: Parameter.IntListParameter) { encoder.encodeSerializableValue(ListSerializer(Int.serializer()), value.items) } } internal object RpcRequestBodySerializer : KSerializer<RpcRequestBody> { override fun deserialize(decoder: Decoder): RpcRequestBody { return decoder.decodeStructure(descriptor) { var rpc: String? = null var method: String? = null var params: List<Parameter> = emptyList() var id: Int? = null loop@ while (true) { when (decodeElementIndex(descriptor)) { CompositeDecoder.DECODE_DONE -> break@loop 0 -> rpc = decodeStringElement(descriptor, 0) 1 -> method = decodeStringElement(descriptor, 1) 2 -> params = decodeSerializableElement(descriptor, 2, ListSerializer(ParameterSerialize)) 3 -> id = decodeIntElement(descriptor, 3) } } RpcRequestBody( jsonrpc = rpc ?: "", method = method ?: "", params = params, id = id ?: 1 ) } } override val descriptor: SerialDescriptor get() = buildClassSerialDescriptor("RpcRequestBody") { this.element<String>("jsonrpc", isOptional = false) this.element<String>("method", isOptional = false) this.element<List<Parameter>>("params", isOptional = false) this.element<Int>("id", isOptional = false) } override fun serialize(encoder: Encoder, value: RpcRequestBody) { encoder.encodeStructure(descriptor) { encodeStringElement(descriptor, 0, value.jsonrpc) encodeStringElement(descriptor, 1, value.method) encodeSerializableElement(descriptor, 2, ListSerializer(ParameterSerialize), value.params) encodeIntElement(descriptor, 3, value.id) } } } ...
- 如果需要deserialize,便需要加上如下选择器
运行单元测试,验证通过:internal object ParameterSerialize : JsonContentPolymorphicSerializer<Parameter>(Parameter::class) { override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Parameter> { // override the condition for your point return when(element) { is JsonPrimitive -> Parameter.StringParameter.serializer() is JsonArray -> Parameter.IntListParameter.serializer() is JsonObject -> Parameter.CallParameter.serializer() } } }
最后附上仓库链接:
Kotlinx-Serialization-Example
总结
只是为了在项目中减少gson的使用,所以花了点时间验证下具体情况,但是:
虽然目前看起来是解决了问题,不过相对来说比较麻烦,并且如果类型越来越多也会增加不必要的工作量,所以笔者觉得如果你的项目接口也有类似的情况,gson或许是个不错的选择