Merge pull request #49152 from osvetlik

* pr/49152:
  Polish "Bind `Optional` value object parameters as empty rather than `null`"
  Bind `Optional` value object parameters as empty rather than `null`

Closes gh-49152
This commit is contained in:
Phillip Webb
2026-02-08 09:52:26 -08:00
3 changed files with 37 additions and 7 deletions
@@ -98,14 +98,12 @@ final class BindConverter {
@SuppressWarnings("unchecked")
<T> @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);
}
@@ -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 <T> @Nullable T convertDefaultValue(BindConverter converter, String[] defaultValue, ResolvableType type,
@@ -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<String> property1) {
}
record RecordPropertiesWithOptionalInNestedRecord(NestedRecord nested) {
record NestedRecord(Optional<String> property1, String property2) {
}
}
static class NonExtractableParameterName {
private @Nullable String value;