Skip to content

NPE in AnnotationFields when entity extends a generic @MappedSuperclass more than one level deep #3801

Description

@dragkes

Bug

A NullPointerException occurs at startup when an @Entity class extends a generic @MappedSuperclass through two or more levels of inheritance (e.g. Concrete extends Middle<Long> / Middle<T> extends Base<T>).

Stack trace

Caused by: java.lang.NullPointerException: Cannot invoke "java.lang.Class.isEnum()"
    because the return value of "io.ebeaninternal.server.deploy.meta.DeployBeanProperty.getPropertyType()" is null
    at io.ebeaninternal.server.deploy.parse.AnnotationFields.readField(AnnotationFields.java:121)
    at io.ebeaninternal.server.deploy.parse.AnnotationFields.parse(AnnotationFields.java:62)
    at io.ebeaninternal.server.deploy.parse.ReadAnnotations.readInitial(ReadAnnotations.java:29)

Reproducer

@MappedSuperclass
public abstract class GenericBaseModel<T> extends Model {
    @Id T id;
    // ...
}

@MappedSuperclass
public abstract class GenericMiddleModel<T> extends GenericBaseModel<T> {
    // intermediate layer
}

@Entity
@Table(name = "middle_product")
public class ProductWithGenericMiddle extends GenericMiddleModel<Long> {
    String name;
}

Ebean fails to start with the NPE above. The single-level case (Concrete extends GenericBaseModel<Long> directly) works correctly.

Root cause

Introduced in ebc90e0. DeployCreateProperties.mapGenerics(Class) built the TypeVariable → Class map by reading only the direct generic superclass of each class as the recursion walked up the hierarchy. For a chain like:

ProductWithGenericMiddle  extends  GenericMiddleModel<Long>
GenericMiddleModel<T>     extends  GenericBaseModel<T>
GenericBaseModel<T>       { @Id T id; }

When the recursion reached GenericBaseModel, it called mapGenerics(GenericMiddleModel). GenericMiddleModel.getGenericSuperclass() is GenericBaseModel<T> - where T is still an unresolved TypeVariable. resolveToClass(TypeVariable) returned null, so the map was empty. genericTypeMap.get(id.getGenericType()) then returned null, leaving propertyType = null. This null was stored in the DeployBeanProperty and caused the NPE in AnnotationFields.

Fix

Build the full type-variable map once for the concrete bean type using TypeResolver.getTypeVariableMap, which walks the entire superclass/interface hierarchy and composes TypeVariable bindings transitively (Middle.T → Long then Base.T → resolve(Middle.T) → Long). The same map is reused at every level of the recursive createProperties walk, so any TypeVariable at any depth in the hierarchy resolves correctly.

Additionally:

  • The map type is widened from Map<TypeVariable<?>, Class<?>> to Map<TypeVariable<?>, Type> so intermediate ParameterizedType values (e.g. List<T>) are preserved and resolved transitively rather than being discarded.
  • A null-safety fallback (propertyType = field.getType() when resolution yields null) prevents the NPE even for unresolvable type variables.
  • Type-resolution helpers (resolveType, resolveToClass, resolveCollectionTarget) are consolidated in TypeReflectHelper for reuse.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions