fix: keep $ref sibling keywords during getNode traversal (draft 2019-09+)#112
Open
samlown wants to merge 1 commit into
Open
fix: keep $ref sibling keywords during getNode traversal (draft 2019-09+)#112samlown wants to merge 1 commit into
samlown wants to merge 1 commit into
Conversation
In draft 2019-09 and later, $ref is an applicator that combines with its
sibling keywords (`{$ref, oneOf}` means "match the target AND the oneOf").
Validation already honours this per-draft, but the navigation/reduction
path does not: getNodeChild resolves a child $ref to the bare target via
resolveRef(), discarding siblings (compileNext only carries over the
annotation keywords in settings.PROPERTIES_TO_MERGE).
As a result getNode() on a location like
{ "$ref": "#/$defs/key", "oneOf": [ { "const": "a" }, ... ] }
returns just the resolved target, dropping the oneOf — so consumers that
rely on getNode to enumerate allowed values (e.g. editor autocomplete)
see nothing. A plain oneOf (no $ref) already returns the expected
one-of-error carrying the full schema; this makes $ref + oneOf behave
identically.
Merge the child's sibling keywords onto the resolved target (dropping the
now-resolved $ref), gated on whether the draft registers a $ref reducer.
draft 2019-09/2020-12 register one and merge; draft-07 and earlier do not
and keep the historical "siblings ignored" behaviour.
Verified against the full multi-draft spec suite (8515 passing, unchanged)
plus 4 new $ref-sibling tests in getNode.test.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In draft 2019-09 and later,
$refis an applicator that combines with its sibling keywords —{ "$ref": …, "oneOf": [...] }means "match the target and theoneOf".validate()honours this correctly (each sibling runs as an independent validator), but thegetNodetraversal path does not.getNodeChildresolves a child$refto the bare target viaresolveRef(), discarding siblings:(
resolveRef→compileNextonly carries oversettings.PROPERTIES_TO_MERGE— title/description/etc. — never applicator keywords.)So traversal silently drops the sibling constraint:
A plain
oneOf(no$ref) already returns the expectedone-of-errorwhoseerror.data.schemacarries the full schema, so consumers that usegetNodeto enumerate allowed values (autocomplete, data generation) work in that case but not when a$refis involved.This appears to be a recent regression: the
resolveRef()call was introduced infix: missing resolveRef in getNodeChild, which resolved child refs (good) but to the bare target (dropping siblings). The official JSON Schema Test Suite only exercisesvalidate(), so no spec test covers this traversal behaviour.Fix
In
getNodeChild, merge the child's sibling keywords onto the resolved target (dropping the now-resolved$ref), gated on whether the draft registers a$refreducer:$refreducer → siblings are merged (applicator semantics).$refreducer → unchanged, historical "siblings ignored" behaviour preserved.$ref+oneOfnow behaves identically to a plainoneOfat every location:Tests
$ref sibling keywordsgroup tosrc/getNode.test.tscovering object-property, array-item, matching-data reduction, and the draft-07 "ignored" case.Notes
dist/is not included (regenerate on publish).resolveRef/compileNextinstead ofgetNodeChild— but thegetNodeChildgate keepsresolveRefreturning target-only forvalidate(), which is what the independent-validator model expects. Open to your preference.