Bind Optional value object parameters as empty rather than null

Update `ValueObjectBinder` so that `Optional` parameters are bound as
`Option.empty()` rather than `null`.

See gh-49152

Signed-off-by: Ondřej Světlík <16480390+osvetlik@users.noreply.github.com>
This commit is contained in:
Ondřej Světlík
2026-02-08 09:48:02 -08:00
committed by Phillip Webb
parent e6472d8bc9
commit 4244dd933a
3 changed files with 40 additions and 2 deletions
@@ -105,7 +105,13 @@ final class BindConverter {
new ResolvableTypeDescriptor(targetType, targetAnnotations));
}
private @Nullable Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
@SuppressWarnings("unchecked")
<T> @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 {
@@ -132,7 +132,12 @@ class ValueObjectBinder implements DataObjectBinder {
return convertDefaultValue(context.getConverter(), defaultValue, type, annotations);
}
}
return null;
return convertNullValue(context, type);
}
private <T> @Nullable T convertNullValue(Binder.Context context, ResolvableType type, Annotation... annotations) {
BindConverter converter = context.getConverter();
return converter.convertNullValue(type, annotations);
}
private <T> @Nullable T convertDefaultValue(BindConverter converter, String[] defaultValue, ResolvableType type,
@@ -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<String> property1) {
}
record RecordPropertiesWithOptionalInNestedRecord(NestedRecord nested) {
record NestedRecord(Optional<String> property1, String property2) {
}
}
static class NonExtractableParameterName {
private @Nullable String value;