Skip to content

Support binding indexed values for record class #32386

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -112,6 +113,7 @@
* @author Stephane Nicoll
* @author Kazuki Shimizu
* @author Sam Brannen
* @author Yanming Zhou
* @see #setAllowedFields
* @see #setRequiredFields
* @see #registerCustomEditor
Expand Down Expand Up @@ -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())) {
Expand Down Expand Up @@ -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) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
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;

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;
Expand All @@ -35,6 +38,7 @@
* Tests for {@link DataBinder} with constructor binding.
*
* @author Rossen Stoyanchev
* @author Yanming Zhou
*/
class DataBinderConstructTests {

Expand Down Expand Up @@ -101,6 +105,24 @@ void dataClassBindingWithConversionError() {
assertThat(bindingResult.getFieldValue("param3")).isNull();
}

@Test
void recordClassBindingWithIndexedValue() {
Map<String, Object> 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);
Expand Down Expand Up @@ -191,4 +213,8 @@ public Set<String> getNames() {
}
}

record RecordClass(Collection<String> collection, List<String> list, Map<String, String> map) {

}

}