Skip to content

5. Common errors and solutions

Balf edited this page May 5, 2024 · 1 revision

Commons errors and solutions

This page lists commonly encountered exceptions, their detailed descriptions and possible solutions.

TypeMappingException

Ambiguous member type

If the mapping process encounters a field or a method of a type that is:

  • missing generic type parameters (e.g. List instead of List<Item>)
  • an unbounded wildcard type (e.g. List<?>)
  • an unresolved type variable (e.g. List<T> where T is unresolvable) a TypeMappingException will be thrown by default.

Example:

public class ItemHolder<T> {

    public T[] getItems() {...}

    public List getItemNames() {...}
}

GraphQLSchema schema = new GraphQLSchemaGenerator()
                .withOperationsFromSingleton(new ItemHolder<Book>()) //no explicit singleton type provided
                .generate();
  • In this case, the items field is impossible to map because the type of T (Book in the example) is lost due to type erasure before the mapper can get a hold of it.

    Solution:

    Provide the type explicitly when registering an instance:

    //using TypeToken
    schemaGenerator.withOperationsFromSingleton(new ItemHolder<Book>(), new TypeToken<ItemHolder<Book>>(){}.getType())

    or

    //dynamically constructing the type
    schemaGenerator.withOperationsFromSingleton(new ItemHolder<Book>(), TypeFactory.parameterizedClass(ItemHolder.class, Book.class))

    The first approach uses the THC pattern to provide a type literal.
    The second approach can be used when the type has to be constructed dynamically.

    If the type should also contain annotations e.g. ItemHolder<@GraphQLNonNull Book>, a similar technique can be used:

    //HAS TO BE DECLARED AT THE TOP LEVEL AND NOT INLINE DUE TO A BUG IN JDK8:
    //https://stackoverflow.com/questions/39952812/why-annotation-on-generic-type-argument-is-not-visible-for-nested-type
    private static final AnnotatedType beanType = new TypeToken<ItemHolder<@GraphQLNonNull Book>>(){}.getAnnotatedType();
    //using TypeToken
    schemaGenerator.withOperationsFromSingleton(new ItemHolder<Book>(), beanType)

    AnnotatedTypes can also be constructed dynamically using TypeFactory.

  • Similarly, the itemNames field is impossible to map because it is completely missing the generic type of List. In case specifying the full type is impractical (e.g. the source is not available or modifiable), it is possible to transform the type prior to mapping.

    Solution:

    To treat all missing type arguments as Object or apply custom type transformation logic, supply a TypeTransformer:

    schemaGenerator.withTypeTransformer(new DefaultTypeTransformer(true, true)) //replace raw and unbounded type with Object

Ambiguous method parameter type

Similarly to the above, if the full type of a method parameter can not be detected from the declaring type, nor is it practical to directly declare the full type, it is possible to transform the type prior to mapping.

Solution:

E.g. to treat all missing types as Object, use:

schemaGenerator.withTypeTransformer(new DefaultTypeTransformer(true, true)) //replace raw and unbounded types with Object

This configures the DefaultTypeTransformer to treat all raw types and unresolvable type variables as Object (and Object type is by default mapped to a complex scalar).

Operation with multiple resolver methods of different types

If an operation has multiple resolver methods with different return types, a TypeMappingException is thrown by default as this situation usually arises out of accidental misconfiguration rather than intention.

Example:

@GraphQLQuery(name = "numbers")
public ArrayList<Long> getLongs(String paramOne) {...}

@GraphQLQuery(name = "numbers")
public LinkedList<Double> getDoubles(String paramTwo) {...}

In this example, both methods resolve the same query, numbers, but they return different types.

If this is intentional, GraphQL SPQR can be configured to automatically infer the most specific common super type and use that for mapping. For the example above, the inferred type would be AbstractList<Number>. This is disabled by default because it can lead to surprising results if used unconsciously.

Solution:

To enable type inference, call e.g.

GraphQLSchemaGenerator#withOperationBuilder(new DefaultOperationBuilder(DefaultOperationBuilder.TypeInference.LIMITED));

when generating the schema. This will still result in an exception if the detected types have no common ancestors except Object, Cloneable, Serializable, Comparable or Annotation.
To allow unrelated types and treat them as Object, use TypeInference.UNLIMITED instead.

Dynamic proxies

Spring and other frameworks implement their features by wrapping POJOs into dynamically generated proxies. When a proxied instance is inspected via reflection, the acquired information is likely to be misleading e.g. their class usually belongs to the default (nameless) package, unlike the original class. For this reason it is necessary to provide the correct type explicitly when registering such beans with the schema generator.

Solution:

Use the two (or three) argument version of withOperationsFromSingleton to provide the type explicitly:

schemaGenerator.withOperationsFromSingleton(bookServiceBean, BookService.class)

or, if the type is generic (and/or requires annotations), use TypeToken or TypeFactory:

schemaGenerator.withOperationsFromSingleton(genericServiceBean, new TypeToken<GenericService<Book>>(){}.getType())

Generic top-level singletons

When an instance used in withOperationsFromSingleton is of a generic type e.g. GenericService<Book>, the full type must be provided explicitly, because in most cases the generic type information can not be extracted via reflection.

Solution:

Use the two (or three) argument version of withOperationsFromSingleton to provide the type explicitly:

schemaGenerator.withOperationsFromSingleton(bookServiceBean, BookService.class)

or, if the type is generic (and/or requires annotations), use TypeToken or TypeFactory:

schemaGenerator.withOperationsFromSingleton(genericServiceBean, new TypeToken<GenericService<Book>>(){}.getType())

Runtime exceptions

Derived types

If an OutputConverter delegates to other converters via ResolutionEnvironment#convertOutput(Object, AnnotatedType), it must implement the DelegatingOutputConverter interface. This assures that the types needed for delegation (the 2nd argument to ResolutionEnvironment#convertOutput) are derived and cached during schema initialization and not during query execution. This is important because type derivation is usually expensive and introduces a significant overhead.

While converting a value, the converter can obtain the cached derived types via ResolutionEnvironment#getDerived(AnnotatedType) (or ResolutionEnvironment#getDerived(AnnotatedType, int)). If the converter does not implement DelegatingOutputConverter or returns no types from getDerivedTypes, calling ResolutionEnvironment#getDerived will result in an empty list or an exception.

See #250 for more details.