ory/kratos · error
identity schema rejected: self-referential $ref cycle
Error message
identity schema rejected: self-referential $ref cycle: %s
What it means
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.
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.
Example fix
// before
"definitions": {
"a": { "$ref": "#/definitions/b" },
"b": { "$ref": "#/definitions/a" } // cycle a -> b -> a
}
// after
"definitions": {
"a": { "type": "object", "properties": { "b": { "$ref": "#/definitions/b" } } },
"b": { "type": "object", "properties": { "name": { "type": "string" } } } // b no longer refs back to a Defensive patterns
Strategy: validation
Validate before calling
func hasRefCycle(defs map[string]string) bool {
for start := range defs {
seen := map[string]bool{}
cur, ok := start, true
for ok {
if seen[cur] { return true }
seen[cur] = true
cur, ok = defs[cur]
}
}
return false
} Try / catch
if strings.Contains(err.Error(), "self-referential $ref cycle") {
// parse the printed cycle chain and restructure definitions
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- invalid $ref URL
- $ref scheme is not permitted in identity schemas
- identity schema rejected: invalid regex in pattern
- identity schema rejected: invalid regex in…
AI-assisted analysis of ory/kratos@b86338da04 (2026-09-07).
Data as JSON: /api/errors/e0b1f51295bc66b6.
Report an issue: GitHub.
Appendix: source
Thrown at schema/prevalidate.go:136
// contains a cycle. Walking from each `$ref` location, follow the target
// path. If the target is itself a `$ref` location, continue. If the chain
// revisits a location, the resulting `*Schema` graph has a cycle that
// crashes Validate via stack overflow.
//
// The chain ends as soon as it reaches a node that is not itself a `$ref`
// — that node has its own validation logic (`properties`, `type`, etc.)
// which consumes input on each cycle iteration, so the recursion is
// bounded. Only pure `$ref` chains form unbounded loops.
func (p *preValidator) detectRefCycles() error {
for start := range p.refs {
visited := map[string]struct{}{}
cur := start
var chain []string
for {
if _, ok := visited[cur]; ok {
idx := slices.Index(chain, cur)
cycle := append(chain[idx:], cur)
return fmt.Errorf("identity schema rejected: self-referential $ref cycle: %s",
formatRefCycle(cycle))
}
visited[cur] = struct{}{}
chain = append(chain, cur)
next, ok := p.refs[cur]
if !ok {
break
}
cur = next
}
}
return nil
}
// escapeJSONPointer encodes a property name as a JSON-pointer reference
// token (RFC 6901): `~` → `~0`, `/` → `~1`. The order matters — `~` must be
// escaped first so a literal `/` does not collide with the escape produced
// for `~`.View on GitHub (pinned to b86338da04)