From a68d60768e7a7430aab5d7538f7863ccf19f500c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 7 Nov 2025 16:19:12 +0100 Subject: [PATCH] Introduce KotlinDetector#hasSerializableAnnotation This commit introduces a KotlinDetector#hasSerializableAnnotation utility method designed to detect types annotated with `@Serializable` at type or generics level. See gh-35761 --- .../springframework/core/KotlinDetector.java | 32 +++++++++++++++++++ .../core/KotlinDetectorTests.kt | 20 ++++++++++++ 2 files changed, 52 insertions(+) diff --git a/spring-core/src/main/java/org/springframework/core/KotlinDetector.java b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java index 20eff90f5d2..269bb88c256 100644 --- a/spring-core/src/main/java/org/springframework/core/KotlinDetector.java +++ b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java @@ -38,6 +38,8 @@ public abstract class KotlinDetector { private static final @Nullable Class KOTLIN_JVM_INLINE; + private static final @Nullable Class KOTLIN_SERIALIZABLE; + private static final @Nullable Class KOTLIN_COROUTINE_CONTINUATION; // For ConstantFieldFeature compliance, otherwise could be deduced from kotlinMetadata @@ -49,6 +51,7 @@ public abstract class KotlinDetector { ClassLoader classLoader = KotlinDetector.class.getClassLoader(); Class metadata = null; Class jvmInline = null; + Class serializable = null; Class coroutineContinuation = null; try { metadata = ClassUtils.forName("kotlin.Metadata", classLoader); @@ -58,6 +61,12 @@ public abstract class KotlinDetector { catch (ClassNotFoundException ex) { // JVM inline support not available } + try { + serializable = ClassUtils.forName("kotlinx.serialization.Serializable", classLoader); + } + catch (ClassNotFoundException ex) { + // Kotlin Serialization not available + } try { coroutineContinuation = ClassUtils.forName("kotlin.coroutines.Continuation", classLoader); } @@ -72,6 +81,7 @@ public abstract class KotlinDetector { KOTLIN_PRESENT = (KOTLIN_METADATA != null); KOTLIN_REFLECT_PRESENT = ClassUtils.isPresent("kotlin.reflect.full.KClasses", classLoader); KOTLIN_JVM_INLINE = (Class) jvmInline; + KOTLIN_SERIALIZABLE = (Class) serializable; KOTLIN_COROUTINE_CONTINUATION = coroutineContinuation; } @@ -125,4 +135,26 @@ public abstract class KotlinDetector { return (KOTLIN_JVM_INLINE != null && clazz.getDeclaredAnnotation(KOTLIN_JVM_INLINE) != null); } + /** + * Determine whether the given {@code ResolvableType} is annotated with {@code @kotlinx.serialization.Serializable} + * at type or generics level. + * @since 7.0 + */ + public static boolean hasSerializableAnnotation(ResolvableType type) { + Class resolvedClass = type.resolve(); + if (KOTLIN_SERIALIZABLE == null || resolvedClass == null) { + return false; + } + if (resolvedClass.isAnnotationPresent(KOTLIN_SERIALIZABLE)) { + return true; + } + @Nullable Class[] resolvedGenerics = type.resolveGenerics(); + for (Class resolvedGeneric : resolvedGenerics) { + if (resolvedGeneric != null && resolvedGeneric.isAnnotationPresent(KOTLIN_SERIALIZABLE)) { + return true; + } + } + return false; + } + } diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinDetectorTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinDetectorTests.kt index 26882fb8d9c..c7fdb28b7de 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/KotlinDetectorTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinDetectorTests.kt @@ -15,6 +15,7 @@ */ package org.springframework.core +import kotlinx.serialization.Serializable import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test @@ -46,7 +47,26 @@ class KotlinDetectorTests { Assertions.assertThat(KotlinDetector.isInlineClass(KotlinDetectorTests::class.java)).isFalse() } + @Test + fun hasSerializableAnnotation() { + Assertions.assertThat(KotlinDetector.hasSerializableAnnotation(ResolvableType.forClass(WithoutSerializable::class.java))).isFalse() + Assertions.assertThat(KotlinDetector.hasSerializableAnnotation(ResolvableType.forClass(WithSerializable::class.java))).isTrue() + + Assertions.assertThat(KotlinDetector.hasSerializableAnnotation(ResolvableType.forClassWithGenerics(List::class.java, WithoutSerializable::class.java))).isFalse() + Assertions.assertThat(KotlinDetector.hasSerializableAnnotation(ResolvableType.forClassWithGenerics(List::class.java, WithSerializable::class.java))).isTrue() + + Assertions.assertThat(KotlinDetector.hasSerializableAnnotation(ResolvableType.forClassWithGenerics(Map::class.java, String::class.java, WithoutSerializable::class.java))).isFalse() + Assertions.assertThat(KotlinDetector.hasSerializableAnnotation(ResolvableType.forClassWithGenerics(Map::class.java, String::class.java, WithSerializable::class.java))).isTrue() + + Assertions.assertThat(KotlinDetector.hasSerializableAnnotation(ResolvableType.NONE)).isFalse() + } + @JvmInline value class ValueClass(val value: String) + data class WithoutSerializable(val value: String) + + @Serializable + data class WithSerializable(val value: String) + }