quarkusio/quarkus · error · RuntimeException
Failed to substitute ${param}
Error message
Failed to substitute ${param} What it means
When a recorder parameter's type has an ObjectSubstitution registered, BytecodeRecorderImpl instantiates the substitution and calls serialize(); any exception in that path is wrapped as 'Failed to substitute <param>'. The root cause is inside the custom ObjectSubstitution implementation (or reflection on it), not in Quarkus.
Source
Thrown at core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java:663
return new DeferredArrayStoreParameter(param, expectedType) {
@Override
void doPrepare(MethodContext context) {
serialized.prepare(context);
super.doPrepare(context);
}
@Override
ResultHandle createValue(MethodContext creator, MethodCreator method, ResultHandle array) {
ResultHandle subInstance = method.newInstance(MethodDescriptor.ofConstructor(finalHolder.sub));
return method.invokeInterfaceMethod(
ofMethod(ObjectSubstitution.class, "deserialize", Object.class, Object.class), subInstance,
creator.loadDeferred(serialized));
}
};
} catch (Exception e) {
throw new RuntimeException("Failed to substitute " + param, e);
}
} else if (param instanceof Optional) {
Optional val = (Optional) param;
if (val.isPresent()) {
DeferredParameter res = loadObjectInstance(val.get(), existing, Object.class, relaxedValidation);
return new DeferredArrayStoreParameter(param, expectedType) {
@Override
void doPrepare(MethodContext context) {
res.prepare(context);
super.doPrepare(context);
}
@Override
ResultHandle createValue(MethodContext context, MethodCreator method, ResultHandle array) {
// If the value is a proxy, it may be non-null at build time but become null
// when we actually create the value during initialization;View on GitHub (pinned to e1c734241f)
Solutions
- Look at the 'Caused by' stack trace to find the exception inside your ObjectSubstitution.serialize()
- Make sure the substitution class has a public no-arg constructor (it is instantiated reflectively)
- Make serialize() handle null/edge-case fields defensively and only capture serializable state
- Verify the substitution's serialize/deserialize are symmetrical so the generated bytecode can rebuild the object at runtime
- Log the offending param's state before serialize() to spot which field is problematic
Example fix
// before
public Foo serialize(Foo t) { return new Foo(t.inner.name); } // NPE when inner==null
// after
public Foo serialize(Foo t) {
return new Foo(t.inner == null ? null : t.inner.name);
} Defensive patterns
Strategy: validation
Validate before calling
// before relying on a substitution ObjectSubstitution<Foo, FooData> sub = new MyFooSubstitution(); FooData data = sub.serialize(testFoo); // run at build step time to surface failures early assert sub.deserialize(data) != null : "serialize/deserialize not symmetrical";
Type guard
static <F, T> boolean isSubstitutable(Object param, Class<F> from) {
return from.isInstance(param);
} Try / catch
try {
recorder.accept(foo);
} catch (RuntimeException e) {
throw new IllegalStateException("Substitution failed for " + foo.getClass(), e.getCause());
} Prevention
- Give substitution classes a public no-arg constructor
- Make serialize() null-safe and symmetric with deserialize()
- Unit-test substitutions at build time with representative objects
- Re-test substitutions after refactoring the substituted type
When it happens
Trigger: A recorder method receives an object whose class (or declared expectedType) has a registered ObjectSubstitution, and substitution.serialize(param) throws — e.g. NullPointerException on a null field, missing no-arg constructor on the substitution class, or a checked exception during serialization.
Common situations: Extension authors writing custom ObjectSubstitution classes; substitutions broken after refactoring the substituted type (renamed/removed fields); substitution class lacking a public no-arg constructor.
Related errors
- Failed to record call to method ${call.method}
- Failed to open path tree with root %s
- Dev services for ${request.getName()} requires a startable s
- Name cannot start with '/':${name}
- The class (${name}) cannot be created during deployment.
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/b30cfa7cd755364f.
Report an issue: GitHub.