Android Retrofit2 搭配kotlinx.serialization不依赖GSON序列化null值方案
根因分析
你遇到的异常是因为kotlinx.serialization默认不支持两种场景:
- 顶层JSON值直接为
null - 响应体完全为空(EOF)时尝试反序列化,哪怕你声明了类型为可空,默认的Converter也不会自动把空body映射为
null
注意:你之前注释的捕获异常匹配错误字符串的方案非常脆弱,序列化库版本更新或者错误文案调整就会失效,不建议在生产环境使用
可行方案
方案一:全局适配(一劳永逸,适合多个接口存在类似场景)
无需修改现有业务代码,只需调整序列化配置和Converter,全局支持可空响应:
- 调整全局Json实例配置,开启宽松解析规则:
@Provides @ExperimentalSerializationApi fun provideJson(): Json { return Json { ignoreUnknownKeys = true // 忽略后端返回的未定义字段 isLenient = true // 宽松解析,兼容不规范的JSON格式 coerceInputValues = true // 类型不匹配时用属性默认值填充,避免直接崩溃 } }
- 自定义Converter.Factory,自动将空响应、顶层null转为
null:
@ExperimentalSerializationApi class NullableSerializationConverterFactory( private val delegate: Converter.Factory ) : Converter.Factory() { override fun responseBodyConverter( type: Type, annotations: Array<out Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { val delegateConverter = delegate.responseBodyConverter(type, annotations, retrofit) return Converter<ResponseBody, Any?> { body -> if (body.contentLength() == 0L) { null } else { body.use { try { delegateConverter?.convert(it) } catch (e: JsonDecodingException) { if (e.message?.contains("JSON input: null") == true) null else throw e } } } } } }
- 替换原来的ConverterFactory注入逻辑:
@Provides @ExperimentalSerializationApi fun provideConverterFactory(json: Json): Converter.Factory { val mediaType = "application/json".toMediaType() val originalFactory = json.asConverterFactory(mediaType) return NullableSerializationConverterFactory(originalFactory) }
配置完成后原有业务代码完全不需要改动,接口声明Response<SupportConfigurationJson?>就可以正常拿到null值。
方案二:单独适配当前接口(侵入性低,适合仅少数接口为该场景)
不需要改动全局配置,只调整当前接口相关代码:
- 修改Api接口返回类型为原始响应体:
interface SupportConfigurationApi { @GET("/mobile_api/v1/support/configuration") suspend fun getSupportConfiguration(): Response<ResponseBody?> }
- 调整Repository层的解析逻辑:
@Singleton class SupportConfigurationRepository @Inject constructor( private val api: SupportConfigurationApi, private val jsonMapper: SupportConfigurationJsonMapper, private val json: Json // 注入全局Json实例 ) { suspend fun getSupportConfiguration(): SupportConfiguration? = mapJsonToSupportConfiguration(api.getSupportConfiguration().extractOrThrow()) private suspend fun mapJsonToSupportConfiguration( body: ResponseBody? ) = withContext(Dispatchers.Default) { val jsonStr = body?.string()?.takeIf { it.isNotBlank() && it != "null" } val configJson = jsonStr?.let { json.decodeFromString<SupportConfigurationJson>(it) } jsonMapper.mapToSupportSettings(configJson) } }
该方案改动范围小,不会影响其他接口的序列化逻辑,风险更低。
内容的提问来源于stack exchange,提问作者gabez_rules
相关产品推荐
相关产品推荐

