quarkusio/quarkus · error · RuntimeException

Cannot serialise field '${i.getName()}' on object '${param}'

Error message

Cannot serialise field '${i.getName()}' on object '${param}' as the property is read only

What it means

Bytecode recording serializes an object by reading its getters and writing the values so they can be replayed via setters at runtime. If a getter's property has no matching setter AND a read-only backing field exists, the recorder cannot reconstruct the object and refuses to serialize it. If there is no backing field, the property is silently ignored.

Source

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

                                                context.loadDeferred(e.getValue()));
                                    }
                                }

                                @Override
                                public void prepare(MethodContext context) {
                                    for (Map.Entry<DeferredParameter, DeferredParameter> e : def.entrySet()) {
                                        e.getKey().prepare(context);
                                        e.getValue().prepare(context);
                                    }
                                }
                            });
                        }
                    } else if (!relaxedValidation && !i.getName().equals("class") && !relaxedOk
                            && nonDefaultConstructorHolder == null) {
                        //check if there is actually a field with the name
                        try {
                            i.getReadMethod().getDeclaringClass().getDeclaredField(i.getName());
                            throw new RuntimeException("Cannot serialise field '" + i.getName() + "' on object '" + param
                                    + "' as the property is read only");
                        } catch (NoSuchFieldException e) {
                            //if there is no underlying field then we ignore the property
                        }

                    }

                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            } else if (i.getReadMethod() != null && (i.getWriteMethod() != null || ctorParamIndex != null)) {
                //normal javabean property
                try {
                    handledProperties.add(i.getName());
                    Object propertyValue = i.read(param);
                    if (propertyValue == null && ctorParamIndex == null) {
                        //we just assume properties are null by default
                        //TODO: is this a valid assumption? Should we check this by creating an instance?

View on GitHub (pinned to e1c734241f)

Solutions

  1. Add a setter matching the getter (setX with compatible type)
  2. Make it a writeable property or remove the getter
  3. Ensure there is no backing field so the property is ignored (e.g. rename the field) — not generally recommended
  4. Register a custom non-default constructor handler for the class instead of property-based serialization

Example fix

// before
public String getName() { return name; } // read-only
// after
public String getName() { return name; }
public void setName(String name) { this.name = name; }
Defensive patterns

Strategy: validation

Validate before calling

for (Method g : clazz.getMethods()) {
    if (g.getParameterCount() == 0 && g.getName().startsWith("get")) {
        String prop = Introspector.decapitalize(g.getName().substring(3));
        boolean hasSetter = Arrays.stream(clazz.getMethods()).anyMatch(s ->
            s.getName().equals("set" + g.getName().substring(3)) && s.getParameterCount() == 1);
        if (!hasSetter && hasDeclaredFieldOfType(clazz, prop)) {
            throw new IllegalStateException("Read-only property with backing field: " + prop);
        }
    }
}

Try / catch

try {
    recorder.record(value);
} catch (RuntimeException e) {
    if (String.valueOf(e.getMessage()).contains("as the property is read only")) {
        throw new IllegalStateException("Add a setter for the read-only property on " + value.getClass(), e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Passing an object to a @Record method whose class exposes a getter (e.g. isX()/getX()) with no corresponding setX(), but which has a final or private field named X; e.g. classes with derived getters over read-only fields.

Common situations: Immutable or builder-style value classes added to config records; classes where a getter exists but the setter was removed or renamed during refactoring.

Related errors


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