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..764b6fa7ffd 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 @@ -105,7 +105,13 @@ final class BindConverter { new ResolvableTypeDescriptor(targetType, targetAnnotations)); } - private @Nullable Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + @SuppressWarnings("unchecked") + @Nullable T convertNullValue(ResolvableType targetType, Annotation... targetAnnotations) { + return (T) convert(null, null, new ResolvableTypeDescriptor(targetType, targetAnnotations)); + } + + private @Nullable Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, + TypeDescriptor targetType) { ConversionException failure = null; for (ConversionService delegate : this.delegates) { try { 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..667ecfe0837 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 @@ -132,7 +132,12 @@ class ValueObjectBinder implements DataObjectBinder { return convertDefaultValue(context.getConverter(), defaultValue, type, annotations); } } - return null; + return convertNullValue(context, type); + } + + private @Nullable T convertNullValue(Binder.Context context, ResolvableType type, Annotation... annotations) { + BindConverter converter = context.getConverter(); + return converter.convertNullValue(type, annotations); } 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..dff3c83fb39 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 @@ -326,6 +326,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 +921,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;