Skip to content

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

Merged
mattpolzin merged 2 commits into
mattpolzin:release/7_0from
aqeelat:feature/359/dynamic-ref
Jul 9, 2026
Merged

feat: parse and preserve $dynamicRef (JSON Schema 2020-12)#501
mattpolzin merged 2 commits into
mattpolzin:release/7_0from
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). This PR on its own 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 on the JSONSchema AST.

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, 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.
  • DereferencedJSONSchema retains no reference cases: per review feedback, local dereferencing throws when it encounters a .dynamicReference it cannot inline (dynamic-scope resolution is a follow-up), preserving the Dereferenced... invariant that no references remain. The raw JSONSchema AST still carries .dynamicReference for tools that read schemas without dereferencing.

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.
  • dereferenced(in:) / locallyDereferenced() throws on a $dynamicRef until the resolution follow-up lands (consistent with how unresolvable static $ref values fail).

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 throws-on-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

Source-breaking (new public enum cases), so targets release/7_0. The v7 migration guide is added documenting it.


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.
@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.

@mattpolzin

Copy link
Copy Markdown
Owner

Hey, thanks for the PR. Just a heads up that I am slow to review non-critical PRs lately for a few reasons but especially given I am on vacation without my computer for another week. Not ignoring you, though!

@aqeelat

aqeelat commented Jun 19, 2026

Copy link
Copy Markdown
Author

No worries. Enjoy your vacation. Should I ping you again in two weeks?

@aqeelat

aqeelat commented Jul 5, 2026

Copy link
Copy Markdown
Author

Hi @mattpolzin
Let me know if there is anything I can do to help with the review.

@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.

Thanks for reminding me to get this reviewed. It's a busy time of life but I do appreciate the contribution.

Can you point this PR at the release/7_0 branch since it contains breaking changes? The updates you are making to the migration guide here look nice.

I'll want to spend a few minutes poking at the code in the next few days here but for now I just have the one high level question I left inline about whether it is justified to retain dynamic references in Dereferenced... types vs. requiring them all to be able to be derefenced.

Comment thread Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift Outdated
Comment thread Sources/OpenAPIKit/JSONDynamicReference.swift Outdated
@aqeelat aqeelat force-pushed the feature/359/dynamic-ref branch from 73c99fb to 2fa36f2 Compare July 6, 2026 13:36
@aqeelat aqeelat changed the base branch from main to release/7_0 July 6, 2026 13:36
@aqeelat aqeelat marked this pull request as draft July 6, 2026 17:50
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 on the JSONSchema AST,
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, 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.
- DereferencedJSONSchema retains no reference cases: local dereferencing
  throws on .dynamicReference (dynamic-scope resolution is a follow-up,
  tracked in mattpolzin#359). The raw JSONSchema AST still carries .dynamicReference
  for tools that read schemas without dereferencing.

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 from 2fa36f2 to 506c075 Compare July 7, 2026 09:55
@aqeelat aqeelat marked this pull request as ready for review July 7, 2026 11:09
@aqeelat

aqeelat commented Jul 7, 2026

Copy link
Copy Markdown
Author

Thanks for the review, @mattpolzin. All three points addressed:

  1. Retargeted to release/7_0.
  2. DereferencedJSONSchema no longer retains .dynamicReference. Agreed that the value of the Dereferenced... family is having all references fully removed. Local dereferencing now throws on .dynamicReference, mirroring how unresolvable static $ref values already fail. The raw JSONSchema AST still carries the keyword for tools that read schemas without dereferencing.
  3. Removed the file header.

Also squashed into a single commit and cleaned up a few minor things (doc comments, isEmpty over count > 0, whitespace). The migration guide documents the new cases and the throwing behavior.

As a sanity check, I validated the AST against apple/swift-openapi-generator (the downstream consumer in #547) — built a prototype that resolves $dynamicRef to Swift generic types and recursive self-references. The parse/preserve layer this PR provides is sufficient; no additional OpenAPIKit changes are needed for the generator side.

@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.

Alright, I've given this a more thorough review now that I've had time to sit down with the code open in an editor and I just have a few small remaining requests before merging.

Thanks for your patience and effort!

Comment thread documentation/migration_guides/v7_migration_guide.md Outdated
Comment thread documentation/migration_guides/v7_migration_guide.md Outdated
Comment thread Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift
Addresses review feedback on mattpolzin#501:
- Migration guide: only JSONSchema.Schema gains .dynamicReference
  (DereferencedJSONSchema no longer has the case); drop the paragraph
  describing Swift switch exhaustiveness mechanics.
- DereferencedJSONSchema: add a TODO on the external-deref dynamicReference
  case noting external dereferencing is deferred (see mattpolzin#359).

Part of mattpolzin#359.
@aqeelat

aqeelat commented Jul 9, 2026

Copy link
Copy Markdown
Author

@mattpolzin

Thank you for taking the time.
I applied your changes.

@mattpolzin mattpolzin merged commit ed64a6d into mattpolzin:release/7_0 Jul 9, 2026
9 checks passed
@mattpolzin

Copy link
Copy Markdown
Owner

Thanks!

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