{"record":{"id":"e0b1f51295bc66b6","repo":"ory/kratos","slug":"identity-schema-rejected-self-referential-ref-cy","errorCode":null,"errorMessage":"identity schema rejected: self-referential $ref cycle: %s","messagePattern":"identity schema rejected: self-referential \\$ref cycle: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"schema/prevalidate.go","lineNumber":136,"sourceCode":"// contains a cycle. Walking from each `$ref` location, follow the target\n// path. If the target is itself a `$ref` location, continue. If the chain\n// revisits a location, the resulting `*Schema` graph has a cycle that\n// crashes Validate via stack overflow.\n//\n// The chain ends as soon as it reaches a node that is not itself a `$ref`\n// — that node has its own validation logic (`properties`, `type`, etc.)\n// which consumes input on each cycle iteration, so the recursion is\n// bounded. Only pure `$ref` chains form unbounded loops.\nfunc (p *preValidator) detectRefCycles() error {\n\tfor start := range p.refs {\n\t\tvisited := map[string]struct{}{}\n\t\tcur := start\n\t\tvar chain []string\n\t\tfor {\n\t\t\tif _, ok := visited[cur]; ok {\n\t\t\t\tidx := slices.Index(chain, cur)\n\t\t\t\tcycle := append(chain[idx:], cur)\n\t\t\t\treturn fmt.Errorf(\"identity schema rejected: self-referential $ref cycle: %s\",\n\t\t\t\t\tformatRefCycle(cycle))\n\t\t\t}\n\t\t\tvisited[cur] = struct{}{}\n\t\t\tchain = append(chain, cur)\n\t\t\tnext, ok := p.refs[cur]\n\t\t\tif !ok {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcur = next\n\t\t}\n\t}\n\treturn nil\n}\n\n// escapeJSONPointer encodes a property name as a JSON-pointer reference\n// token (RFC 6901): `~` → `~0`, `/` → `~1`. The order matters — `~` must be\n// escaped first so a literal `/` does not collide with the escape produced\n// for `~`.","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/ory/kratos/blob/b86338da04a040247a07f46100a86dcfb3875909/schema/prevalidate.go#L118-L154","documentation":"preValidateSchema tracks $ref targets in a graph and detectRefCycles walks the chain of refs from each starting point. If it revisits a node already on the current chain, it reports a self-referential $ref cycle, naming the chain via formatRefCycle. Cycles are rejected because the schema compiler cannot resolve infinitely recursive references.","triggerScenarios":"An identity schema's $ref graph contains a cycle, e.g. \"#/definitions/a\" -> \"#/definitions/b\" -> \"#/definitions/a\", or a ref pointing back to its own definition. Raised by detectRefCycles during pre-validation, before upstream compilation.","commonSituations":"Splitting schemas across definitions where two definitions $ref each other (mutual recursion); refactor merges that accidentally pointed a definition back at itself; copying ref paths between schemas so a relative pointer resolves to the same document.","solutions":["Break the cycle shown in the error message (the ref chain is printed): restructure one of the mutually-referencing definitions by inlining its structure or extracting the shared part into a third definition.","Make the recursion truly non-cyclic by giving the recursive variant different content than what it references (actual recursion without identical pointer cycles may still be rejected here — flatten it).","Use \"$defs\" with a clear hierarchy so each ref points strictly 'downward' (a definition must not point back at an ancestor on its own chain).","Draw the ref graph (each $ref target as an edge) for complex schemas to spot cycles before submission."],"exampleFix":"// before\n\"definitions\": {\n  \"a\": { \"$ref\": \"#/definitions/b\" },\n  \"b\": { \"$ref\": \"#/definitions/a\" }   // cycle a -> b -> a\n}\n// after\n\"definitions\": {\n  \"a\": { \"type\": \"object\", \"properties\": { \"b\": { \"$ref\": \"#/definitions/b\" } } },\n  \"b\": { \"type\": \"object\", \"properties\": { \"name\": { \"type\": \"string\" } } }   // b no longer refs back to a","handlingStrategy":"validation","validationCode":"func hasRefCycle(defs map[string]string) bool {\n    for start := range defs {\n        seen := map[string]bool{}\n        cur, ok := start, true\n        for ok {\n            if seen[cur] { return true }\n            seen[cur] = true\n            cur, ok = defs[cur]\n        }\n    }\n    return false\n}","typeGuard":null,"tryCatchPattern":"if strings.Contains(err.Error(), \"self-referential $ref cycle\") {\n    // parse the printed cycle chain and restructure definitions\n}","preventionTips":["Keep $ref graphs acyclic: a definition must never ref an ancestor on its own chain","Draw or script-check the ref graph for large schemas","Extract shared structure into separate definitions instead of cross-referencing siblings"],"tags":["jsonschema","ref-resolution","cycle-detection","identity-schema"],"backgroundTag":"schema-validation-failed","analyzedSha":"b86338da04a040247a07f46100a86dcfb3875909","analyzedAt":"2026-09-07T15:58:15.934Z","contentChangedAt":"2026-09-07T15:58:15.934Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}