apache/pulsar · error · IllegalArgumentException

The message payload processor class %s does not implement th

Error message

The message payload processor class %s does not implement the desired constructor.

What it means

Thrown by ValidatorUtils.validateMessagePayloadProcessor when a configured MessagePayloadProcessor class does not declare the constructor required by its config. If MessagePayloadProcessorConfig.config is null or empty, the class must have a no-arg constructor; otherwise it must have a single-argument constructor taking a java.util.Map. The check runs at function-submission time via ByteBuddy's TypePool (bytecode inspection, no instantiation).

Source

Thrown at pulsar-functions/utils/src/main/java/org/apache/pulsar/functions/utils/ValidatorUtils.java:132

                    String.format("The message payload processor class %s does not exist", payloadProcessorClassName));
        }
        if (!payloadProcessorClass.asErasure().isAssignableTo(MessagePayloadProcessor.class)) {
            throw new IllegalArgumentException(String.format("%s does not implement %s", payloadProcessorClassName,
                    MessagePayloadProcessor.class.getName()));
        }

        boolean hasConstructor;
        if (conf.getConfig() == null || conf.getConfig().isEmpty()) {
            hasConstructor = payloadProcessorClass.getDeclaredMethods().stream()
                    .anyMatch(method -> method.isConstructor() && method.getParameters().size() == 0);
        } else {
            hasConstructor = payloadProcessorClass.getDeclaredMethods().stream()
                    .anyMatch(method -> method.isConstructor() && method.getParameters().size() == 1
                            && method.getParameters().get(0).getType().asErasure().represents(Map.class));
        }

        if (!hasConstructor) {
            throw new IllegalArgumentException(
                    String.format("The message payload processor class %s does not implement the desired constructor.",
                            conf.getClassName()));
        }
    }

    public static void validateSerde(String inputSerializer, TypeDefinition typeArg, TypePool typePool,
                                     boolean deser) {
        if (isEmpty(inputSerializer)) {
            return;
        }
        if (inputSerializer.equals(DEFAULT_SERDE)) {
            return;
        }
        TypeDescription serdeClass;
        try {
            serdeClass = typePool.describe(inputSerializer).resolve();
        } catch (TypePool.Resolution.NoSuchTypeException e) {
            throw new IllegalArgumentException(

View on GitHub (pinned to 820761864e)

Solutions

  1. If you pass no config, add an explicit public no-arg constructor to the MessagePayloadProcessor class.
  2. If you pass config (conf.setConfig(...)), add a public constructor accepting a single java.util.Map<String, Object> (or Map<String, String>) parameter.
  3. Ensure the constructor is declared directly on the class, not inherited from a superclass.
  4. If the constructor exists but has a subtypes of Map parameter, change it to take exactly java.util.Map (raw or matching erasure) as its sole parameter.

Example fix

// before: processor with config supplied but no matching constructor
public class MyProcessor implements MessagePayloadProcessor {
    public MyProcessor(String topic) { ... }
}
// after
public class MyProcessor implements MessagePayloadProcessor {
    public MyProcessor() { ... }
    public MyProcessor(Map<String, Object> config) { ... }
}
Defensive patterns

Strategy: validation

Validate before calling

static boolean hasDesiredConstructor(String className, Map<String, String> config, ClassLoader cl) {
    try {
        Class<?> c = Class.forName(className, false, cl);
        boolean wantsMap = config != null && !config.isEmpty();
        for (Constructor<?> ctor : c.getDeclaredConstructors()) {
            if (wantsMap) {
                if (ctor.getParameterCount() == 1 && Map.class.isAssignableFrom(ctor.getParameterTypes()[0])) return true;
            } else if (ctor.getParameterCount() == 0) {
                return true;
            }
        }
        return false;
    } catch (ClassNotFoundException e) { return false; }
}

Type guard

static boolean isMessagePayloadProcessorWithCtor(Class<?> c, boolean withConfig) {
    return MessagePayloadProcessor.class.isAssignableFrom(c)
        && (withConfig
            ? java.util.Arrays.stream(c.getDeclaredConstructors()).anyMatch(k -> k.getParameterCount() == 1 && Map.class.isAssignableFrom(k.getParameterTypes()[0]))
            : java.util.Arrays.stream(c.getDeclaredConstructors()).anyMatch(k -> k.getParameterCount() == 0));
}

Try / catch

try {
    functionConfigBuilder.validate();
} catch (IllegalArgumentException e) {
    if (e.getMessage() != null && e.getMessage().contains("does not implement the desired constructor")) {
        log.error("Payload processor {} needs a {} constructor", className, configEmpty ? "no-arg" : "Map", e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling validateMessagePayloadProcessor(conf, typePool) where conf.getClassName() resolves to a MessagePayloadProcessor subclass, the class exists and implements MessagePayloadProcessor, but: (a) conf.getConfig() is null/empty and the class has no declared no-arg constructor, or (b) conf.getConfig() is non-empty and the class lacks a constructor with exactly one Map parameter. Note the check inspects getDeclaredMethods(), so inherited constructors do not count.

Common situations: Declaring a payload processor that only has a constructor taking custom types (e.g. a String) while supplying config; supplying windowConfig config properties but the processor only defines a no-arg constructor; the processor relying on its superclass's constructor; Lombok or other annotation processors removing/altering the implicit default constructor.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/169d1ca67003e4c3. Report an issue: GitHub.