quarkusio/quarkus · error · RuntimeException

Unable to determine the recordable constructor to use for ${

Error message

Unable to determine the recordable constructor to use for ${param.getClass()}

What it means

When serializing an object whose class has no default constructor and no explicitly registered recordable constructor, BytecodeRecorderImpl tries to infer which constructor to use. If multiple constructors share the same (largest) parameter count, or no constructor could be selected, the choice is ambiguous or impossible and recording fails. Quarkus requires an unambiguous way to reconstruct the object at runtime.

Source

Thrown at core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java:1248

            for (int i = 0; i < params.size(); i++) {
                Object obj = params.get(i);
                nonDefaultConstructorHandles[i] = loadObjectInstance(obj, existing,
                        parameterTypes[count++], relaxedValidation);
            }
            extractConstructorParameterNames(nonDefaultConstructorHolder.constructor, constructorParamNameMap);
        } else if (classesToUseRecordableConstructor.contains(param.getClass())) {
            Constructor<?> current = null;
            int count = 0;
            for (var c : param.getClass().getConstructors()) {
                if (current == null || current.getParameterCount() < c.getParameterCount()) {
                    current = c;
                    count = 0;
                } else if (current.getParameterCount() == c.getParameterCount()) {
                    count++;
                }
            }
            if (current == null || count > 0) {
                throw new RuntimeException("Unable to determine the recordable constructor to use for " + param.getClass());
            }

            nonDefaultConstructorHolder = new NonDefaultConstructorHolder(current, null);
            nonDefaultConstructorHandles = new DeferredParameter[current.getParameterCount()];
            extractConstructorParameterNames(current, constructorParamNameMap);
        } else {
            Constructor<?>[] ctors = param.getClass().getConstructors();
            Constructor<?> selectedCtor = null;
            if (ctors.length == 1) {
                // if there is a single constructor we use it regardless of the presence of @RecordableConstructor annotation
                selectedCtor = ctors[0];
            }
            for (Constructor<?> ctor : ctors) {
                if (RecordingAnnotationsUtil.isRecordableConstructor(ctor)) {
                    selectedCtor = ctor;
                    break;
                }
            }

View on GitHub (pinned to e1c734241f)

Solutions

  1. Annotate the intended constructor with jakarta.inject.Inject so the recorder picks it explicitly
  2. Ensure the class has a single public constructor (or one unique by parameter count)
  3. Add a default (no-arg) constructor if the state can be set via setters/getters

Example fix

// before
public Point(int x, int y) {...}
public Point(int x) {...}
// after
@Inject
public Point(int x, int y) {...}
public Point(int x) {...}
Defensive patterns

Strategy: validation

Validate before calling

Constructor<?>[] ctors = clazz.getConstructors();
long max = Arrays.stream(ctors).mapToInt(Constructor::getParameterCount).max().orElse(-1);
long matches = Arrays.stream(ctors).filter(c -> c.getParameterCount() == max).count();
if (matches != 1 && Arrays.stream(ctors).noneMatch(c -> c.isAnnotationPresent(Inject.class))) {
    throw new IllegalStateException(clazz + " has no unambiguous recordable constructor");
}

Type guard

boolean hasUnambiguousCtor(Class<?> c) {
    Constructor<?>[] ctors = c.getConstructors();
    if (Arrays.stream(ctors).anyMatch(k -> k.isAnnotationPresent(Inject.class))) return true;
    int max = Arrays.stream(ctors).mapToInt(Constructor::getParameterCount).max().orElse(-1);
    return Arrays.stream(ctors).filter(k -> k.getParameterCount() == max).count() == 1;
}

Try / catch

try {
    recorder.record(value);
} catch (RuntimeException e) {
    if (String.valueOf(e.getMessage()).startsWith("Unable to determine the recordable constructor")) {
        throw new IllegalStateException("Add @Inject to one constructor of " + value.getClass(), e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Passing an object of a class with several constructors of equal parameter count (and no @Inject annotation and no registered RecordableConstructor) into a @Record method; passing an object with zero accessible constructors it can disambiguate.

Common situations: Value/config-holder classes in extensions with overloaded constructors; adding an extra convenience constructor to a class that previously had one, breaking the unique-parameter-count heuristic after an upgrade.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/8870dab645046c8b. Report an issue: GitHub.