quarkusio/quarkus · error · RuntimeException

Unknown recorder constructor parameter: %s in recorder %s

Error message

Unknown recorder constructor parameter: %s in recorder %s

What it means

Recorder constructor parameters that are generic (ParameterizedType) must be RuntimeValue<T>; any other parameterized type is unsupported. Quarkus unwraps RuntimeValue's type argument and otherwise cannot map the generic type to a runtime-injectable value, so it throws during loadStepsFromClass.

Source

Thrown at core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java:575

                        }
                        methodParamFns.add((bc, bri) -> {
                            assert bri != null;
                            return bri.getRecordingProxy(parameterClass);
                        });
                        //now look for recorder parameter injection
                        //as we now inject config directly into recorders we need to look at the constructor params
                        Constructor<?>[] ctors = parameter.getType().getDeclaredConstructors();
                        for (var ctor : ctors) {
                            if (ctors.length == 1 || ctor.isAnnotationPresent(Inject.class)) {
                                for (var type : ctor.getGenericParameterTypes()) {
                                    Class<?> theType;
                                    boolean isRuntimeValue = false;
                                    if (type instanceof ParameterizedType pt) {
                                        if (pt.getRawType().equals(RuntimeValue.class)) {
                                            theType = (Class<?>) pt.getActualTypeArguments()[0];
                                            isRuntimeValue = true;
                                        } else {
                                            throw new RuntimeException("Unknown recorder constructor parameter: " + type
                                                    + " in recorder " + parameter.getType());
                                        }
                                    } else {
                                        theType = (Class<?>) type;
                                    }
                                    ConfigRoot annotation = theType.getAnnotation(ConfigRoot.class);
                                    if (annotation != null) {
                                        methodConsumingConfigPhases.add(recordAnnotation.value() == ExecutionTime.STATIC_INIT
                                                ? ConfigPhase.BUILD_AND_RUN_TIME_FIXED
                                                : annotation.phase());
                                        if (annotation.phase().isAvailableAtBuild() && !annotation.phase().isAvailableAtRun()) {
                                            throw reportError(parameter, annotation.phase() + " configuration "
                                                    + type.getTypeName()
                                                    + " cannot be consumed in a Recorder");
                                        } else if (annotation.phase().isReadAtMain() && !isRuntimeValue) {
                                            throw reportError(parameter, annotation.phase() + " configuration "
                                                    + type.getTypeName()
                                                    + " can only be injected in a @Recorder constructor as a RuntimeValue<"

View on GitHub (pinned to e1c734241f)

Solutions

  1. Change the parameter type to RuntimeValue<T> if a generic wrapped value is intended.
  2. Use a non-generic class type for the constructor parameter (e.g. a custom config class annotated @ConfigRoot, or a plain class).
  3. Pass the generic data at record time via a recorder method call instead of the constructor.

Example fix

// before
public MyRecorder(List<String> names) { ... }
// after
public MyRecorder(RuntimeValue<List<String>> names) { ... }
// or: use a concrete type
public MyRecorder(String[] names) { ... }
Defensive patterns

Strategy: validation

Validate before calling

for (Type t : ctor.getGenericParameterTypes()) {
    if (t instanceof ParameterizedType pt && !pt.getRawType().equals(RuntimeValue.class))
        throw new IllegalArgumentException("Unsupported generic recorder param: " + t);
}

Type guard

static boolean isAllowedRecorderParam(Type t) {
    if (t instanceof ParameterizedType pt) {
        return pt.getRawType().equals(RuntimeValue.class);
    }
    return t instanceof Class<?>;
}

Prevention

When it happens

Trigger: Declaring a recorder constructor parameter like List<String>, Optional<Foo>, Map<K,V> or any other parameterized type that is not RuntimeValue<T>; the raw type check pt.getRawType().equals(RuntimeValue.class) fails.

Common situations: Injecting collections or config-annotated generics into a Recorder constructor expecting Quarkus to resolve them; after upgrading Quarkus which tightened the allowed parameter types.

Related errors


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