diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index 414bfeb0285..5a453b28545 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -1069,6 +1069,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { if (map == null) { map = CollectionFactory.createMap(paramType, 16); } + else if (map.containsKey(key)) { + continue; + } String indexedPath = name.substring(0, endIdx + 1); map.put(key, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver)); diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java index dc4b0d5cb3f..3eac155ad32 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import jakarta.validation.constraints.NotNull; import org.jspecify.annotations.Nullable; @@ -158,6 +159,27 @@ class DataBinderConstructTests { assertThat(map.get("c").param1()).isEqualTo("value3"); } + @Test // gh-37019 + void dataClassWithMapBindingConstructsValueOncePerKey() { + CountingRecord.constructorCallCount.set(0); + MapValueResolver valueResolver = new MapValueResolver(Map.of( + "countingMap[a].param1", "value1", "countingMap[a].param2", "value2", + "countingMap[b].param1", "value3", "countingMap[b].param2", "value4")); + + DataBinder binder = initDataBinder(CountingMapRecord.class); + binder.construct(valueResolver); + + CountingMapRecord target = getTarget(binder); + Map map = target.countingMap(); + + assertThat(map).hasSize(2); + assertThat(map.get("a").param1()).isEqualTo("value1"); + assertThat(map.get("a").param2()).isEqualTo("value2"); + assertThat(map.get("b").param1()).isEqualTo("value3"); + assertThat(map.get("b").param2()).isEqualTo("value4"); + assertThat(CountingRecord.constructorCallCount).hasValue(2); + } + @Test void dataClassWithArrayBinding() { MapValueResolver valueResolver = new MapValueResolver(Map.of( @@ -327,6 +349,20 @@ class DataBinderConstructTests { } + record CountingRecord(String param1, String param2) { + + static final AtomicInteger constructorCallCount = new AtomicInteger(); + + CountingRecord { + constructorCallCount.incrementAndGet(); + } + } + + + private record CountingMapRecord(Map countingMap) { + } + + private record DataClassArrayRecord(DataClass[] dataClassArray) { }