Skip to content

Commit e99e4a7

Browse files
author
Keegan Caruso
committed
JSONSchemaService: Prevent duplicate anchor errors and ensure correct anchor resolution in schemas
1 parent d6e8322 commit e99e4a7

File tree

2 files changed

+406
-3
lines changed

2 files changed

+406
-3
lines changed

src/services/jsonSchemaService.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -780,12 +780,17 @@ export class JSONSchemaService implements IJSONSchemaService {
780780

781781
const collectAnchors = (root: JSONSchema): Map<string, JSONSchema> => {
782782
const result = new Map<string, JSONSchema>();
783+
const seen = new Set<JSONSchema>();
784+
// Use the schema's own $schema to determine draft, so that an external
785+
// schema referenced from a doesn't inherit the parent's anchor rules.
786+
const draft = root.$schema ? getSchemaDraftFromId(root.$schema) : schemaDraft;
783787
// Traversal that stops at sub-schemas with their own $id
784788
// because those create a new URI scope for anchors
785789
const traverseForAnchors = (node: JSONSchema, isRoot: boolean): void => {
786-
if (!node || typeof node !== 'object') {
790+
if (!node || typeof node !== 'object' || seen.has(node)) {
787791
return;
788792
}
793+
seen.add(node);
789794
// If this node has its own $id, and it's not the root, it's a new URI scope
790795
const id = getSchemaId(node);
791796
if (!isRoot && isString(id) && id.charAt(0) !== '#') {
@@ -795,8 +800,8 @@ export class JSONSchemaService implements IJSONSchemaService {
795800
// Collect anchor from this node
796801
// In draft-04/06/07, anchors are defined via $id/#fragment (e.g., "$id": "#myanchor")
797802
// In 2019-09+, $id fragments are no longer anchors; $anchor is used instead
798-
const fragmentAnchor = (schemaDraft === undefined || schemaDraft < SchemaDraft.v2019_09) && isString(id) && id.charAt(0) === '#' ? id.substring(1) : undefined;
799-
const dollarAnchor = (schemaDraft === undefined || schemaDraft >= SchemaDraft.v2019_09) ? node.$anchor : undefined;
803+
const fragmentAnchor = (draft === undefined || draft < SchemaDraft.v2019_09) && isString(id) && id.charAt(0) === '#' ? id.substring(1) : undefined;
804+
const dollarAnchor = (draft === undefined || draft >= SchemaDraft.v2019_09) ? node.$anchor : undefined;
800805
const anchor = fragmentAnchor ?? dollarAnchor;
801806
if (anchor) {
802807
if (result.has(anchor)) {
@@ -863,6 +868,11 @@ export class JSONSchemaService implements IJSONSchemaService {
863868
// Register embedded schemas before resolving refs
864869
registerEmbeddedSchemas(schema, handle.uri);
865870

871+
// Collect anchors eagerly before $ref resolution mutates the schema.
872+
// resolveRefs merges referenced nodes (including $anchor) into $ref targets,
873+
// so a lazy collectAnchors call could see duplicates from merged copies.
874+
handle.anchors = collectAnchors(schema);
875+
866876
// Resolve meta-schema to extract vocabularies if present
867877
const resolveMetaschemaVocabularies = (): PromiseLike<void> => {
868878
if (!schema.$schema || typeof schema.$schema !== 'string') {

0 commit comments

Comments
 (0)