From 012fb290972a6a01285c7750086186531c126ee6 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 23 Dec 2025 11:37:44 +0100 Subject: [PATCH] Fix nullness mismatch for Converter/ConverterFactory In gh-35947, the `Converter` contract was refined to allow for nullable return values. This created a mismatch with the `ConverterFactory` contract. This commit fixes this mismatch by allowing nullable return values in `Converter` instances created by `ConverterFactory`. Fixes gh-36063 --- .../convert/converter/ConverterFactory.java | 4 +- .../support/CharacterToNumberFactory.java | 4 +- .../IntegerToEnumConverterFactory.java | 4 +- .../NumberToNumberConverterFactory.java | 4 +- .../support/StringToEnumConverterFactory.java | 2 +- .../StringToNumberConverterFactory.java | 2 +- .../ConverterFactoryNullnessTests.kt | 53 +++++++++++++++++++ 7 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 spring-core/src/test/kotlin/org/springframework/core/convert/converter/ConverterFactoryNullnessTests.kt diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java index 057fdc8cb7f..3aac06b900f 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java @@ -16,6 +16,8 @@ package org.springframework.core.convert.converter; +import org.jspecify.annotations.Nullable; + /** * A factory for "ranged" converters that can convert objects from S to subtypes of R. * @@ -36,6 +38,6 @@ public interface ConverterFactory { * @param targetType the target type to convert to * @return a converter from S to T */ - Converter getConverter(Class targetType); + Converter getConverter(Class targetType); } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java index 9fbca57ae5c..ac88807a73c 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java @@ -16,6 +16,8 @@ package org.springframework.core.convert.support; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.util.NumberUtils; @@ -41,7 +43,7 @@ import org.springframework.util.NumberUtils; final class CharacterToNumberFactory implements ConverterFactory { @Override - public Converter getConverter(Class targetType) { + public Converter getConverter(Class targetType) { return new CharacterToNumber<>(targetType); } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java index c5842da07dc..306400cdc36 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java @@ -16,6 +16,8 @@ package org.springframework.core.convert.support; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; @@ -30,7 +32,7 @@ import org.springframework.core.convert.converter.ConverterFactory; final class IntegerToEnumConverterFactory implements ConverterFactory { @Override - public Converter getConverter(Class targetType) { + public Converter getConverter(Class targetType) { return new IntegerToEnum(ConversionUtils.getEnumType(targetType)); } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/NumberToNumberConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/NumberToNumberConverterFactory.java index 4dad03bee5a..abf200874e9 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/NumberToNumberConverterFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/NumberToNumberConverterFactory.java @@ -16,6 +16,8 @@ package org.springframework.core.convert.support; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalConverter; import org.springframework.core.convert.converter.Converter; @@ -43,7 +45,7 @@ import org.springframework.util.NumberUtils; final class NumberToNumberConverterFactory implements ConverterFactory, ConditionalConverter { @Override - public Converter getConverter(Class targetType) { + public Converter getConverter(Class targetType) { return new NumberToNumber<>(targetType); } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java index f544317a453..93c32d6d700 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java @@ -32,7 +32,7 @@ import org.springframework.core.convert.converter.ConverterFactory; final class StringToEnumConverterFactory implements ConverterFactory { @Override - public Converter getConverter(Class targetType) { + public Converter getConverter(Class targetType) { return new StringToEnum(ConversionUtils.getEnumType(targetType)); } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java index e450803e904..ce129e56968 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java @@ -43,7 +43,7 @@ import org.springframework.util.NumberUtils; final class StringToNumberConverterFactory implements ConverterFactory { @Override - public Converter getConverter(Class targetType) { + public Converter getConverter(Class targetType) { return new StringToNumber<>(targetType); } diff --git a/spring-core/src/test/kotlin/org/springframework/core/convert/converter/ConverterFactoryNullnessTests.kt b/spring-core/src/test/kotlin/org/springframework/core/convert/converter/ConverterFactoryNullnessTests.kt new file mode 100644 index 00000000000..877e20817c9 --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/convert/converter/ConverterFactoryNullnessTests.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.convert.converter + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import kotlin.reflect.full.primaryConstructor + +/** + * @author Brian Clozel + */ +class ConverterFactoryNullnessTests { + + @Test + fun converterFactoryWithNullableTypes() { + val factory = StringToIdConverterFactory + + val userIdConverter = factory.getConverter(UserId::class.java) + assertThat(userIdConverter.convert("42")).isEqualTo(UserId("42")) + } + + object StringToIdConverterFactory : ConverterFactory { + override fun getConverter(targetType: Class): Converter { + val constructor = checkNotNull(targetType.kotlin.primaryConstructor) + return Converter { source -> + constructor.call(source) + } + } + } + + abstract class Id { + abstract val value: String + } + + data class UserId(override val value: String) : Id() + + data class ProductId(override val value: String) : Id() + +} \ No newline at end of file