Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kotlin Serialization support does not take null-safety into account #33016

Closed
fkneier-bikeleasing opened this issue Jun 12, 2024 · 8 comments
Closed
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) theme: kotlin An issue related to Kotlin support type: bug A general bug
Milestone

Comments

@fkneier-bikeleasing
Copy link

The json deserialization is broken if kotlinx-serialization-json is in classpath. It seems that type nullability information is lost when using KotlinSerializationStringDecoder.

@RestController
@SpringBootApplication
open class Application {

    @PostMapping
    fun get(
        @RequestBody body: Map<String, String?>,
    ) = run {
        body
    }

}
POST http://localhost:8080
Content-Type: application/json

{
  "value": null
}
kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 13: Expected string literal but 'null' literal was found at path: $['value']
Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value.
JSON input: {
  "value": null
}
	at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	*__checkpoint ⇢ HTTP POST "/" [ExceptionHandlingWebHandler]
Original Stack Trace:
		at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
		at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:32) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
		at kotlinx.serialization.json.internal.AbstractJsonLexer.fail(AbstractJsonLexer.kt:580) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
		at kotlinx.serialization.json.internal.AbstractJsonLexer.unexpectedToken(AbstractJsonLexer.kt:221) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
		at kotlinx.serialization.json.internal.StringJsonLexer.consumeNextToken(StringJsonLexer.kt:76) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
		at kotlinx.serialization.json.internal.StringJsonLexer.consumeKeyString(StringJsonLexer.kt:88) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
		at kotlinx.serialization.json.internal.AbstractJsonLexer.consumeString(AbstractJsonLexer.kt:365) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
		at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeString(StreamingJsonDecoder.kt:339) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
		at kotlinx.serialization.internal.StringSerializer.deserialize(Primitives.kt:160) ~[kotlinx-serialization-core-jvm-1.7.0.jar:1.7.0]
		at kotlinx.serialization.internal.StringSerializer.deserialize(Primitives.kt:156) ~[kotlinx-serialization-core-jvm-1.7.0.jar:1.7.0]
		at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:69) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
		at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:43) ~[kotlinx-serialization-core-jvm-1.7.0.jar:1.7.0]
		at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableElement(AbstractDecoder.kt:70) ~[kotlinx-serialization-core-jvm-1.7.0.jar:1.7.0]
		at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableElement(StreamingJsonDecoder.kt:168) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
		at kotlinx.serialization.encoding.CompositeDecoder$DefaultImpls.decodeSerializableElement$default(Decoding.kt:538) ~[kotlinx-serialization-core-jvm-1.7.0.jar:1.7.0]
		at kotlinx.serialization.internal.MapLikeSerializer.readElement(CollectionSerializers.kt:111) ~[kotlinx-serialization-core-jvm-1.7.0.jar:1.7.0]
		at kotlinx.serialization.internal.MapLikeSerializer.readElement(CollectionSerializers.kt:84) ~[kotlinx-serialization-core-jvm-1.7.0.jar:1.7.0]
		at kotlinx.serialization.internal.AbstractCollectionSerializer.readElement$default(CollectionSerializers.kt:51) ~[kotlinx-serialization-core-jvm-1.7.0.jar:1.7.0]
		at kotlinx.serialization.internal.AbstractCollectionSerializer.merge(CollectionSerializers.kt:36) ~[kotlinx-serialization-core-jvm-1.7.0.jar:1.7.0]
		at kotlinx.serialization.internal.AbstractCollectionSerializer.deserialize(CollectionSerializers.kt:43) ~[kotlinx-serialization-core-jvm-1.7.0.jar:1.7.0]
		at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:69) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
		at kotlinx.serialization.json.Json.decodeFromString(Json.kt:165) ~[kotlinx-serialization-json-jvm-1.7.0.jar:na]
		at org.springframework.http.codec.KotlinSerializationStringDecoder.lambda$decodeToMono$2(KotlinSerializationStringDecoder.java:118) ~[spring-web-6.1.8.jar:6.1.8]
		at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:113) ~[reactor-core-3.6.6.jar:3.6.6]
		at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129) ~[reactor-core-3.6.6.jar:3.6.6]
		at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107) ~[reactor-core-3.6.6.jar:3.6.6]
		at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:299) ~[reactor-core-3.6.6.jar:3.6.6]
		at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337) ~[reactor-core-3.6.6.jar:3.6.6]
		at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2097) ~[reactor-core-3.6.6.jar:3.6.6]
		at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:145) ~[reactor-core-3.6.6.jar:3.6.6]
		at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144) ~[reactor-core-3.6.6.jar:3.6.6]
		at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260) ~[reactor-core-3.6.6.jar:3.6.6]
		at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144) ~[reactor-core-3.6.6.jar:3.6.6]
		at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:415) ~[reactor-netty-core-1.1.19.jar:1.1.19]
		at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:446) ~[reactor-netty-core-1.1.19.jar:1.1.19]
		at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:689) ~[reactor-netty-http-1.1.19.jar:1.1.19]
		at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:114) ~[reactor-netty-core-1.1.19.jar:1.1.19]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:286) ~[reactor-netty-http-1.1.19.jar:1.1.19]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346) ~[netty-codec-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318) ~[netty-codec-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:801) ~[netty-transport-classes-epoll-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:501) ~[netty-transport-classes-epoll-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:399) ~[netty-transport-classes-epoll-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.109.Final.jar:4.1.109.Final]
		at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.109.Final.jar:4.1.109.Final]
		at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 12, 2024
