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 12b222d33580..fb428426703e 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -49,6 +49,7 @@ import org.springframework.beans.SimpleTypeConverter; import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeMismatchException; +import org.springframework.core.CollectionFactory; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; @@ -112,6 +113,7 @@ * @author Stephane Nicoll * @author Kazuki Shimizu * @author Sam Brannen + * @author Yanming Zhou * @see #setAllowedFields * @see #setRequiredFields * @see #registerCustomEditor @@ -952,6 +954,19 @@ private Object createObject(ResolvableType objectType, String nestedPath, ValueR ResolvableType type = ResolvableType.forMethodParameter(param); args[i] = createObject(type, paramPath + ".", valueResolver); } + else if (value == null && (Collection.class == paramType || List.class.isAssignableFrom(paramType) + || Map.class.isAssignableFrom(paramType)) && hasIndexedValuesFor(paramPath, valueResolver)) { + if (Collection.class == paramType) { + // CollectionFactory.createCollection() will create LinkedHashSet which doesn't support indexed property path + args[i] = new ArrayList<>(16); + } + else if (List.class.isAssignableFrom(paramType)) { + args[i] = CollectionFactory.createCollection(paramType, 16); + } + else if (Map.class.isAssignableFrom(paramType)) { + args[i] = CollectionFactory.createMap(paramType, 16); + } + } else { try { if (value == null && (param.isOptional() || getBindingResult().hasErrors())) { @@ -1031,6 +1046,15 @@ private boolean hasValuesFor(String paramPath, ValueResolver resolver) { return false; } + private boolean hasIndexedValuesFor(String paramPath, ValueResolver resolver) { + for (String name : resolver.getNames()) { + if (name.startsWith(paramPath + "[")) { + return true; + } + } + return false; + } + private void validateConstructorArgument( Class constructorClass, String nestedPath, String name, @Nullable Object value) { 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 9eb00934def4..275d54faa849 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java @@ -17,6 +17,8 @@ package org.springframework.validation; import java.beans.ConstructorProperties; +import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -24,6 +26,7 @@ import jakarta.validation.constraints.NotNull; import org.junit.jupiter.api.Test; +import org.springframework.beans.MutablePropertyValues; import org.springframework.core.ResolvableType; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.lang.Nullable; @@ -35,6 +38,7 @@ * Tests for {@link DataBinder} with constructor binding. * * @author Rossen Stoyanchev + * @author Yanming Zhou */ class DataBinderConstructTests { @@ -101,6 +105,24 @@ void dataClassBindingWithConversionError() { assertThat(bindingResult.getFieldValue("param3")).isNull(); } + @Test + void recordClassBindingWithIndexedValue() { + Map data = Map.of("collection[0]", "test","list[0]", "test", "map[foo]", "bar"); + MapValueResolver valueResolver = new MapValueResolver(data); + DataBinder binder = initDataBinder(RecordClass.class); + binder.construct(valueResolver); + binder.bind(new MutablePropertyValues(data)); + + BindingResult bindingResult = binder.getBindingResult(); + assertThat(bindingResult.getAllErrors()).hasSize(0); + + RecordClass target = (RecordClass) binder.getTarget(); + assertThat(target).isNotNull(); + assertThat(target.collection).hasSize(1).first().isEqualTo("test"); + assertThat(target.list).hasSize(1).first().isEqualTo("test"); + assertThat(target.map).hasSize(1).containsEntry("foo", "bar"); + } + @SuppressWarnings("SameParameterValue") private static DataBinder initDataBinder(Class targetType) { DataBinder binder = new DataBinder(null); @@ -191,4 +213,8 @@ public Set getNames() { } } + record RecordClass(Collection collection, List list, Map map) { + + } + }