diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java index 1630d2d8997..cd3620b4173 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -567,6 +567,7 @@ public class Binder { if (source == null) { return supplier.get(); } + ConfigurationPropertySource previous = this.source.get(0); this.source.set(0, source); this.sourcePushCount++; try { @@ -574,6 +575,7 @@ public class Binder { } finally { this.sourcePushCount--; + this.source.set(0, previous); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java index 8d3ca49f126..7d63b531034 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -123,6 +123,41 @@ class CollectionBinderTests { }); } + @Test + void bindToCollectionWhenNonKnownIndexedChildNotBoundThrowsException() { + // gh-45994 + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0].first", "Spring"); + source.put("foo[0].last", "Boot"); + source.put("foo[1].missing", "bad"); + this.sources.add(source); + assertThatExceptionOfType(BindException.class) + .isThrownBy(() -> this.binder.bind("foo", Bindable.listOf(Name.class))) + .satisfies((ex) -> { + Set unbound = ((UnboundConfigurationPropertiesException) ex.getCause()) + .getUnboundProperties(); + assertThat(unbound).hasSize(1); + ConfigurationProperty property = unbound.iterator().next(); + assertThat(property.getName()).hasToString("foo[1].missing"); + assertThat(property.getValue()).isEqualTo("bad"); + }); + } + + @Test + void bindToNestedCollectionWhenNonKnownIndexed() { + // gh-46039 + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo[0].items[0]", "a"); + source.put("foo[0].items[1]", "b"); + source.put("foo[0].string", "test"); + this.sources.add(source); + List list = this.binder.bind("foo", Bindable.listOf(ExampleCollectionBean.class)).get(); + assertThat(list).hasSize(1); + ExampleCollectionBean bean = list.get(0); + assertThat(bean.getItems()).containsExactly("a", "b", "d"); + assertThat(bean.getString()).isEqualTo("test"); + } + @Test void bindToNonScalarCollectionWhenNonSequentialShouldThrowException() { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); @@ -436,6 +471,8 @@ class CollectionBinderTests { private Set itemsSet = new LinkedHashSet<>(); + private String string; + List getItems() { return this.items; } @@ -452,6 +489,14 @@ class CollectionBinderTests { this.itemsSet = itemsSet; } + String getString() { + return this.string; + } + + void setString(String string) { + this.string = string; + } + } static class ExampleCustomNoDefaultConstructorBean { @@ -562,4 +607,8 @@ class CollectionBinderTests { } + record Name(String first, String last) { + + } + }