Skip to content

Commit ceaff33

Browse files
committed
resolve cross-schema $ref with json pointer fragments
bundleStandalone failed when a schema referenced a sibling via "<id>#/<json-pointer>" syntax. The closure resolver, codegenSafe, and all three codegen paths (genCode / genCodeE / genCodeC) now walk the pointer into the host schema.
1 parent b1ac634 commit ceaff33

2 files changed

Lines changed: 86 additions & 3 deletions

File tree

lib/js-compiler.js

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,50 @@ function collectDefs(schema) {
496496
return defs
497497
}
498498

499+
// Walk a JSON-pointer fragment ("/foo/bar/0") into a schema object.
500+
// Returns the target node, or null if any segment is missing.
501+
function walkJsonPointer(root, fragment) {
502+
if (!fragment || fragment === '/' || fragment === '#') return root
503+
const path = fragment.startsWith('#') ? fragment.slice(1) : fragment
504+
if (!path.startsWith('/')) return null
505+
const parts = path.split('/').slice(1).map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'))
506+
let target = root
507+
for (const p of parts) {
508+
if (target == null || typeof target !== 'object') return null
509+
target = target[p]
510+
}
511+
return target == null ? null : target
512+
}
513+
514+
// Resolve a cross-schema $ref of the form "<id>#/<json-pointer>" (or just "<id>").
515+
// Returns { schema, fullId } where fullId is the resolved $id of the host schema.
516+
function resolveCrossSchemaRef(ref, schemaMap) {
517+
if (!schemaMap) return null
518+
const hashIdx = ref.indexOf('#')
519+
const baseId = hashIdx >= 0 ? ref.slice(0, hashIdx) : ref
520+
const fragment = hashIdx >= 0 ? ref.slice(hashIdx) : ''
521+
if (!baseId) return null
522+
523+
let baseSchema = null
524+
let fullId = null
525+
if (schemaMap.has(baseId)) {
526+
baseSchema = schemaMap.get(baseId)
527+
fullId = baseId
528+
} else if (!ref.includes('://')) {
529+
for (const [id] of schemaMap) {
530+
if (id.endsWith('/' + baseId)) {
531+
baseSchema = schemaMap.get(id)
532+
fullId = id
533+
break
534+
}
535+
}
536+
}
537+
if (!baseSchema) return null
538+
const target = fragment ? walkJsonPointer(baseSchema, fragment) : baseSchema
539+
if (target == null) return null
540+
return { schema: target, fullId }
541+
}
542+
499543
function resolveRef(ref, defs, schemaMap) {
500544
// Self-reference: "#" — treat as permissive to avoid infinite recursion
501545
if (ref === '#') return () => true
@@ -520,7 +564,15 @@ function resolveRef(ref, defs, schemaMap) {
520564
const fn = compileToJS(resolved, null, schemaMap)
521565
return fn || (() => true)
522566
}
523-
// 3. Cross-schema ref (relative URI resolution)
567+
// 3. Cross-schema ref with JSON pointer fragment ("<id>#/<path>")
568+
if (schemaMap && ref.includes('#')) {
569+
const r = resolveCrossSchemaRef(ref, schemaMap)
570+
if (r) {
571+
const fn = compileToJS(r.schema, null, schemaMap)
572+
return fn || (() => true)
573+
}
574+
}
575+
// 4. Cross-schema ref (relative URI resolution, no fragment)
524576
if (schemaMap && !ref.includes('://') && !ref.startsWith('#')) {
525577
for (const [id] of schemaMap) {
526578
if (id.endsWith('/' + ref)) {
@@ -652,6 +704,11 @@ function codegenSafe(schema, schemaMap) {
652704
if (id.endsWith('/' + schema.$ref)) { isResolvable = true; resolvedTarget = schemaMap.get(id); break }
653705
}
654706
}
707+
// Cross-schema ref with JSON pointer fragment: "<id>#/<path>"
708+
if (!isLocal && !isResolvable && schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
709+
const r = resolveCrossSchemaRef(schema.$ref, schemaMap)
710+
if (r) { isResolvable = true; resolvedTarget = r.schema }
711+
}
655712
// Anchor-style ref: #name (not #/path, not bare #) — resolvable at compile time via anchors map
656713
const isAnchorRef = !isLocal && !isResolvable && schema.$ref.length > 1 && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')
657714
if (!isLocal && !isResolvable && !isAnchorRef) return false
@@ -1093,13 +1150,17 @@ function genCode(schema, v, lines, ctx, knownType) {
10931150
}
10941151
}
10951152
} else if (schema.$ref !== '#' && ctx.schemaMap) {
1096-
// 2. Cross-schema ref (exact match or relative URI)
1153+
// 2. Cross-schema ref (exact match, relative URI, or JSON pointer fragment)
10971154
let resolved = ctx.schemaMap.get(schema.$ref)
10981155
if (!resolved && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
10991156
for (const [id, s] of ctx.schemaMap) {
11001157
if (id.endsWith('/' + schema.$ref)) { resolved = s; break }
11011158
}
11021159
}
1160+
if (!resolved && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
1161+
const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
1162+
if (r) resolved = r.schema
1163+
}
11031164
if (resolved) {
11041165
if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
11051166
else {
@@ -2516,6 +2577,17 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
25162577
ctx.refStack.delete(schema.$ref)
25172578
return
25182579
}
2580+
// Cross-schema ref with JSON pointer fragment ("<id>#/<path>")
2581+
if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
2582+
const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
2583+
if (r) {
2584+
if (ctx.refStack.has(schema.$ref)) return
2585+
ctx.refStack.add(schema.$ref)
2586+
genCodeE(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
2587+
ctx.refStack.delete(schema.$ref)
2588+
return
2589+
}
2590+
}
25192591
}
25202592

25212593
// $dynamicRef — resolve via anchors map
@@ -3022,6 +3094,17 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
30223094
ctx.refStack.delete(schema.$ref)
30233095
return
30243096
}
3097+
// Cross-schema ref with JSON pointer fragment ("<id>#/<path>")
3098+
if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
3099+
const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
3100+
if (r) {
3101+
if (ctx.refStack.has(schema.$ref)) return
3102+
ctx.refStack.add(schema.$ref)
3103+
genCodeC(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
3104+
ctx.refStack.delete(schema.$ref)
3105+
return
3106+
}
3107+
}
30253108
}
30263109

30273110
// $dynamicRef — resolve via anchors map

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ata-validator",
3-
"version": "0.12.4",
3+
"version": "0.12.5",
44
"description": "Ultra-fast JSON Schema validator. 5x faster validation, 159,000x faster compilation. Works without native addon. Cross-schema $ref, Draft 2020-12 + Draft 7, V8-optimized JS codegen, simdjson, RE2, multi-core. Standard Schema V1 compatible.",
55
"main": "index.js",
66
"module": "index.mjs",

0 commit comments

Comments
 (0)