Circular refs#19
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes parsing of JSON Schemas containing circular $ref graphs (issue #18) by adding reference-cycle detection to avoid unbounded recursion / stack overflow.
Changes:
- Track currently-resolving
$refpointers to break circular reference recursion during parsing. - When a circular
$refis detected, short-circuit to a namedComplexValueinstead of recursing. - Add a regression test exercising a mutual circular reference between definitions.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/json-schema-parser.ts | Adds $ref cycle detection (resolvingRefs) and short-circuits circular $ref parsing to avoid stack overflows. |
| src/json-schema-parser.test.ts | Adds a regression test to ensure circular-reference schemas can be parsed without crashing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const service = (await parser(sourceContent, absoluteSourcePath)).service; | ||
|
|
||
| // ASSERT | ||
| expect(service).toBeTruthy(); |
There was a problem hiding this comment.
This test only asserts that service is truthy, so it won't catch cases where circular refs no longer overflow but still produce an invalid/partially-parsed service (e.g., missing type declarations or parser violations). It would be stronger to also validate the returned service (and/or assert no parser violations) and check that the expected types are present (e.g. A and B).
| expect(service).toBeTruthy(); | |
| const validation = validate(service); | |
| expect(validation.errors).toEqual([]); | |
| const typeNames = service.types.map((t) => t.name); | |
| expect(typeNames).toEqual(expect.arrayContaining(['A', 'B'])); |
| inheritedDescription: [], | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
In the circular-ref short-circuit path, the parser returns a ComplexValue based on parseTypeName(resolved) without ensuring the referenced schema will actually result in a declared Type/Enum/Union. For cycles made entirely of $ref aliases (e.g. A -> B -> A where both schemas are just $ref), this will break the stack overflow but can still produce a service that references a type name that never gets created, and no violation is reported. Consider detecting $ref-only cycles and emitting a parser error (return untyped()), or registering an in-progress placeholder in the relevant map before descending so circular references always point at an eventual declaration.
| const { range, sourceIndex } = decodeRange( | |
| encodeRange(0, schema.ref.loc), | |
| ); | |
| this.violations.push({ | |
| code: 'PARSER_ERROR', | |
| message: `Circular ref '${schema.ref.value}' does not resolve to a concrete type`, | |
| severity: 'error', | |
| range, | |
| sourcePath: this.sourcePaths[sourceIndex], | |
| }); |
resolves #18