Skip to content

Commit 4960400

Browse files
rstoyanchevkoenpunt
authored andcommitted
Support binding to Map as target value
Before this commit, we supported binding to a higher level object, or a scalar, or a List of either, but not to a Map of either, which requires similar support as what we have for a List, i.e. creating the target Map and populating it in a recursive manner. This commit adds that support. Closes spring-projectsgh-449
1 parent 7fbef26 commit 4960400

File tree

2 files changed

+82
-7
lines changed

2 files changed

+82
-7
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public class GraphQlArgumentBinder {
7171

7272
private final BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor();
7373

74-
private List<Consumer<DataBinder>> dataBinderInitializers = new ArrayList<>();
74+
private final List<Consumer<DataBinder>> dataBinderInitializers = new ArrayList<>();
7575

7676

7777
public GraphQlArgumentBinder() {
@@ -160,7 +160,7 @@ public Object bind(
160160
// From Map
161161

162162
if (rawValue instanceof Map) {
163-
Object target = createValue((Map<String, Object>) rawValue, targetClass, bindingResult, segments);
163+
Object target = createValue((Map<String, Object>) rawValue, targetType, bindingResult, segments);
164164
return wrapAsOptionalIfNecessary(target, targetType);
165165
}
166166

@@ -204,6 +204,7 @@ private <T> Collection<T> createCollection(
204204
return Collections.emptyList();
205205
}
206206

207+
ResolvableType elementType = collectionType.asCollection().getGeneric(0);
207208
Class<?> elementClass = collectionType.asCollection().getGeneric(0).resolve();
208209
if (elementClass == null) {
209210
bindingResult.rejectValue(toArgumentPath(segments), "unknownElementType", "Unknown element type");
@@ -218,7 +219,7 @@ private <T> Collection<T> createCollection(
218219
collection.add((T) rawValue);
219220
}
220221
else if (rawValue instanceof Map) {
221-
collection.add((T) createValueOrNull((Map<String, Object>) rawValue, elementClass, bindingResult, segments));
222+
collection.add((T) createValueOrNull((Map<String, Object>) rawValue, elementType, bindingResult, segments));
222223
}
223224
else {
224225
collection.add((T) convertValue(rawValue, elementClass, bindingResult, segments));
@@ -230,7 +231,7 @@ else if (rawValue instanceof Map) {
230231

231232
@Nullable
232233
private Object createValueOrNull(
233-
Map<String, Object> rawMap, Class<?> targetType, BindingResult result, Stack<String> segments) {
234+
Map<String, Object> rawMap, ResolvableType targetType, BindingResult result, Stack<String> segments) {
234235

235236
try {
236237
return createValue(rawMap, targetType, result, segments);
@@ -242,11 +243,40 @@ private Object createValueOrNull(
242243

243244
@SuppressWarnings("unchecked")
244245
private Object createValue(
245-
Map<String, Object> rawMap, Class<?> targetType, BindingResult bindingResult,
246+
Map<String, Object> rawMap, ResolvableType targetType, BindingResult bindingResult,
246247
Stack<String> segments) throws BindException {
247248

249+
Class<?> targetClass = targetType.resolve();
250+
Assert.notNull(targetClass, "Unknown target class");
251+
252+
if (Map.class.isAssignableFrom(targetClass)) {
253+
ResolvableType valueType = targetType.asMap().getGeneric(1);
254+
Class<?> valueClass = valueType.resolve();
255+
if (valueClass == null) {
256+
bindingResult.rejectValue(toArgumentPath(segments), "unknownMapValueType", "Unknown Map value type");
257+
return Collections.emptyMap();
258+
}
259+
Map<String, Object> map = CollectionFactory.createMap(targetClass, rawMap.size());
260+
for (Map.Entry<String, Object> entry : rawMap.entrySet()) {
261+
Object rawValue = entry.getValue();
262+
segments.push("[" + entry.getKey() + "]");
263+
if (rawValue == null || valueType.isAssignableFrom(rawValue.getClass())) {
264+
map.put(entry.getKey(), entry.getValue());
265+
}
266+
else if (rawValue instanceof Map) {
267+
map.put(entry.getKey(), createValueOrNull(
268+
(Map<String, Object>) rawValue, valueType, bindingResult, segments));
269+
}
270+
else {
271+
map.put(entry.getKey(), convertValue(rawValue, valueClass, bindingResult, segments));
272+
}
273+
segments.pop();
274+
}
275+
return map;
276+
}
277+
248278
Object target;
249-
Constructor<?> ctor = BeanUtils.getResolvableConstructor(targetType);
279+
Constructor<?> ctor = BeanUtils.getResolvableConstructor(targetClass);
250280

251281
// Default constructor + data binding via properties
252282

@@ -293,7 +323,7 @@ else if (isApproximableCollectionType(rawValue)) {
293323
}
294324
else if (rawValue instanceof Map) {
295325
boolean isOptional = (paramTypes[i] == Optional.class);
296-
Class<?> type = (isOptional ? methodParam.nestedIfOptional().getNestedParameterType() : paramTypes[i]);
326+
ResolvableType type = ResolvableType.forMethodParameter(methodParam.nestedIfOptional());
297327
Object value = createValueOrNull((Map<String, Object>) rawValue, type, bindingResult, segments);
298328
args[i] = (isOptional ? Optional.ofNullable(value) : value);
299329
}

spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,37 @@ void primaryConstructorBindingErrorWithNestedBeanList() {
280280
});
281281
}
282282

283+
@Test
284+
void primaryConstructorWithMapArgument() throws Exception {
285+
286+
Object result = this.binder.bind(
287+
environment(
288+
"{\"key\":{" +
289+
"\"map\":{" +
290+
"\"item1\":{" +
291+
"\"name\":\"Jason\"," +
292+
"\"age\":\"21\"" +
293+
"}," +
294+
"\"item2\":{" +
295+
"\"name\":\"James\"," +
296+
"\"age\":\"22\"" +
297+
"}" +
298+
"}}}"),
299+
"key",
300+
ResolvableType.forClass(PrimaryConstructorItemMapBean.class));
301+
302+
assertThat(result).isNotNull().isInstanceOf(PrimaryConstructorItemMapBean.class);
303+
Map<String, Item> map = ((PrimaryConstructorItemMapBean) result).getMap();
304+
305+
Item item1 = map.get("item1");
306+
assertThat(item1.getName()).isEqualTo("Jason");
307+
assertThat(item1.getAge()).isEqualTo(21);
308+
309+
Item item2 = map.get("item2");
310+
assertThat(item2.getName()).isEqualTo("James");
311+
assertThat(item2.getAge()).isEqualTo(22);
312+
}
313+
283314
@Test // gh-447
284315
@SuppressWarnings("unchecked")
285316
void primaryConstructorWithGenericObject() throws Exception {
@@ -428,6 +459,20 @@ public List<Item> getItems() {
428459
}
429460

430461

462+
static class PrimaryConstructorItemMapBean {
463+
464+
private final Map<String, Item> map;
465+
466+
public PrimaryConstructorItemMapBean(Map<String, Item> map) {
467+
this.map = map;
468+
}
469+
470+
public Map<String, Item> getMap() {
471+
return this.map;
472+
}
473+
}
474+
475+
431476
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
432477
static class PrimaryConstructorOptionalItemBean {
433478

0 commit comments

Comments
 (0)