Version information

Spring-boot version: 2.2.6.RELEASE

Issue reproduction and actual result

I have some simple constructor-bound ConfigurationProperties:

    package com.example.demo;

    import java.nio.file.Path;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.boot.context.properties.ConstructorBinding;
    import org.springframework.boot.context.properties.bind.DefaultValue;

    @ConfigurationProperties
    @ConstructorBinding
    public class ConfigurationPropertiesWithPathProperty {
        private final Path somePathProperty;

        public ConfigurationPropertiesWithPathProperty(Path somePathProperty) {
            this.somePathProperty = somePathProperty;
        }

        public Path getSomePathProperty() {
            return somePathProperty;
        }
    }

This test, in which I try to bind an empty string, fails:

    package com.example.demo;

    import static org.assertj.core.api.Assertions.assertThat;

    import java.nio.file.Paths;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.Map;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.context.properties.bind.BindResult;
    import org.springframework.boot.context.properties.bind.Bindable;
    import org.springframework.boot.context.properties.bind.Binder;
    import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;

    public class PropertiesTest {

        @Test
        void bindsEmptyToPathProperty() {
            Map<String, String> properties = new HashMap<>();
            properties.put("some_path_property", "");
            Binder binder = new Binder(new MapConfigurationPropertySource(properties));

            BindResult<ConfigurationPropertiesWithPathProperty> bindResult =
                    binder.bind("", Bindable.of(ConfigurationPropertiesWithPathProperty.class));

            assertThat(bindResult.isBound()).isTrue();
            assertThat(bindResult.get().getSomePathProperty()).isEqualTo(Paths.get(""));
        }
    }

Additional observations

When I add a @DefaultValue to the constructor property:

    public ConfigurationPropertiesWithPathProperty(@DefaultValue("some_default") Path somePathProperty) {
        this.somePathProperty = somePathProperty;
    }

The same test fails with an exception. The exception is the same as in spring-projects/spring-boot#21264, but it means that binding to the default value is attempted.

Note that the same issue occurs not only with constructor-bound properties, but java bean binding as well.

Expected result

An empty string can be bound to a property of java.nio.file.Path type resulting in a relative empty path (equal to the result of Paths.get("")).

Comment From: philwebb

Thanks for the detailed description. The cause of the problem is that the conversion service does not support the array type used in @DefaultValue when using the TypeConverterConverter service.

The Path object uses a PropertyEditor based converter. If it has a full Converter then the ArrayToObjectConverter would take care of conversion.

Comment From: philwebb

From @mbhave in the original issue:

For this, we rely on Spring Framework's PathEditor for converting from String to Path. The PathEditor returns a null value if the source value is "". This might be something that needs to be fixed in Spring Framework. If the rest of the team agrees, this issue will need to be transferred to Spring Framework's issue tracker.

Comment From: snicoll

Unfortunately, that's how the editors work. They treat an empty value as the absence of a value. I understand that the empty string has a special meaning for Path but this could come as a surprise for users that rely on the current behavior.