spring-graphql icon indicating copy to clipboard operation
spring-graphql copied to clipboard

@Argument Input Objects with Generic Type Fail with a Type Mismatch Error

Open crispydc opened this issue 3 years ago • 3 comments

We have a project built using spring-graphql version 1.0.0-M2 that includes filtering logic using input objects with fields of Enum generics. This approach works fine in M2, but when attempting to upgrade to M4 these Enum input fields were now always blank regardless of what was sent in the query. In the latest M6 version, queries including these fields now result in an INTERNAL_ERROR similar to this:

org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'filter' on field 'enums[0]': rejected value [ONE]; codes [typeMismatch.target.enums[0],typeMismatch.target.enums,typeMismatch.enums[0],typeMismatch.enums,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [target.enums[0],enums[0]]; arguments []; default message [enums[0]]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.lang.Enum' for property 'enums[0]'; nested exception is java.lang.IllegalArgumentException: The target type java.lang.Enum does not refer to an enum]

I was able to recreate the issue we're seeing on a clean spring-boot 2.7.0-M3 project with spring-boot-starter-graphql and the following:

enum.graphqls

type Query {}

extend type Query {
    enums(filter: EnumFilterInput!): [FancyEnum]
}

input EnumFilterInput {
    enums: [FancyEnum]
}

enum FancyEnum {
    ONE
    TWO
    THREE
}

EnumController.java

@Controller
public class EnumController {

    @QueryMapping
    public List<FancyEnum> enums(@Argument EnumFilterInput<FancyEnum> filter) {
        return filter.getEnums();
    }
}

EnumFilterInput.java

public class EnumFilterInput<E extends Enum<E>> {

    private List<E> enums;

    public List<E> getEnums() {
        return enums;
    }

    public void setEnums(List<E> enums) {
        this.enums = enums;
    }
}

FancyEnum.java

public enum FancyEnum {
    ONE, TWO, THREE
}

The above project on spring-boot 2.6.6 with spring-graphql 1.0.0-M2 works as expected with the following query:

query {
  enums(filter:{
    enums:[ONE]
  })
}

However on spring-boot 2.7.0-M3 and spring-boot-starter-graphql (1.0.0-M6) this query fails with the error given above.

Was this an intended change made to the project or is this an issue that can be resolved in a future release? If it is intended, is there a way to get something similar working that is properly supported by spring-graphql?

Thanks in advance for any help you can provide.

crispydc avatar Apr 08 '22 20:04 crispydc

Between M2 and M3 argument serialization changed from JSON encode/decode to using a DataBinder, which caused some things to work differently, or not work at all anymore. I experienced something similar with a custom OptionalInput generic type.

We were able to work around it by defining a static of(value) method for every generic type argument.

sealed class OptionalInput<out T> {

    // ...

    companion object {
        @JvmStatic
        fun of(value: String?): OptionalInput<String> {
            return Defined(value)
        }
    }
}

Or I think the equivalent java code:

class OptionalInput<T> {
    // ...

    static OptionalInput<String> of(String value) {
        return new OptionalInput.Defined(value);
    }
}

Definitely not perfect, and also doesn't work recursively, but solved the majority of the issues we had with it.

koenpunt avatar Apr 13 '22 13:04 koenpunt

See also the comments on this PR: https://github.com/spring-projects/spring-graphql/pull/140

koenpunt avatar Apr 13 '22 14:04 koenpunt

Indeed as @koenpunt pointed out, we switched from JSON (de)-serialization to using DataBinder for the reasons explained in #122.

Given that DataBinder is only has the actual object to populate, in this case created through the default constructor, it does not the actual enum type to convert to. I confirmed that it works with a class that declares the generic parameter type:

public class FancyEnumFilterInput extends EnumFilterInput<FancyEnum> {
}

@Controller
public class EnumController {

	@QueryMapping
	public List<FancyEnum> enums(@Argument FancyEnumFilterInput filter) {
		return filter.getEnums();
	}

}

The problem relates more generally to any class-level, generic parameter. We might be able to register a TypeDescriptor and ConvertingPropertyEditorAdapter on the DataBinder for fields whose type is class-level, generic parameter, where the type information would come from the method parameter declaration.

rstoyanchev avatar Apr 13 '22 19:04 rstoyanchev