apache/flink · error · InvalidTypesException

POJO type expected but was: {}

Error message

POJO type expected but was: {}

What it means

Thrown by Types.POJO(Class) when Flink's TypeExtractor cannot extract a valid PojoTypeInfo from the given class. The analyzer fell back to a different TypeInformation (typically GenericTypeInfo/Kryo, or TupleTypeInfo, or a primitive type) because the class violated one or more POJO requirements: not public, a non-static inner class, missing a public no-arg constructor, or fields that are neither public-and-non-final nor properly getter/setter accessible.

Source

Thrown at flink-core/src/main/java/org/apache/flink/api/common/typeinfo/Types.java:301

     * <p>The generic types for all fields of the POJO can be defined in a hierarchy of subclasses.
     *
     * <p>Java Record classes can also be used as valid POJOs (even though they don't fulfill some
     * of the above criteria). In this case Flink will use the record canonical constructor to
     * create the objects.
     *
     * <p>If Flink's type analyzer is unable to extract a valid POJO type information with type
     * information for all fields, an {@link
     * org.apache.flink.api.common.functions.InvalidTypesException} is thrown. Alternatively, you
     * can use {@link Types#POJO(Class, Map)} to specify all fields manually.
     *
     * @param pojoClass POJO class to be analyzed by Flink
     */
    public static <T> TypeInformation<T> POJO(Class<T> pojoClass) {
        final TypeInformation<T> ti = TypeExtractor.createTypeInfo(pojoClass);
        if (ti instanceof PojoTypeInfo) {
            return ti;
        }
        throw new InvalidTypesException("POJO type expected but was: " + ti);
    }

    /**
     * Returns type information for a POJO (Plain Old Java Object) and allows to specify all fields
     * manually.
     *
     * <p>A type is considered a FLink POJO type, if it fulfills the conditions below.
     *
     * <ul>
     *   <li>It is a public class, and standalone (not a non-static inner class)
     *   <li>It has a public no-argument constructor.
     *   <li>All non-static, non-transient fields in the class (and all superclasses) are either
     *       public (and non-final) or have a public getter and a setter method that follows the
     *       Java beans naming conventions for getters and setters.
     *   <li>It is a fixed-length, null-aware composite type with non-deterministic field order.
     *       Every field can be null independent of the field's type.
     * </ul>
     *

View on GitHub (pinned to 2f3c205e92)

Solutions

  1. Ensure the class is public, top-level or static-inner, and has a public no-argument constructor.
  2. Make every non-static non-transient field either public (and non-final) or provide public JavaBeans-compliant getter and setter for it.
  3. If a field's type cannot be auto-resolved, use Types.POJO(Class, Map) to specify all fields and their TypeInformation manually.
  4. If the class genuinely cannot be made a POJO, use Types.GENERIC(Class) to accept Kryo serialization, or switch to a Tuple/Row/POJO-registered alternative.
  5. Run TypeExtractor.createTypeInfo(MyClass.class) in a unit test and inspect the returned TypeInformation to see what Flink inferred instead.

Example fix

// before — private fields, no accessors
public class Event {
    private String id;
    private long ts;
}
TypeInformation<Event> ti = Types.POJO(Event.class); // throws

// after — public fields or getters/setters + no-arg constructor
public class Event {
    public String id;
    public long ts;
    public Event() {}
}
TypeInformation<Event> ti = Types.POJO(Event.class); // ok

// alternative — specify fields manually
Map<String, TypeInformation<?>> fields = new HashMap<>();
fields.put("id", Types.STRING);
fields.put("ts", Types.LONG);
TypeInformation<Event> ti = Types.POJO(Event.class, fields);
Defensive patterns

Strategy: type-guard

Validate before calling

// Validate POJO requirements before calling Types.POJO
public static boolean isLikelyValidPojo(Class<?> clazz) {
    int modifiers = clazz.getModifiers();
    if (!Modifier.isPublic(modifiers)) return false;
    if (clazz.isMemberClass() && !Modifier.isStatic(modifiers)) return false;
    try {
        clazz.getConstructor(); // public no-arg
    } catch (NoSuchMethodException e) {
        return false;
    }
    return true;
}

if (isLikelyValidPojo(MyType.class)) {
    TypeInformation<MyType> ti = Types.POJO(MyType.class);
}

Type guard

// Type guard that inspects extracted type info before assuming POJO
TypeInformation<MyType> extracted = TypeExtractor.createTypeInfo(MyType.class);
if (extracted instanceof PojoTypeInfo) {
    PojoTypeInfo<MyType> pojo = (PojoTypeInfo<MyType>) extracted;
    // safe to use as POJO
} else {
    // fall back to Types.GENERIC or Types.POJO(Class, Map)
}

Try / catch

try {
    TypeInformation<MyType> ti = Types.POJO(MyType.class);
} catch (InvalidTypesException e) {
    // log and fall back to manual POJO spec or GENERIC
    Map<String, TypeInformation<?>> fields = Map.of(
        "id", Types.STRING,
        "ts", Types.LONG
    );
    ti = Types.POJO(MyType.class, fields);
}

Prevention

When it happens

Trigger: Calling Types.POJO(MyClass.class) where MyClass is a non-static inner class, has no public no-arg constructor, has private fields without JavaBeans getters/setters, has final fields, or has interface/generic fields the analyzer cannot resolve. Also triggered when the class is an enum, an interface, an array type, or is itself recognized as a Tuple subclass.

Common situations: Defining a DataStream or Table sink POJO with private fields and no getters/setters. Using a non-static inner class as a POJO. Annotating a class with Lombok @Builder without @NoArgsConstructor. Having a field whose type is Object or an unbounded generic. Migrating from Tuple to POJO and forgetting to add accessors.

Related errors


AI-assisted analysis of apache/flink@2f3c205e92 (2026-08-14). Data as JSON: /api/errors/9f4813219514b82e. Report an issue: GitHub.