-
Notifications
You must be signed in to change notification settings - Fork 181
5. Common errors and solutions
This page lists commonly encountered exceptions, their detailed descriptions and possible solutions.
If the mapping process encounters a field or a method of a type that is:
- missing generic type parameters (e.g.
List
instead ofList<Item>
) - an unbounded wildcard type (e.g.
List<?>
) - an unresolved type variable (e.g.
List<T>
whereT
is unresolvable) aTypeMappingException
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 ofT
(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)
AnnotatedType
s can also be constructed dynamically usingTypeFactory
. -
Similarly, the
itemNames
field is impossible to map because it is completely missing the generic type ofList
. 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 aTypeTransformer
:schemaGenerator.withTypeTransformer(new DefaultTypeTransformer(true, true)) //replace raw and unbounded type with Object
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).
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.
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())
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())
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.