It's not possible to use @ConfigurationProperties with third party collections such as Guava's ImmutableList.

I couldn't find an extensibility point to add one myself and lot of the code in org.springframework.boot.context.properties.bind is package private so probably there isn't one but I'm happy to be proven wrong.

Comment From: mwisnicki

I poked around the code and it seems like one would need extension point within Binder#getAggregateBinder.

Comment From: mwisnicki

Turns out ConfigurationPropertiesBindHandlerAdvisor provides all that's needed.

I swap out types onStart and then convert results in onSuccess:

@Configuration
public class GuavaImmutableConfigurationSupport {

    @Bean
    ConfigurationPropertiesBindHandlerAdvisor configurationPropertiesBindHandlerAdvisor() {
        return bindHandler -> new AbstractBindHandler(bindHandler) {

            private final Set<ConfigurationPropertyName> immutableProperties = new HashSet<>();

            private <T> Bindable<T> swapType(ConfigurationPropertyName name, Bindable<T> target, Class<?> oldClass, Class<?> newClass) {
                var klass = target.getType().getRawClass();
                if (oldClass.equals(klass)) {
                    var newType = ResolvableType.forClassWithGenerics(newClass, target.getType().getGenerics());
                    var newTarget = Bindable.<T>of(newType).withAnnotations(target.getAnnotations());
                    immutableProperties.add(name);
                    return newTarget;
                }
                return target;
            }

            @Override
            public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target, BindContext context) {
                var newTarget = target;
                newTarget = swapType(name, newTarget, ImmutableList.class, List.class);
                newTarget = swapType(name, newTarget, ImmutableSet.class, Set.class);
                newTarget = swapType(name, newTarget, ImmutableMap.class, Map.class);
                return super.onStart(name, newTarget, context);
            }

            @Override
            public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
                var object = super.onSuccess(name, target, context, result);
                if (immutableProperties.contains(name)) {
                    if (object instanceof List)
                        return ImmutableList.copyOf((List<?>) object);
                    if (object instanceof Set)
                        return ImmutableSet.copyOf((Set<?>) object);
                    if (object instanceof Map)
                        return ImmutableMap.copyOf((Map<?, ?>) object);
                }
                return object;
            }
        };
    }

}

Comment From: mwisnicki

IMHO it would be nicer if @ConfigurationProperties just used converters. There's already quite a bit of code inside to handle unsupported collections and invoke conversions but it didn't work when I tried.

Comment From: philwebb

It would be quite nice to support these types, but I'm not very keen on adding Guava as a dependency. I think the ConfigurationPropertiesBindHandlerAdvisor approach is probably the best option.

Comment From: mwisnicki

@philwebb How about making it work with registered converters?

Comment From: philwebb

Which converters did you have in mind? We do already call converters in IndexedElementsBinder, but only if the list is defined as a complete String (i.e. prop=a,b,c will be converted but a YAML list would not).

Do you have some kind of CollectionToImmutableCollection converter that you want to use?