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

The Pitfall of Kotlin's Null Safety

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Yasuhisa Honda Yasuhisa Honda
November 13, 2025
10

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