Spring Reactive WebClient未遵循KotlinFeature.NullIsSameAsDefault问题
Kotlin数据类WebClient反序列化null值问题
问题现象
将Spring Boot应用重写为Kotlin后,使用响应式WebClient时,带默认值的数据类反序列化出现异常:
- 单独调用
objectMapper.readValue<List<EsportMatchVm>>(mockJsonWithNullValues)可正常解析含null值的JSON - 通过WebClient的
bodyToFlux<EsportMatchVm>解析时,name字段为null会触发非空错误;status字段通过@JsonSetter(nulls = Nulls.AS_EMPTY)解决了null问题,但name字段同样配置却不生效
相关代码
数据类定义
data class EsportMatchVm( val id: Long? = null, val name: String = "", @field:JsonProperty("begin_at") val beginAt: String? = null, @JsonSetter(nulls = Nulls.AS_EMPTY) val status: String = "", )
ObjectMapper配置类
@Configuration(proxyBeanMethods = false) class JsonCustomizerConfig { @Bean fun jsonCustomizer(): Jackson2ObjectMapperBuilderCustomizer = Jackson2ObjectMapperBuilderCustomizer { builder: Jackson2ObjectMapperBuilder -> builder.modulesToInstall( KotlinModule.Builder() // 替换现有KotlinModule .configure(KotlinFeature.NullIsSameAsDefault, true) .build() ) } }
WebClient客户端实现
@Service class EsportClient( private val webClient: WebClient, @Value("\${esport.token}") private val token: String ) { fun getMatches(matchType: MatchType): Flux<EsportMatchVm> = webClient .get() .uri("$PANDASCORE_BASE_URL${matchType.name.lowercase()}") .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) .header(HttpHeaders.AUTHORIZATION, "$BEARER_PREFIX$token") .retrieve() .bodyToFlux<EsportMatchVm>() companion object { private const val PANDASCORE_BASE_URL = "https://api.pandascore.co/csgo/matches/" private const val BEARER_PREFIX = "Bearer " } }
测试代码
@SpringBootTest class EsportClientTest( objectMapper: ObjectMapper, builder: WebClient.Builder ) : StringSpec({ val exchangeFunction: ExchangeFunction = mockk() val client = builder .exchangeFunction(exchangeFunction) .build() val sut = EsportClient(client, "~token~") "getMatches当匹配项含null值时应返回正确结果" { val mockJsonWithNullValues = """ [{ "id": null, "name": null, "begin_at": "", "status": null }]""".trimIndent() // 此方法正常工作 val parsed = objectMapper.readValue<List<EsportMatchVm>>(mockJsonWithNullValues) parsed.shouldNotBeNull() every { exchangeFunction.exchange(any()) } answers { Mono.just( createClientResponse(mockJsonWithNullValues) ) } val runningMatches = sut.getMatches(MatchType.RUNNING) val expectedMatch = EsportMatchVm( id = null, name = "", beginAt = "", status = "" ) StepVerifier.create(runningMatches) // 抛出org.springframework.core.codec.DecodingException: JSON解码错误 .expectNext(expectedMatch) .verifyComplete() }) { companion object { private fun createClientResponse(body: String) = ClientResponse.create(HttpStatus.OK) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .body(body) .build() } }
错误栈追踪
Suppressed: org.springframework.core.codec.DecodingException: JSON decoding error: Instantiation of [simple type, class no.vicx.backend.esport.vm.EsportMatchVm] value failed for JSON property name due to missing (therefore NULL) value for creator parameter name which is a non-nullable type at org.springframework.http.codec.json.AbstractJackson2Decoder.processException(AbstractJackson2Decoder.java:282) Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below:
环境信息
- Spring Boot版本:3.5.0
- Kotlin版本:2.1.21
- Jackson KotlinModule版本:2.19.1
- JDK:21
解决方案
核心原因
WebClient默认使用的Jackson解码器未加载自定义配置的ObjectMapper,导致KotlinModule的NullIsSameAsDefault配置和@JsonSetter注解在WebClient反序列化时不生效,而单独调用ObjectMapper时配置是正常的。
解决步骤
给WebClient配置自定义Jackson解码器
修改WebClient构建逻辑,传入配置好的ObjectMapper:@Configuration class WebClientConfig { @Bean fun webClient(objectMapper: ObjectMapper): WebClient { val jacksonDecoder = Jackson2JsonDecoder(objectMapper) return WebClient.builder() .codecs { configurer -> // 替换默认的Jackson解码器 configurer.defaultCodecs().jackson2JsonDecoder(jacksonDecoder) } .build() } }测试代码同步配置解码器
测试中手动构建WebClient时,也需传入自定义解码器,否则测试环境仍用默认配置:val client = builder .codecs { configurer -> configurer.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper)) } .exchangeFunction(exchangeFunction) .build()可选:给name字段添加@JsonSetter注解
确保name字段也能处理null值,和status字段保持一致:data class EsportMatchVm( val id: Long? = null, @JsonSetter(nulls = Nulls.AS_EMPTY) val name: String = "", @field:JsonProperty("begin_at") val beginAt: String? = null, @JsonSetter(nulls = Nulls.AS_EMPTY) val status: String = "", )
说明
通过上述配置,WebClient会使用与ObjectMapper一致的序列化规则,null值会被正确映射为数据类的默认值,解决反序列化时的非空错误。
内容的提问来源于stack exchange,提问作者Roar S.
相关产品推荐
相关产品推荐

