quarkusio/quarkus · error · RuntimeException

Cyclic dependency when marshaling an object

Error message

Cyclic dependency when marshaling an object

What it means

HalEntityWrapperJsonbSerializer serializes a HAL entity with JSON-B's custom SerializationContext. To detect infinite recursion it calls ProcessingContext.addProcessedObject(entity); if the entity is already being processed (a reference cycle), it throws RuntimeException 'Cyclic dependency when marshaling an object'.

Source

Thrown at extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java:23

import jakarta.json.bind.serializer.JsonbSerializer;
import jakarta.json.bind.serializer.SerializationContext;
import jakarta.json.stream.JsonGenerator;

import org.eclipse.yasson.internal.ProcessingContext;
import org.eclipse.yasson.internal.model.ClassModel;
import org.eclipse.yasson.internal.model.PropertyModel;

// Using the raw type here as eclipse yasson doesn't like custom serializers for
// generic root types, see https://github.com/eclipse-ee4j/yasson/issues/639
public class HalEntityWrapperJsonbSerializer implements JsonbSerializer<HalEntityWrapper> {

    @Override
    public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, SerializationContext context) {
        ProcessingContext processingContext = (ProcessingContext) context;
        Object entity = wrapper.getEntity();

        if (!processingContext.addProcessedObject(entity)) {
            throw new RuntimeException("Cyclic dependency when marshaling an object");
        }

        try {
            generator.writeStartObject();
            ClassModel classModel = processingContext.getMappingContext().getOrCreateClassModel(entity.getClass());

            for (PropertyModel property : classModel.getSortedProperties()) {
                if (property.isReadable()) {
                    writeValue(property.getWriteName(), property.getValue(entity), generator, context);
                }
            }

            writeLinks(wrapper.getLinks(), generator, context);
            generator.writeEnd();
        } finally {
            processingContext.removeProcessedObject(entity);
        }
    }

View on GitHub (pinned to e1c734241f)

Solutions

  1. Annotate the back-referencing field with @JsonbTransient to break the cycle
  2. Restructure DTOs so the serialized view has no cycles
  3. Use @JsonbNillable/filtered views or manual mapping to plain DTOs
  4. Serialize only one side of the relationship

Example fix

// before
class Node {
    public Node parent;
    public List<Node> children;
}
// after
class Node {
    @JsonbTransient
    public Node parent;
    public List<Node> children;
}
Defensive patterns

Strategy: validation

Validate before calling

Set<Object> seen = new IdentityHashMap<>();
Deque<Object> stack = new ArrayDeque<>(List.of(root));
while (!stack.isEmpty()) {
    Object o = stack.pop();
    if (!seen.add(o)) throw new IllegalStateException("Cycle detected at " + o.getClass());
    for (Field f : o.getClass().getDeclaredFields()) {
        if (!f.getType().isPrimitive() && !f.getType().getName().startsWith("java.")) {
            f.setAccessible(true);
            Object v = f.get(o);
            if (v != null) stack.push(v);
        }
    }
}

Type guard

boolean isAcyclic(Object root, Set<Object> seen) {
    if (root == null || !seen.add(root)) return root != null ? false : true;
    for (Field f : root.getClass().getDeclaredFields()) {
        try {
            if (!f.getType().isPrimitive() && !f.getType().getName().startsWith("java.")) {
                f.setAccessible(true);
                Object v = f.get(root);
                if (v != null && !isAcyclic(v, seen)) return false;
            }
        } catch (IllegalAccessException ignored) { }
    }
    return true;
}

Try / catch

try {
    jsonb.toJson(halWrapper);
} catch (RuntimeException e) {
    if (e.getMessage().contains("Cyclic dependency")) {
        throw new IllegalStateException("Entity graph has a cycle; add @JsonbTransient to the back-reference", e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Serializing an entity graph with JSON-B where an object references itself directly or indirectly (e.g. parent<->child relations both mapped) while wrapped in a HAL entity.

Common situations: Bidirectional JPA relations serialized without @JsonbTransient on the back-reference; self-referencing tree nodes; lazy relations forming a cycle.

Related errors


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