{"record":{"id":"4524cc338e01cc4e","repo":"quarkusio/quarkus","slug":"cyclic-dependency-when-marshaling-an-object","errorCode":null,"errorMessage":"Cyclic dependency when marshaling an object","messagePattern":"Cyclic dependency when marshaling an object","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java","lineNumber":23,"sourceCode":"import jakarta.json.bind.serializer.JsonbSerializer;\nimport jakarta.json.bind.serializer.SerializationContext;\nimport jakarta.json.stream.JsonGenerator;\n\nimport org.eclipse.yasson.internal.ProcessingContext;\nimport org.eclipse.yasson.internal.model.ClassModel;\nimport org.eclipse.yasson.internal.model.PropertyModel;\n\n// Using the raw type here as eclipse yasson doesn't like custom serializers for\n// generic root types, see https://github.com/eclipse-ee4j/yasson/issues/639\npublic class HalEntityWrapperJsonbSerializer implements JsonbSerializer<HalEntityWrapper> {\n\n    @Override\n    public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, SerializationContext context) {\n        ProcessingContext processingContext = (ProcessingContext) context;\n        Object entity = wrapper.getEntity();\n\n        if (!processingContext.addProcessedObject(entity)) {\n            throw new RuntimeException(\"Cyclic dependency when marshaling an object\");\n        }\n\n        try {\n            generator.writeStartObject();\n            ClassModel classModel = processingContext.getMappingContext().getOrCreateClassModel(entity.getClass());\n\n            for (PropertyModel property : classModel.getSortedProperties()) {\n                if (property.isReadable()) {\n                    writeValue(property.getWriteName(), property.getValue(entity), generator, context);\n                }\n            }\n\n            writeLinks(wrapper.getLinks(), generator, context);\n            generator.writeEnd();\n        } finally {\n            processingContext.removeProcessedObject(entity);\n        }\n    }","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java#L5-L41","documentation":"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'.","triggerScenarios":"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.","commonSituations":"Bidirectional JPA relations serialized without @JsonbTransient on the back-reference; self-referencing tree nodes; lazy relations forming a cycle.","solutions":["Annotate the back-referencing field with @JsonbTransient to break the cycle","Restructure DTOs so the serialized view has no cycles","Use @JsonbNillable/filtered views or manual mapping to plain DTOs","Serialize only one side of the relationship"],"exampleFix":"// before\nclass Node {\n    public Node parent;\n    public List<Node> children;\n}\n// after\nclass Node {\n    @JsonbTransient\n    public Node parent;\n    public List<Node> children;\n}","handlingStrategy":"validation","validationCode":"Set<Object> seen = new IdentityHashMap<>();\nDeque<Object> stack = new ArrayDeque<>(List.of(root));\nwhile (!stack.isEmpty()) {\n    Object o = stack.pop();\n    if (!seen.add(o)) throw new IllegalStateException(\"Cycle detected at \" + o.getClass());\n    for (Field f : o.getClass().getDeclaredFields()) {\n        if (!f.getType().isPrimitive() && !f.getType().getName().startsWith(\"java.\")) {\n            f.setAccessible(true);\n            Object v = f.get(o);\n            if (v != null) stack.push(v);\n        }\n    }\n}","typeGuard":"boolean isAcyclic(Object root, Set<Object> seen) {\n    if (root == null || !seen.add(root)) return root != null ? false : true;\n    for (Field f : root.getClass().getDeclaredFields()) {\n        try {\n            if (!f.getType().isPrimitive() && !f.getType().getName().startsWith(\"java.\")) {\n                f.setAccessible(true);\n                Object v = f.get(root);\n                if (v != null && !isAcyclic(v, seen)) return false;\n            }\n        } catch (IllegalAccessException ignored) { }\n    }\n    return true;\n}","tryCatchPattern":"try {\n    jsonb.toJson(halWrapper);\n} catch (RuntimeException e) {\n    if (e.getMessage().contains(\"Cyclic dependency\")) {\n        throw new IllegalStateException(\"Entity graph has a cycle; add @JsonbTransient to the back-reference\", e);\n    }\n    throw e;\n}","preventionTips":["Annotate bidirectional JPA back-references with @JsonbTransient","Map entities to cycle-free DTOs for REST responses","Test serialization of the full entity graph in dev"],"tags":["jsonb","serialization","hal","circular-reference","quarkus"],"backgroundTag":"circular-reference-serialization","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}