@bclozel bclozel added in: web Issues in web modules (web, webmvc, webflux, websocket) theme: kotlin An issue related to Kotlin support labels Jun 12, 2024
@fkneier-bikeleasing
Copy link
Author

This really is a major problem since it can not be disabled easily. But for now I found this hacky workaround:

    @Bean
    fun disableKotlinSerializationJsonCustomizer() = CodecCustomizer {
        if (ClassUtils.isPresent("kotlinx.serialization.json.Json", javaClass.classLoader)) {
            it.defaultCodecs().kotlinSerializationJsonDecoder(
                object : KotlinSerializationJsonDecoder() {
                    override fun canDecode(elementType: ResolvableType, mimeType: MimeType?): Boolean = false
                }
            )
            it.defaultCodecs().kotlinSerializationJsonEncoder(
                object : KotlinSerializationJsonEncoder() {
                    override fun canEncode(elementType: ResolvableType, mimeType: MimeType?): Boolean = false
                }
            )
        }
    }

@sdeleuze sdeleuze self-assigned this Jun 12, 2024
@sdeleuze
Copy link
Contributor

sdeleuze commented Jun 12, 2024

Kotlin Serialization does not work exactly like Jackson, so I suspect what you are looking for is to disable the default codec registration which on purpose uses Kotlin Serialization when it is on the classpath, and just enable the ones you want like Jackson and potentially a few others. If you need more help, Stack Overflow is probably a better place to ask, since we would like to keep the bug tracker focused on Spring Framework issues.

As a consequence, I close this issue as invalid. If you think otherwise, please describe the Spring Framework issue in detail. Just please take in account that automatic registration of Kotlin Serialization which takes precedence over Jackson when present on the classpath is the expected behavior and is not a bug.

@sdeleuze sdeleuze closed this as not planned Won't fix, can't repro, duplicate, stale Jun 12, 2024
@sdeleuze sdeleuze added status: invalid An issue that we don't feel is valid and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jun 12, 2024
@fkneier-bikeleasing
Copy link
Author

In certain cases Kotlin Serialization does not work at all. It is broken, which is clearly stated in the exception. I'm actually stunned that this ticket has been closed as invalid. This is not a matter of preference.

Either the type resolution has to be fixed (which DOES NOT correctly resolve nullability) or Kotlin Serialization must not be used in these cases (which DOES check nullability).

But currently a controller method with a valid body parameter has become unusable when Kotlin Serialization is used instead of Jackson, which is the default handling if Kotlin Serialization is found in the classpath.

@sdeleuze sdeleuze reopened this Jun 13, 2024
@sdeleuze sdeleuze added status: waiting-for-triage An issue we've not yet triaged or decided on and removed status: invalid An issue that we don't feel is valid labels Jun 13, 2024
@sdeleuze
Copy link
Contributor

sdeleuze commented Jun 13, 2024

Kotlin Serialization sometimes by design works differently (for example you have to annotate the classes with @Serializable) so I thought initially that was just another difference, but after another look it looks like indeed that we are indeed losing the null-safety information probably before ResolvableType don't have this information. A related hint could be pass via AbstractMessageReaderArgumentResolver#readBody(MethodParameter, MethodParameter, boolean, BindingContext, ServerWebExchange) but it is not obvious how to do that in a non intrusive way. I need to discuss that with the team.

@sdeleuze sdeleuze removed the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 13, 2024
@sdeleuze sdeleuze added this to the 6.1.10 milestone Jun 13, 2024
@sdeleuze
Copy link
Contributor

sdeleuze commented Jun 13, 2024

ResolvableType#getSource should allow to get the KType with the relevant null-safety information on WebFlux. For WebMVC, that's less obvious.

@snicoll snicoll added type: enhancement A general enhancement labels Jun 13, 2024
@sdeleuze sdeleuze modified the milestones: 6.1.10, 6.2.0-M5 Jun 13, 2024
@fkneier-bikeleasing
Copy link
Author

Isn't this considered a bug? Maybe even a serious one? I think it breaks existing code just because of the existence of a dependency and there even isn't an easy workaround.

@sdeleuze sdeleuze changed the title Kotlin Serialization Dependency breaks deserialization. Kotlin Serialization support does not take in account null-safety Jun 14, 2024
@sdeleuze sdeleuze added type: bug A general bug and removed type: enhancement A general enhancement labels Jun 14, 2024
@sdeleuze
Copy link
Contributor

If you do not want to use Kotlin Serialization, you can easily bring back Jackson as explain above.

@sbrannen sbrannen changed the title Kotlin Serialization support does not take in account null-safety Kotlin Serialization support does not take null-safety into account Jun 18, 2024
@sdeleuze
Copy link
Contributor

Depends on #33118.

sdeleuze added a commit to sdeleuze/spring-framework that referenced this issue Jul 1, 2024
In order to take in account properly Kotlin null-safety with the
annotation programming model.

See spring-projectsgh-33016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) theme: kotlin An issue related to Kotlin support type: bug A general bug
Projects
None yet
Development

No branches or pull requests

5 participants