Skip to content

feat: resolve $dynamicRef against the dynamic scope#504

Open
aqeelat wants to merge 2 commits into
mattpolzin:release/7_0from
aqeelat:dynamic-scope-resolution
Open

feat: resolve $dynamicRef against the dynamic scope#504
aqeelat wants to merge 2 commits into
mattpolzin:release/7_0from
aqeelat:dynamic-scope-resolution

Conversation

@aqeelat

@aqeelat aqeelat commented Jul 11, 2026

Copy link
Copy Markdown

Part of #359.

Implements the "dynamic-scope-aware dereferencing" item from the #359 task list. locallyDereferenced() / JSONSchema.dereferenced(in:) now resolve $dynamicRef against the dynamic scope (the outermost in-scope $dynamicAnchor wins, per JSON Schema 2020-12 §7.7), instead of throwing on every $dynamicRef.

Behavior

  • Non-recursive $dynamicRef (e.g. the JSON Schema "generics" pattern — $dynamicRef "#T" → a leaf $defs entry): the target is inlined.
  • Recursive or unresolvable $dynamicRef: throws (ReferenceCycleError / GenericError), consistent with how recursive static $ref already fails and with the Dereferenced... invariant that a DereferencedJSONSchema contains no references.
  • The dynamic scope is reconstructed from the document's static structure: seeded from each schema's own $dynamicAnchor (and $defs-declared anchors, first-wins/outermost-wins) and threaded through subschemas and across $ref boundaries.

Notes

  • Raw-AST consumers (most generators, e.g. swift-openapi-generator) are unaffected — .dynamicReference on JSONSchema is unchanged; only locallyDereferenced() behavior changes (strictly an improvement: previously it threw on all $dynamicRef; now only recursive/unresolvable ones).
  • Edits the same migration-guide section as feat: externally dereference $dynamicRef #503 (external deref); trivial doc reconcile on merge.

Testing

  • New deref tests: generics-inline, recursive-throws, unresolvable-throws, non-anchor-throws, optional+described propagation, sibling dynamic-ref independence, scope-propagates-across-$ref, $ref$dynamicRef name propagation, optional $ref property.
  • Full suite: 2165 tests, 0 failures (2157 prior + 8 new; no regressions — the .reference refactor is behavior-preserving).
  • 100% region coverage on the new code (llvm-cov-verified: 0 uncovered regions in the scope-aware _dereferenced + the ArrayContext/ObjectContext threading).

This PR was drafted with assistance from AI tooling. The submitter has reviewed and validated the contents prior to submission.

…ferencing

locallyDereferenced() / dereferenced(in:) now resolve $dynamicRef against the
dynamic scope (outermost $dynamicAnchor wins, per JSON Schema 2020-12).
Non-recursive targets are inlined; recursive or unresolvable dynamic refs
throw, consistent with static $ref cycles and the Dereferenced... invariant
(a DereferencedJSONSchema must not contain references).

Implements the 'dynamic-scope-aware dereferencing' item from mattpolzin#359. Edits the
same migration-guide section as mattpolzin#503 (external deref); reconcile on merge.

Part of mattpolzin#359.
@aqeelat
aqeelat force-pushed the dynamic-scope-resolution branch from cbba88a to 26b74d9 Compare July 11, 2026 09:16

@mattpolzin mattpolzin left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks mostly ready to go. Thanks! A few nits with comments and migration guide stuff but just one request for refactoring.

Comment on lines +488 to +493
/// `dynamicScope` maps a `$dynamicAnchor` name to the **outermost** schema
/// resource bearing that anchor on the current resolution path (first-wins
/// insertion). A `$dynamicRef` is resolved against this scope per JSON
/// Schema 2020-12 dynamic-scope rules: the outermost matching anchor wins.
/// Non-recursive targets are inlined; recursive or unresolvable dynamic
/// references throw (a `DereferencedJSONSchema` must not contain references).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// `dynamicScope` maps a `$dynamicAnchor` name to the **outermost** schema
/// resource bearing that anchor on the current resolution path (first-wins
/// insertion). A `$dynamicRef` is resolved against this scope per JSON
/// Schema 2020-12 dynamic-scope rules: the outermost matching anchor wins.
/// Non-recursive targets are inlined; recursive or unresolvable dynamic
/// references throw (a `DereferencedJSONSchema` must not contain references).
/// `dynamicScope` maps a `$dynamicAnchor` name to the **outermost** schema
/// resource bearing that anchor on the current resolution path per JSON
/// Schema 2020-12 dynamic-scope rules.
/// Non-recursive targets are inlined; recursive or unresolvable dynamic
/// references throw.

