Skip to content

feat: parse and preserve $dynamicRef (JSON Schema 2020-12)#501

Open
aqeelat wants to merge 1 commit into
mattpolzin:mainfrom
aqeelat:feature/359/dynamic-ref
Open

feat: parse and preserve $dynamicRef (JSON Schema 2020-12)#501
aqeelat wants to merge 1 commit into
mattpolzin:mainfrom
aqeelat:feature/359/dynamic-ref

Conversation

@aqeelat

@aqeelat aqeelat commented Jun 15, 2026

Copy link
Copy Markdown

Part of #359.

Note: This is PR 1 of 2. It does parse / preserve / serialize only. Dynamic-scope resolution during dereferencing is a follow-up tracked in #359 (see the task list there). I'm happy to land this on its own — it unblocks downstream tooling (e.g. apple/swift-openapi-generator#547) to observe the keyword.

Summary

Adds parsing and round-trip serialization for the JSON Schema 2020-12 $dynamicRef / $dynamicAnchor keywords (spec §7.7), which OpenAPI 3.1.x adopts as its schema dialect.

$dynamicAnchor parsing landed in #360, but $dynamicRef was dropped: schemas whose only attribute was $dynamicRef decoded as empty fragments with a "Found nothing but unsupported attributes" warning. This PR makes the keyword parse and round-trip.

Changes

  • New JSONDynamicReference type wrapping JSONReference<JSONSchema> with $dynamicRef encode/decode.
  • New .anchor(String) case on JSONReference.InternalReference so plain fragment references like #category parse and round-trip.
  • New .dynamicReference case on JSONSchema.Schema and DereferencedJSONSchema, threaded through every exhaustive switch and the schema transformations (factory, encode/decode, optional/required/nullable, with(...), fragment combining, external dereferencing).
  • Decoder recognizes $dynamicRef before the unsupported-attributes fallthrough.
  • Local dereferencing preserves .dynamicReference unchanged — no resolution yet (that's the follow-up).

Behavior

  • $dynamicRef-only schema → decodes as .dynamicReference, no warning (previously an empty .fragment + "unsupported attributes").
  • $dynamicRef survives encode/decode round-trips, including inside real document trees (recursive patterns, generics).
  • dereferenced(in:) preserves .dynamicReference as-is.

Scope decision

This PR is intentionally scoped to parse/preserve/serialize, matching what you sketched on feature/359/dynamic-ref and the "parse the references/anchors but not support them more deeply" approach. The dynamic-scope resolution (inlining non-recursive targets, breaking cycles on recursive ones) is a separate concern that I'll propose in a follow-up — it touches dereferencing semantics you flagged as subtle, so it seems best to review independently.

Testing

  • 10 new tests in JSONSchemaDynamicReferenceTests: decode (anchor/component), the no-"unsupported-attributes" regression, encode round-trip, a full document round-trip, accessors/transformations, $ref plain-fragment round-trip, and a passthrough-survival dereferencing test.
  • Full suite passes with no regressions: 2157 tests, 0 failures (2147 prior + 10 new).
  • Verified the fix against the validator-backed fixture recursive-category-tree.yaml$dynamicRef now survives decode (previously stripped).

Breaking change

This adds new cases to public enums (JSONSchema.Schema, DereferencedJSONSchema, JSONReference.InternalReference), so it's source-breaking for exhaustive switches. The v7 migration guide is added documenting it. Happy to retarget to release/7_0 if you'd prefer breaking changes land there rather than main.


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

aqeelat added a commit to aqeelat/OpenAPIKit that referenced this pull request Jun 15, 2026
…nd-trip

Address review feedback on mattpolzin#501:
- Add test verifying the dynamic scope travels across a $ref boundary
  (anchor in Outer's $defs resolved by a $dynamicRef inside a referenced
  Inner component) -- the key mechanism added by the .reference refactor.
- Add test verifying a plain-fragment $ref ("#foo") round-trips verbatim
  instead of being rewritten with a slash.
- Document the $ref plain-fragment round-trip behavior change in the v7
  migration guide.
Add parsing and round-trip serialization for the JSON Schema 2020-12
$dynamicRef / $dynamicAnchor keywords (spec §7.7), adopted by OpenAPI 3.1.x
as its schema dialect.

$dynamicAnchor parsing landed in mattpolzin#360, but $dynamicRef was previously
dropped: schemas whose only attribute was $dynamicRef decoded as empty
fragments with a 'Found nothing but unsupported attributes' warning. With
this change the keyword is parsed and preserved, unblocking downstream
tooling (e.g. apple/swift-openapi-generator#547) to observe it.

Scope (parse/preserve only):
- New JSONDynamicReference type wrapping JSONReference<JSONSchema> with
  $dynamicRef encode/decode.
- New .anchor(String) case on JSONReference.InternalReference so plain
  fragment references like '#category' parse and round-trip.
- New .dynamicReference case on JSONSchema.Schema and
  DereferencedJSONSchema, threaded through every exhaustive switch and the
  schema transformations (factory, encode/decode, optional/required/
  nullable, with(...), fragment combining, external dereferencing).
- Decoder recognizes $dynamicRef before the unsupported-attributes
  fallthrough.
- Local dereferencing preserves .dynamicReference unchanged (dynamic-scope
  resolution is a follow-up, tracked in mattpolzin#359).

This is a source-breaking change (new public enum cases). The v7 migration
guide is added documenting it.

Part of mattpolzin#359.
@aqeelat aqeelat force-pushed the feature/359/dynamic-ref branch 2 times, most recently from ce0b317 to 73c99fb Compare June 15, 2026 19:35
@aqeelat aqeelat changed the title feat: support $dynamicRef / $dynamicAnchor (JSON Schema 2020-12) feat: parse and preserve $dynamicRef (JSON Schema 2020-12) Jun 15, 2026
@aqeelat aqeelat changed the base branch from release/7_0 to main June 15, 2026 19:35
@aqeelat aqeelat marked this pull request as ready for review June 15, 2026 19:40
@aqeelat

aqeelat commented Jun 15, 2026

Copy link
Copy Markdown
Author

Two quick design questions before a deeper review — both could change the shape of this PR, so figured I'd surface them up front.

1. Target branch. This adds new cases to public enums (JSONSchema.Schema, DereferencedJSONSchema, JSONReference.InternalReference), so it's source-breaking. I pointed it at main, but CONTRIBUTING.md sends breaking changes for the next major to release/7_0. Happy to retarget — just let me know which you'd prefer.

2. AST shape. I went with a dedicated .dynamicReference enum case (mirroring the feature/359/dynamic-ref sketch). The alternative is a dynamicRef: String? field on CoreContext, which is non-breaking and could land on the 6.x line instead. The enum case is cleaner to pattern-match; the field avoids the major bump. Any preference?

The rest (parse / decode / encode, the decoder no longer warning on $dynamicRef-only schemas, passthrough dereferencing) should be uncontroversial. I deliberately left dynamic-scope resolution out of this PR — that's the subtle part, and I'll send it as a follow-up once this lands.

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.

1 participant