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

Errors with inputs using extended generics

Open shepherd85 opened this issue 10 months ago • 3 comments

I'm trying to write some filter input classes, but it throws errors when generic types include "extends":

public class Filter<FieldType> {
  public FieldType value;
  public FieldType[] values;
  public Operator op=Operator.EQUAL;

  public Predicate toPredicate(...) {
    ... create a predicate ...
  }
}

public class StringFilter extends Filter<String> { // Works
  @Override
  public Predicate toPredicate(...) {
    ... custom predicate options for strings specifically ...
    ... or super.toPredicate(...) ...
  }
}


public class NumberFilter<NumberType extends Number> extends Filter<NumberType> {
  @Override
  public Predicate toPredicate(...) {
    ... custom predicate options for number types specifically ...
    ... or super.toPredicate(...) ...
  }
}

public class IntFilter extends Filter<Integer> {} // works
public class IntFilter extends NumberFilter<Integer> {} // but this one errors:
// io.smallrye.graphql.schema.SchemaBuilderException: 
//   Don't know what to do with [FieldType] of kind [TYPE_VARIABLE] as parent object reference doesn't contain necessary info: 
// Reference{
//   className=<pkg>.v1.query.filters.IntFilter,
//   name=IntFilterInput,
//   type=INPUT,
//   graphQLClassName=null,
//   adaptTo=null,
//   adaptWith=null, 
//   directiveInstances=null,
//   classParametrizedTypesnull,
//   extendedClassParametrizedTypes{
//     NumberType=Reference{
//       className=java.lang.Integer, 
//       name=Int,
//       type=SCALAR,
//       graphQLClassName=java.lang.Integer,
//       adaptTo=null,
//       adaptWith=null, 
//       directiveInstances=null,
//       classParametrizedTypesnull,
//       extendedClassParametrizedTypesnull,
//       wrapper=null
//     }
//   }, wrapper=null
// }

Is this a bug, or is there a better way to handle this scenario, without boilerplate code for each number scalar?

shepherd85 avatar Feb 07 '25 15:02 shepherd85

@jmartisk ^^^

phillip-kruger avatar Feb 12 '25 22:02 phillip-kruger

This looks like the bug that I described in one of the PRs: https://github.com/smallrye/smallrye-graphql/pull/2027#issuecomment-1932005310

But I don't think the issue is the extends per se in the NumberFilter<NumberType extends Number>, it's that the Type Variables are stored within the map and are stored as NumberType => Integer. So when the Jandex index is in the Filter and it tries to look into the map, it won't find the FieldType type.

But I think that the overall generic parent processing is a bit unfortunate. I will give it a try tomorrow with a better solution.

mskacelik avatar Aug 25 '25 14:08 mskacelik

ReferenceCreator

The main issues are in the ReferenceCreator class, particularly in these branches:

} else if (fieldType.kind().equals(Type.Kind.CLASS)) {
  // ...
} else if (fieldType.kind().equals(Type.Kind.PARAMETERIZED_TYPE)) {
  // ...
} else if (fieldType.kind().equals(Type.Kind.TYPE_VARIABLE)) {
  // ...
}

CLASS branch

A CLASS branch handles a non-parameterized class, for example:

MyClass a;

class MyClass extends MyClassParent<Integer> implements Attribute<Long> {}

Problem:

  • findParametrizedParentType only searches for the first parameterized superclass. This makes type-variable mapping effectively impossible when the hierarchy includes multiple parameterized classes.

Ideally, it should return a list of all parameterized superclasses (flatten their argument types).

Note:

  • collectParametrizedTypes, funnily enough, already collects type variables across the entire hierarchy (see collectTypeVariables).

Another problem in collectParametrizedTypes:

  • The map created (String -> Reference) stores type-variable identifiers as keys, e.g., for MyClassParent<A> it saves A -> Integer.
  • This breaks when there are multiple parameterized supertypes/interfaces because a type-variable identifier like A is not globally unique.

Example of conflicting mappings:

class MyClass extends MyClassParent<Integer> implements Attribute<Long> { }
class MyClassParent<A> extends MySuperClassParent<Double> {}
class MySuperClassParent<A> {}

Here, type variable A collides: once mapped to Integer, and elsewhere to Double.

PARAMETERIZED_TYPE branch

This branch handles any parameterized class except Maps and Collections (filtered earlier).

Problem:

  • It collects the type arguments of the class itself, but not those of its superclasses.
MyParametrizedClass<Long> x;

class MyParametrizedClass<T> extends MyParametrizedClass<Double> { }

It collects Long but not Double, breaking some type-variable mappings. It also inherits the same uniqueness problem as the CLASS branch.

Parameterized types differentiate between their own arguments and extended superclass arguments. This is why the current code splits classParametrizedTypes and extendedClassParametrizedTypes (e.g., to distinguish MyClass<Integer> vs MyClass<Long> in final schema names like MyClass_Integer, MyClass_BigInteger).

This is in retrospect a bit unfortunate implementation, I would probably change it to a single map: String -> ClassParametrizedReference, e.g.:

private boolean isExtendedType;
private Reference reference;

A single map is less confusing; filter entries by need.

TYPE_VARIABLE branch

Example:

class MyClass<A> {
  A aField;
}

Looks up the type-variable identifier A in parentObjectReference and replaces it with its resolved value.

Problem:

  • Only a single parameterized parent class is considered, which is insufficient for deeper/multiple parameterized hierarchies.

Possible solution ?

Use of class-qualified identifiers for type variables

  • Key format: fqcn-of-declaring-class + - + type-variable-name.
  • Example:
    MyClass<Long> x = ...
    class MyClass<T> extends MyParentClass<Double> {
      T tField;
    }
    class MyParentClass<T> {
      T tField1;
    }
    
    Mappings:
    • path.to.MyClass-T -> Long
    • path.to.MyParentClass-T -> Double
  • This prevents collisions and supports multiple parameterized supertypes. FQCN can be set in the FieldCreator by passing the field’s fully qualified declaring class name into createReference.

Status

I have attempted to fix the issue (for a couple of days now), but the required refactoring is non-trivial. Leaving these observations and a proposed direction for someone to continue. :(

TL;DR

  • Current implementation cannot reliably handle more than one parameterized inheritance. Collisions arise because type-variable identifiers are not unique.
  • It should be possible to support multiple parameterized inheritances by:
    • Traversing the full hierarchy (collect all parameterized supertypes)
    • Using class-qualified type-variable keys.

mskacelik avatar Aug 29 '25 13:08 mskacelik