This comment is technically accurate but a bit repetitive and or verbose for my taste.

@aqeelat aqeelat Jul 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I shortened it.

case .reference(let reference, let context):
var dereferenced = try reference
._dereferenced(in: components, following: references, dereferencedFromComponentNamed: nil)
// Thread the dynamic scope across the `$ref` boundary (outermost-wins).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Thread the dynamic scope across the `$ref` boundary (outermost-wins).

I prefer to add comments when they will tell the next reader more about what's going on. While this comment does describe the following lines, it doesn't (to me) say anything that the code itself doesn't also say.

On the other hand, if the code did something that had been misunderstood initially then a comment explaining why the code does what it does instead of what the code does would be generally appreciated.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the comment to only state the non-obvious things.

Comment on lines +558 to +559
// Recursive dynamic reference: cannot inline without retaining a
// reference, so fail -- consistent with static `$ref` cycles.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Recursive dynamic reference: cannot inline without retaining a
// reference, so fail -- consistent with static `$ref` cycles.

See other comments for my thoughts on code comments.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the comment to only state the non-obvious things.

Comment on lines +22 to +27
`locallyDereferenced()` and `JSONSchema.dereferenced(in:)` now resolve
`$dynamicRef` against the dynamic scope (the outermost in-scope `$dynamicAnchor`
wins, per JSON Schema 2020-12). Non-recursive targets are inlined. Recursive
or unresolvable `$dynamicRef`s **throw** — a `DereferencedJSONSchema` must not
contain references, so a dynamic ref that cannot be fully inlined fails the
same way a recursive static `$ref` does (`ReferenceCycleError`).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`locallyDereferenced()` and `JSONSchema.dereferenced(in:)` now resolve
`$dynamicRef` against the dynamic scope (the outermost in-scope `$dynamicAnchor`
wins, per JSON Schema 2020-12). Non-recursive targets are inlined. Recursive
or unresolvable `$dynamicRef`s **throw** — a `DereferencedJSONSchema` must not
contain references, so a dynamic ref that cannot be fully inlined fails the
same way a recursive static `$ref` does (`ReferenceCycleError`).
`locallyDereferenced()` and `JSONSchema.dereferenced(in:)` now resolve
`$dynamicRef` against the dynamic scope.

The migration guide should describe what needs to be changed or considered to adopt a new OpenAPIKit version. It should not describe pre-existing behavior or how the OpenAPI or JSON Schema specifications work.

@aqeelat aqeelat Jul 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified the migration-guide wording to keep it adoption-focused.

throw OpenAPI.Components.ReferenceCycleError(ref: reference.absoluteString)
}
var dereferenced = try components
.lookup(reference)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the way this skips over the code that dereferences a JSONReference but reproduces all of that code here feels off. I think it mostly makes the LocallyDereferenceable implementation in JSONReference irrelevant which may be ok but for the fact that it is still there and still public interface.

For now, I'd like to refactor things so that there's an additional helper next to JSONReference's LocallyDereferenceable helper allowing that code to be used in both ways instead of copied into this location. Does that make sense? I can describe this more completely if helpful.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I refactored JSONReference._dereferenced(in:following:) through an internal continuation helper so cycle detection and lookup stay in one place, and JSONSchema can thread dynamic scope across the $ref boundary without duplicating that logic.

Extract JSONReference._dereferenced(in:following:then:) so JSONSchema's
.dynamicReference path can thread dynamic scope through the shared cycle-
detection + lookup logic instead of duplicating it. Addresses the one
non-nit request from review of mattpolzin#504. Also applies comment/migration-guide
nits from the same review.
@aqeelat
aqeelat requested a review from mattpolzin July 19, 2026 01:30
@aqeelat

aqeelat commented Jul 19, 2026

Copy link
Copy Markdown
Author

@mattpolzin Thanks for the review. I addressed the refactor request by sharing the JSONReference cycle-check/lookup logic through an internal helper, and trimmed the comments and migration-guide wording as suggested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants