Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Pitfall of Kotlin's Null Safety

Avatar for Yasuhisa Honda Yasuhisa Honda
November 13, 2025
3

The Pitfall of Kotlin's Null Safety

Avatar for Yasuhisa Honda

Yasuhisa Honda

November 13, 2025
Tweet

Transcript

  1. The Pitfall of Kotlin’s Null Safety Kotlin - Java Interoperability

    Honda Yasuhisa @hondaya14 Server-Side Kotlin LT in LY
  2. Null’s History • Antony Hoare • Quick Sort • CSP:

    Communicating Sequential Process • e.g. Go: goroutine + channel, Kotlin: coroutine + channel • 1960s, ALGOL • Null Pointer • 2009 QCon • Null References: The Billion Dollar Mistake
  3. Case Study: API request null bypasses Kotlin’s type safety data

    class XXXXXCreateRequest( @get:Size(min=1,max=20) @Schema(required = true) @get:JsonProperty("shopIds", required = true) val shopIds: kotlin.collections.List<kotlin.Long>, @get:Min(0) @get:Max(100) @Schema(required = true) @get:JsonProperty("commissionRate", required = true) val commissionRate: kotlin.Int, @get:Min(0L) @get:Max(10000000L) @Schema(required = true) @get:JsonProperty("budget", required = true) val budget: kotlin.Long, ) @Validated interface XXXXXApi { @RequestMapping( method = [RequestMethod.POST], value = [“/v1/path/to/api”], produces = ["application/json"], consumes = ["application/json"] ) fun xxxxxCreate( @Parameter(required = true) @Valid @RequestBody request: XXXXXCreateRequest ): ResponseEntity<XXXXXXCreateResponse> } @RestController class XXXXXController( private val xxxxxUseCase: XXXXXCreateUseCase, ) : XXXXXApi { override fun xxxxxCreate( request: XXXXXCreateRequest, ): ResponseEntity<XXXXXXCreateResponse> { xxxxxUseCase.create( shopIds = request.shopIds.map { ShopId(value = it) }.toList(), commissionRate = request.commissionRate, budget = request.budgetAmount, ) return ResponseEntity.ok().body(CampaignsCreateResponse( … )) } }
  4. Case Study: API request null bypasses Kotlin’s type safety yyyy-MM-dd

    HH:mm:ss.SSS ERROR [xxxx] c.d.d.a.a.r.i.ExceptionHandler - Cannot invoke "java.lang.Number.longValue()" because "item$iv$iv" is null java.lang.NullPointerException: Cannot invoke "java.lang.Number.longValue()" because "item$iv$iv" is null at … What happened, NPE ??? ERROR: NullPointerException
  5. Case Study: API request null bypasses Kotlin’s type safety java.lang.NullPointerException:

    Cannot invoke "java.lang.Number.longValue()" because "item$iv$iv" is null at com.xxx.yyy.zzz.XXXXXController.xxxxxCreate(XXXXXController.kt:193) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:19 6) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) stack trace
  6. Case Study: API request null bypasses Kotlin’s type safety java.lang.NullPointerException:

    Cannot invoke "java.lang.Number.longValue()" because "item$iv$iv" is null at com.xxx.yyy.zzz.XXXXXController.xxxxxCreate(XXXXXController.kt:193) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:19 6) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) stack trace @RestController class XXXXXController( private val xxxxxUseCase: XXXXXCreateUseCase, ) : XXXXXApi { override fun campaignsCreate( request: XXXXXCreateRequest, ): ResponseEntity<XXXXXXCreateResponse> { xxxxxUseCase.create( shopIds = request.shopIds.map { ShopId(value = it) }.toList(), commissionRate = request.commissionRate, budget = request.budgetAmount, ) return ResponseEntity.ok().body(CampaignsCreateResponse( … )) } } 193
  7. data class XXXXXCreateRequest( @get:Size(min=1,max=20) @Schema(required = true) @get:JsonProperty("shopIds", required =

    true) val shopIds: kotlin.collections.List<kotlin.Long>, @get:Min(0) @get:Max(100) @Schema(required = true) @get:JsonProperty("commissionRate", required = true) val commissionRate: kotlin.Int, @get:Min(0L) @get:Max(10000000L) @Schema(required = true) @get:JsonProperty("budget", required = true) val budget: kotlin.Long, ) @RestController class XXXXXController( private val xxxxxUseCase: XXXXXCreateUseCase, ) : XXXXXApi { override fun xxxxxCreate( request: XXXXXCreateRequest, ): ResponseEntity<XXXXXXCreateResponse> { xxxxxUseCase.create( shopIds = request.shopIds.map { ShopId(value = it) }.toList(), commissionRate = request.commissionRate, budget = request.budgetAmount, ) return ResponseEntity.ok().body(CampaignsCreateResponse( … )) } } Back to the implementation Why does List<Long> contains “null”? Case Study: API request null bypasses Kotlin’s type safety Request Log: { … ”shopIds”: [null], … }
  8. Why does it contains null? Case Study: API request null

    bypasses Kotlin’s type safety - Spring MVC - Tomcat
  9. Case Study: API request null bypasses Kotlin’s type safety -

    Why does it contains null? Tomcat
 - Socket I/O ( coyote )
 - Find Servlet, Filter Process, Call Servlet ( catalina: Servlet Container ) https://github.com/apache/tomcat/blob/11.0.x/webapps/docs/architecture/requestProcess/11_nio.png
  10. Case Study: API request null bypasses Kotlin’s type safety -

    Why does it contains null? Socket I/O Filter Chain Call Servlet public final class ApplicationFilterChain implements FilterChain { ɹɹɹ : : public void doFilter(ServletRequest request, ServletResponse response) throws … { : : this.servlet.service(request, response);
  11. Case Study: API request null bypasses Kotlin’s type safety -

    Why does it contains null? Socket I/O Filter Chain Call Servlet public final class ApplicationFilterChain implements FilterChain { ɹɹɹ : : public void doFilter(ServletRequest request, ServletResponse response) throws … { : : this.servlet.service(request, response); Spring public class DispatcherServlet extends FrameworkServlet { : protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws … { : // Determine handler adapter and invoke the handler. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); dispatch to Controller
  12. Case Study: API request null bypasses Kotlin’s type safety -

    Why does it contains null? Socket I/O Filter Chain Call Servlet public final class ApplicationFilterChain implements FilterChain { ɹɹɹ : : public void doFilter(ServletRequest request, ServletResponse response) throws … { : : this.servlet.service(request, response); Spring public class DispatcherServlet extends FrameworkServlet { : protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws … { : // Determine handler adapter and invoke the handler. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); dispatch to Controller return parameter.hasParameterAnnotation(RequestBody.class); protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws … { for (HttpMessageConverter<?> converter : this.messageConverters) { :
 : ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse); : Detect @RequestBody Read w/ MessageConverter
  13. Case Study: API request null bypasses Kotlin’s type safety -

    Why does it contains null? Socket I/O Filter Chain Call Servlet public final class ApplicationFilterChain implements FilterChain { ɹɹɹ : : public void doFilter(ServletRequest request, ServletResponse response) throws … { : : this.servlet.service(request, response); Spring public class DispatcherServlet extends FrameworkServlet { : protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws … { : // Determine handler adapter and invoke the handler. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); dispatch to Controller return parameter.hasParameterAnnotation(RequestBody.class); protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws … { for (HttpMessageConverter<?> converter : this.messageConverters) { :
 : ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse); : Detect @RequestBody Read w/ MessageConverter
  14. Case Study: API request null bypasses Kotlin’s type safety -

    Why does it contains null? Socket I/O Filter Chain Call Servlet public final class ApplicationFilterChain implements FilterChain { ɹɹɹ : : public void doFilter(ServletRequest request, ServletResponse response) throws … { : : this.servlet.service(request, response); Spring public class DispatcherServlet extends FrameworkServlet { : protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws … { : // Determine handler adapter and invoke the handler. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); dispatch to Controller return parameter.hasParameterAnnotation(RequestBody.class); protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws … { for (HttpMessageConverter<?> converter : this.messageConverters) { :
 : ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse); : Detect @RequestBody Read w/ MessageConverter MappingJackson2HttpMessageConverter
  15. Case Study: API request null bypasses Kotlin’s type safety -

    Why does it contains null? Socket I/O Filter Chain Call Servlet Spring dispatch to Controller Detect @RequestBody Read w/ MessageConverter Deserialize JSON private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException { : objectReader = this.customizeReader(objectReader, javaType); if (isUnicode) { return objectReader.readValue(inputStream); } else { Reader reader = new InputStreamReader(inputStream, charset); return objectReader.readValue(reader); } : :
  16. Case Study: API request null bypasses Kotlin’s type safety -

    Why does it contains null? Socket I/O Filter Chain Call Servlet Spring dispatch to Controller Detect @RequestBody Read w/ MessageConverter Deserialize JSON ( jackson-databind ) public class CollectionDeserializer extends ContainerDeserializerBase<Collection<Object>> implements ContextualDeserializer { : protected Collection<Object> _deserializeFromArray(JsonParser p, DeserializationContext ctxt, Collection<Object> result) throws IOException { : : JsonToken t; while ((t = p.nextToken()) != JsonToken.END_ARRAY) { try { Object value; if (t == JsonToken.VALUE_NULL) { if (_skipNullValues) { continue; } value = null; } else { value = _deserializeNoNullChecks(p, ctxt); } if (value == null) { value = _nullProvider.getNullValue(ctxt); // _skipNullValues is checked by _tryToAddNull. if (value == null) { _tryToAddNull(p, ctxt, result); continue; } } result.add(value); :
  17. Case Study: API request null bypasses Kotlin’s type safety -

    Why does it contains null? Socket I/O Filter Chain Call Servlet Spring dispatch to Controller Detect @RequestBody Read w/ MessageConverter Deserialize JSON ( jackson-databind ) public class CollectionDeserializer extends ContainerDeserializerBase<Collection<Object>> implements ContextualDeserializer { : protected Collection<Object> _deserializeFromArray(JsonParser p, DeserializationContext ctxt, Collection<Object> result) throws IOException { : : JsonToken t; while ((t = p.nextToken()) != JsonToken.END_ARRAY) { try { Object value; if (t == JsonToken.VALUE_NULL) { if (_skipNullValues) { continue; } value = null; } else { value = _deserializeNoNullChecks(p, ctxt); } if (value == null) { value = _nullProvider.getNullValue(ctxt); // _skipNullValues is checked by _tryToAddNull. if (value == null) { _tryToAddNull(p, ctxt, result); continue; } } result.add(value); : jackson-databind deserializes it as java.util.List, not as a kotlin.collections.List. Therefore, it may contain null elements.
  18. Case Study: API request null bypasses Kotlin’s type safety -

    Why does it contains null? Socket I/O Filter Chain Call Servlet Spring dispatch to Controller Detect @RequestBody Read w/ MessageConverter Deserialize JSON Invoke Controller method @RestController class XXXXXController( private val xxxxxUseCase: XXXXXCreateUseCase, ) : XXXXXApi { override fun xxxxxCreate( request: XXXXXCreateRequest, ): ResponseEntity<XXXXXXCreateResponse> { xxxxxUseCase.create( shopIds = request.shopIds.map { ShopId(value = it) }.toList(), commissionRate = request.commissionRate, budget = request.budgetAmount, ) return ResponseEntity.ok().body(CampaignsCreateResponse( … )) } }
  19. Case Study: API request null bypasses Kotlin’s type safety -

    Why does it contains null? Socket I/O Filter Chain Call Servlet dispatch to Controller Detect @RequestBody Read w/ MessageConverter Deserialize JSON Invoke Controller method - Java-based deserialization by Jackson - di ff erence in nullability between Kotlin and Java type i.e. Java - Kotlin interoperation pitfall 🕳 Why does it contains null ?
  20. Kotlin’s Null Safety T T? T T null OR =

    = T! T null OR = Java interop Platform Type Non-denotable type … T ( actually T! )
  21. How to fi x ? @Configuration class JacksonConfig { @Bean

    fun kotlinModule(): KotlinModule { return KotlinModule.Builder() .enable(KotlinFeature.StrictNullChecks) .build() } } • Enable StrictNullChecks FasterXML/jackson-module-kotlin
  22. How to fi x ? @Configuration class JacksonConfig { @Bean

    fun kotlinModule(): KotlinModule { return KotlinModule.Builder() .enable(KotlinFeature.StrictNullChecks) .build() } } • Enable StrictNullChecks
  23. How to fi x ? • Enable StrictNullChecks • Annotate

    to Java code. @NonNull / @Nullable • Kotlin Compiler Option ( -Xjsr305=strict ) The other case: In my case:
  24. Wrap up • Kotlin’s null safety is strong • Be

    aware of nullability ( platform types ) in Java interoperation