diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java b/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java index 175e4d3a..9c839b75 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java @@ -178,8 +178,8 @@ public class GraphQlArgumentBinder { value = bindMap(name, (Map) rawValue, targetType, targetClass, bindingResult); } else { - value = (targetClass.isAssignableFrom(rawValue.getClass()) ? - rawValue : convertValue(name, rawValue, targetClass, bindingResult)); + value = (!targetClass.isAssignableFrom(rawValue.getClass()) ? + convertValue(name, rawValue, targetType, targetClass, bindingResult) : rawValue); } if (isOptional) { @@ -229,8 +229,8 @@ public class GraphQlArgumentBinder { Constructor constructor = BeanUtils.getResolvableConstructor(targetClass); Object value = constructor.getParameterCount() > 0 ? - bindMapToObjectViaConstructor(rawMap, constructor, bindingResult) : - bindMapToObjectViaSetters(rawMap, constructor, bindingResult); + bindMapToObjectViaConstructor(rawMap, constructor, targetType, bindingResult) : + bindMapToObjectViaSetters(rawMap, constructor, targetType, bindingResult); bindingResult.popNestedPath(); @@ -261,7 +261,8 @@ public class GraphQlArgumentBinder { @Nullable private Object bindMapToObjectViaConstructor( - Map rawMap, Constructor constructor, ArgumentsBindingResult bindingResult) { + Map rawMap, Constructor constructor, ResolvableType parentType, + ArgumentsBindingResult bindingResult) { String[] paramNames = BeanUtils.getParameterNames(constructor); Class[] paramTypes = constructor.getParameterTypes(); @@ -270,8 +271,11 @@ public class GraphQlArgumentBinder { for (int i = 0; i < paramNames.length; i++) { String name = paramNames[i]; boolean isOmitted = !rawMap.containsKey(name); - ResolvableType paramType = ResolvableType.forConstructorParameter(constructor, i); - args[i] = bindRawValue(name, rawMap.get(name), isOmitted, paramType, paramTypes[i], bindingResult); + + ResolvableType targetType = ResolvableType.forType( + ResolvableType.forConstructorParameter(constructor, i).getType(), parentType); + + args[i] = bindRawValue(name, rawMap.get(name), isOmitted, targetType, paramTypes[i], bindingResult); } try { @@ -287,20 +291,26 @@ public class GraphQlArgumentBinder { } private Object bindMapToObjectViaSetters( - Map rawMap, Constructor constructor, ArgumentsBindingResult bindingResult) { + Map rawMap, Constructor constructor, ResolvableType parentType, + ArgumentsBindingResult bindingResult) { Object target = BeanUtils.instantiateClass(constructor); BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(target); for (Map.Entry entry : rawMap.entrySet()) { String key = entry.getKey(); - TypeDescriptor type = beanWrapper.getPropertyTypeDescriptor(key); - if (type == null) { + TypeDescriptor typeDescriptor = beanWrapper.getPropertyTypeDescriptor(key); + if (typeDescriptor == null) { // Ignore unknown property continue; } + + ResolvableType targetType = + ResolvableType.forType(typeDescriptor.getResolvableType().getType(), parentType); + Object value = bindRawValue( - key, entry.getValue(), false, type.getResolvableType(), type.getType(), bindingResult); + key, entry.getValue(), false, targetType, typeDescriptor.getType(), bindingResult); + try { if (value != null) { beanWrapper.setPropertyValue(key, value); @@ -320,18 +330,24 @@ public class GraphQlArgumentBinder { @SuppressWarnings("unchecked") @Nullable private T convertValue( - String name, @Nullable Object rawValue, Class type, ArgumentsBindingResult bindingResult) { + String name, @Nullable Object rawValue, ResolvableType type, Class clazz, + ArgumentsBindingResult bindingResult) { Object value = null; try { - TypeConverter converter = (this.typeConverter != null ? this.typeConverter : new SimpleTypeConverter()); - value = converter.convertIfNecessary(rawValue, (Class) type); + TypeConverter converter = + (this.typeConverter != null ? this.typeConverter : new SimpleTypeConverter()); + + value = converter.convertIfNecessary( + rawValue, (Class) clazz, + (type.getSource() instanceof MethodParameter param ? new TypeDescriptor(param) : null)); } catch (TypeMismatchException ex) { bindingResult.pushNestedPath(name); bindingResult.rejectValue(rawValue, ex.getErrorCode(), "Failed to convert argument value"); bindingResult.popNestedPath(); } + return (T) value; } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java index eff8ab73..725960d5 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java @@ -16,6 +16,8 @@ package org.springframework.graphql.data; +import java.lang.reflect.Method; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -31,8 +33,11 @@ import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingEnvironmentImpl; import org.junit.jupiter.api.Test; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.graphql.Book; +import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.lang.Nullable; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; @@ -51,7 +56,7 @@ class GraphQlArgumentBinderTests { private final ObjectMapper mapper = new ObjectMapper(); - private final GraphQlArgumentBinder binder = new GraphQlArgumentBinder(); + private final GraphQlArgumentBinder binder = new GraphQlArgumentBinder(new DefaultFormattingConversionService()); @Test @@ -113,6 +118,24 @@ class GraphQlArgumentBinderTests { assertThat(((ItemListHolder) result).getItems()).hasSize(0); } + @Test // gh-349 + void dataBindingToBeanWithEnumGenericType() throws Exception { + + Map argumentMap = + Collections.singletonMap("filter", Collections.singletonMap("enums", Arrays.asList("ONE", "TWO"))); + + Method method = EnumController.class.getMethod("enums", EnumInput.class); + ResolvableType targetType = ResolvableType.forMethodParameter(new MethodParameter(method, 0)); + + Object result = this.binder.bind( + DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(argumentMap).build(), + "filter", targetType); + + assertThat(result).isNotNull().isInstanceOf(EnumInput.class); + EnumInput input = (EnumInput) result; + assertThat(input.getEnums()).hasSize(2).containsExactly(FancyEnum.ONE, FancyEnum.TWO); + } + @Test // gh-280 void dataBindingBindingError() { assertThatThrownBy( @@ -329,6 +352,24 @@ class GraphQlArgumentBinderTests { Collections.singletonMap("name", "second")); } + @Test // gh-349 + void primaryConstructorWithEnumGenericType() throws Exception { + + Map argumentMap = + Collections.singletonMap("filter", Collections.singletonMap("enums", Arrays.asList("ONE", "TWO"))); + + Method method = EnumController.class.getMethod("enums", ConstructorEnumInput.class); + ResolvableType targetType = ResolvableType.forMethodParameter(new MethodParameter(method, 0)); + + Object result = this.binder.bind( + DataFetchingEnvironmentImpl.newDataFetchingEnvironment().arguments(argumentMap).build(), + "filter", targetType); + + assertThat(result).isNotNull().isInstanceOf(ConstructorEnumInput.class); + ConstructorEnumInput input = (ConstructorEnumInput) result; + assertThat(input.enums()).hasSize(2).containsExactly(FancyEnum.ONE, FancyEnum.TWO); + } + @SuppressWarnings("unchecked") @Nullable private Object bind(String json, ResolvableType targetType) throws Exception { @@ -570,4 +611,39 @@ class GraphQlArgumentBinderTests { } } + + static class EnumController { + + public List enums(@Argument EnumInput filter) { + return filter.getEnums(); + } + + public List enums(@Argument ConstructorEnumInput filter) { + return filter.enums(); + } + } + + + enum FancyEnum { + ONE, TWO, THREE + } + + + static class EnumInput> { + + private List enums; + + public List getEnums() { + return enums; + } + + public void setEnums(List enums) { + this.enums = enums; + } + } + + + record ConstructorEnumInput>(List enums) { + } + } \ No newline at end of file