diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java index 29ac4e3fb23..a6fd1c7c826 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java @@ -98,14 +98,12 @@ final class BindConverter { @SuppressWarnings("unchecked") @Nullable T convert(@Nullable Object source, ResolvableType targetType, Annotation... targetAnnotations) { - if (source == null) { - return null; - } - return (T) convert(source, TypeDescriptor.forObject(source), - new ResolvableTypeDescriptor(targetType, targetAnnotations)); + TypeDescriptor sourceType = (source != null) ? TypeDescriptor.forObject(source) : null; + return (T) convert(source, sourceType, new ResolvableTypeDescriptor(targetType, targetAnnotations)); } - private @Nullable Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + private @Nullable Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, + TypeDescriptor targetType) { ConversionException failure = null; for (ConversionService delegate : this.delegates) { try { @@ -119,6 +117,9 @@ final class BindConverter { } } } + if (source == null) { + return null; + } throw (failure != null) ? failure : new ConverterNotFoundException(sourceType, targetType); } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java index bdaffd38958..919c6fc4fd2 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java @@ -61,6 +61,7 @@ import org.springframework.util.Assert; * @author Stephane Nicoll * @author Phillip Webb * @author Scott Frederick + * @author Ondřej Světlík */ class ValueObjectBinder implements DataObjectBinder { @@ -132,7 +133,7 @@ class ValueObjectBinder implements DataObjectBinder { return convertDefaultValue(context.getConverter(), defaultValue, type, annotations); } } - return null; + return context.getConverter().convert(null, type); } private @Nullable T convertDefaultValue(BindConverter converter, String[] defaultValue, ResolvableType type, diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index 2f1d0bd3b4b..7b1661b3a48 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -50,6 +50,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * @author Phillip Webb * @author Pavel Anisimov * @author Yanming Zhou + * @author Ondřej Světlík */ class ValueObjectBinderTests { @@ -326,6 +327,25 @@ class ValueObjectBinderTests { assertThat(bound.getOptionalValue()).isEmpty(); } + @Test + void bindWhenOptionalParameterWithNoValueForPrimitiveShouldReturnEmptyInstance() { + RecordPropertiesWithOptional bound = this.binder.bindOrCreate("foo", + Bindable.of(RecordPropertiesWithOptional.class)); + assertThat(bound.property1()).isEmpty(); + } + + @Test + void bindWhenOptionalParameterInNestedWithNoValueForPrimitiveShouldReturnEmptyInstance() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.nested.property2", "str"); + this.sources.add(source); + RecordPropertiesWithOptionalInNestedRecord bound = this.binder.bindOrCreate("foo", + Bindable.of(RecordPropertiesWithOptionalInNestedRecord.class)); + assertThat(bound.nested()).isNotNull(); + assertThat(bound.nested().property1()).isNotNull().isEmpty(); + assertThat(bound.nested().property2()).isEqualTo("str"); + } + @Test void bindWhenEnumParameterWithEmptyDefaultValueShouldThrowException() { assertThatExceptionOfType(BindException.class) @@ -902,6 +922,14 @@ class ValueObjectBinderTests { @DefaultValue("default-value-2") String property2) { } + record RecordPropertiesWithOptional(Optional property1) { + } + + record RecordPropertiesWithOptionalInNestedRecord(NestedRecord nested) { + record NestedRecord(Optional property1, String property2) { + } + } + static class NonExtractableParameterName { private @Nullable String value;