From fe490de31d83175b659e3b990c9621ed2b20fc8a Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki Date: Wed, 20 May 2026 01:56:17 +0900 Subject: [PATCH 1/4] Add pending specs for $dynamicAnchor rule (3.1 strategy PR18) --- .../rules/dynamic_anchor_in_30_spec.rb | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 spec/openapi_parser/spec_validator/rules/dynamic_anchor_in_30_spec.rb diff --git a/spec/openapi_parser/spec_validator/rules/dynamic_anchor_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/dynamic_anchor_in_30_spec.rb new file mode 100644 index 0000000..8e6ec52 --- /dev/null +++ b/spec/openapi_parser/spec_validator/rules/dynamic_anchor_in_30_spec.rb @@ -0,0 +1,46 @@ +require_relative '../../../spec_helper' + +RSpec.describe 'OpenAPIParser::SpecValidator::Rules::DynamicAnchorIn30' do + def base_doc(openapi_version_string, sample_schema) + { + 'openapi' => openapi_version_string, + 'info' => { 'title' => 'test', 'version' => '1.0' }, + 'paths' => {}, + 'components' => { 'schemas' => { 'Sample' => sample_schema } }, + } + end + + def doc_with_dynamic_anchor(openapi_version_string) + raw = base_doc(openapi_version_string, { '$dynamicAnchor' => 'meta' }) + OpenAPIParser.parse(raw, strict_reference_validation: false) + end + + def doc_without_dynamic_anchor(openapi_version_string) + raw = base_doc(openapi_version_string, { 'type' => 'string' }) + OpenAPIParser.parse(raw, strict_reference_validation: false) + end + + def run_rule_for(root) + OpenAPIParser::SpecValidator::Rules::DynamicAnchorIn30.new(root.openapi_version).check(root) + end + + context 'with a 3.1 document using $dynamicAnchor' do + it 'reports no violation' + end + + context 'with a 3.1 document without $dynamicAnchor' do + it 'reports no violation' + end + + context 'with a 3.0 document using $dynamicAnchor' do + it 'reports one violation pointing at the offending schema' + end + + context 'with a 3.0 document without $dynamicAnchor' do + it 'reports no violation' + end + + context 'with an :unknown version document' do + it 'reports no violation (rule skipped)' + end +end From ca424b0795d6402d0e5841045124f3e326ae7194 Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki Date: Wed, 20 May 2026 01:57:09 +0900 Subject: [PATCH 2/4] $dynamicAnchor 3.0 spec_validator rule (3.1 strategy PR18) Closes the 3.1 divergence enumeration. Matching pattern to $dynamicRef: raw_schema key detection, 3.0 only emits, parse stays permissive. --- lib/openapi_parser/spec_validator.rb | 2 ++ .../rules/dynamic_anchor_in_30.rb | 25 +++++++++++++++++ sig/openapi_parser/spec_validator.rbs | 4 +++ .../rules/dynamic_anchor_in_30_spec.rb | 28 +++++++++++++++---- 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 lib/openapi_parser/spec_validator/rules/dynamic_anchor_in_30.rb diff --git a/lib/openapi_parser/spec_validator.rb b/lib/openapi_parser/spec_validator.rb index f239dec..dfee7ca 100644 --- a/lib/openapi_parser/spec_validator.rb +++ b/lib/openapi_parser/spec_validator.rb @@ -12,6 +12,7 @@ require_relative 'spec_validator/rules/webhooks_in_30' require_relative 'spec_validator/rules/const_in_30' require_relative 'spec_validator/rules/dynamic_ref_in_30' +require_relative 'spec_validator/rules/dynamic_anchor_in_30' module OpenAPIParser class SpecViolationError < OpenAPIError @@ -71,6 +72,7 @@ def rules Rules::WebhooksIn30, Rules::ConstIn30, Rules::DynamicRefIn30, + Rules::DynamicAnchorIn30, ] end end diff --git a/lib/openapi_parser/spec_validator/rules/dynamic_anchor_in_30.rb b/lib/openapi_parser/spec_validator/rules/dynamic_anchor_in_30.rb new file mode 100644 index 0000000..a42ce5f --- /dev/null +++ b/lib/openapi_parser/spec_validator/rules/dynamic_anchor_in_30.rb @@ -0,0 +1,25 @@ +module OpenAPIParser + class SpecValidator + module Rules + # `$dynamicAnchor` is the companion of `$dynamicRef` from JSON Schema + # 2020-12, declaring the dynamic resolution point. 3.0 has nothing + # equivalent; the rule flags it on 3.0 documents. + class DynamicAnchorIn30 < Rule + def check(root) + return [] unless version == :v3_0 + + violations = [] + each_schema(root) do |schema| + next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('$dynamicAnchor') + + violations << violation( + path: schema.object_reference, + message: '`$dynamicAnchor` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent', + ) + end + violations + end + end + end + end +end diff --git a/sig/openapi_parser/spec_validator.rbs b/sig/openapi_parser/spec_validator.rbs index c634973..fca7c80 100644 --- a/sig/openapi_parser/spec_validator.rbs +++ b/sig/openapi_parser/spec_validator.rbs @@ -84,6 +84,10 @@ module OpenAPIParser class DynamicRefIn30 < Rule def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] end + + class DynamicAnchorIn30 < Rule + def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] + end end end diff --git a/spec/openapi_parser/spec_validator/rules/dynamic_anchor_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/dynamic_anchor_in_30_spec.rb index 8e6ec52..10c6707 100644 --- a/spec/openapi_parser/spec_validator/rules/dynamic_anchor_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/dynamic_anchor_in_30_spec.rb @@ -25,22 +25,40 @@ def run_rule_for(root) end context 'with a 3.1 document using $dynamicAnchor' do - it 'reports no violation' + it 'reports no violation' do + root = doc_with_dynamic_anchor('3.1.0') + expect(run_rule_for(root)).to eq [] + end end context 'with a 3.1 document without $dynamicAnchor' do - it 'reports no violation' + it 'reports no violation' do + root = doc_without_dynamic_anchor('3.1.0') + expect(run_rule_for(root)).to eq [] + end end context 'with a 3.0 document using $dynamicAnchor' do - it 'reports one violation pointing at the offending schema' + it 'reports one violation pointing at the offending schema' do + root = doc_with_dynamic_anchor('3.0.0') + violations = run_rule_for(root) + expect(violations.size).to eq 1 + expect(violations.first.path).to eq '#/components/schemas/Sample' + expect(violations.first.rule_name).to eq :dynamic_anchor_in30 + end end context 'with a 3.0 document without $dynamicAnchor' do - it 'reports no violation' + it 'reports no violation' do + root = doc_without_dynamic_anchor('3.0.0') + expect(run_rule_for(root)).to eq [] + end end context 'with an :unknown version document' do - it 'reports no violation (rule skipped)' + it 'reports no violation (rule skipped)' do + root = doc_with_dynamic_anchor('4.0.0') + expect(run_rule_for(root)).to eq [] + end end end From 425123e215070564a9a13c2179656e3e5f0c9976 Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki Date: Sat, 30 May 2026 16:49:45 +0900 Subject: [PATCH 3/4] Integration test: $dynamicAnchor new in 3.1 $dynamicAnchor on a 3.0 document warns and raises (JSON Schema 2020-12 dynamic resolution point with no 3.0 equivalent); the same keyword on a 3.1 document stays clean. --- spec/data/openapi_3_1/dynamic_anchor_30.yaml | 25 +++++++++++++++++++ spec/data/openapi_3_1/dynamic_anchor_31.yaml | 25 +++++++++++++++++++ .../spec_validator/integration_3_1_spec.rb | 14 +++++++++++ 3 files changed, 64 insertions(+) create mode 100644 spec/data/openapi_3_1/dynamic_anchor_30.yaml create mode 100644 spec/data/openapi_3_1/dynamic_anchor_31.yaml diff --git a/spec/data/openapi_3_1/dynamic_anchor_30.yaml b/spec/data/openapi_3_1/dynamic_anchor_30.yaml new file mode 100644 index 0000000..477b517 --- /dev/null +++ b/spec/data/openapi_3_1/dynamic_anchor_30.yaml @@ -0,0 +1,25 @@ +openapi: 3.0.3 +info: + title: Tree API + version: '1.0' +paths: + /trees: + get: + summary: Fetch a tree + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Node' +components: + schemas: + Node: + type: object + # `$dynamicAnchor` is a JSON Schema 2020-12 dynamic resolution point + # adopted by 3.1; 3.0 has no equivalent, so this is a spec violation. + $dynamicAnchor: node + properties: + label: + type: string diff --git a/spec/data/openapi_3_1/dynamic_anchor_31.yaml b/spec/data/openapi_3_1/dynamic_anchor_31.yaml new file mode 100644 index 0000000..935d0e0 --- /dev/null +++ b/spec/data/openapi_3_1/dynamic_anchor_31.yaml @@ -0,0 +1,25 @@ +openapi: 3.1.0 +info: + title: Tree API + version: '1.0' +paths: + /trees: + get: + summary: Fetch a tree + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Node' +components: + schemas: + Node: + type: object + # `$dynamicAnchor` is legitimate under 3.1 (JSON Schema 2020-12), so no + # violation is expected here. + $dynamicAnchor: node + properties: + label: + type: string diff --git a/spec/openapi_parser/spec_validator/integration_3_1_spec.rb b/spec/openapi_parser/spec_validator/integration_3_1_spec.rb index 53fb8f4..183a787 100644 --- a/spec/openapi_parser/spec_validator/integration_3_1_spec.rb +++ b/spec/openapi_parser/spec_validator/integration_3_1_spec.rb @@ -212,4 +212,18 @@ def expect_clean(file) expect_clean('dynamic_ref_31.yaml') end end + + describe '$dynamicAnchor (JSON Schema 2020-12 referencing new in 3.1)' do + it 'warns on the version-mismatched document under :warn' do + expect_mismatch_warns('dynamic_anchor_30.yaml', [:dynamic_anchor_in30]) + end + + it 'raises SpecViolationError on the version-mismatched document under :raise' do + expect_mismatch_raises('dynamic_anchor_30.yaml', [:dynamic_anchor_in30]) + end + + it 'stays clean on the correctly-versioned document' do + expect_clean('dynamic_anchor_31.yaml') + end + end end From db10718688e930df81f9d3d63bda829fde2e7bbd Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki Date: Sat, 4 Jul 2026 12:19:11 +0900 Subject: [PATCH 4/4] Add CHANGELOG entry for DynamicAnchorIn30 rule --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e0171..38bb250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * `WebhooksIn30`: detect root-level `webhooks` usage in 3.0 documents (3.1 addition) * `ConstIn30`: detect `const` usage in 3.0 documents (3.1 addition) * `DynamicRefIn30`: detect `$dynamicRef` usage in 3.0 documents (3.1 addition) + * `DynamicAnchorIn30`: detect `$dynamicAnchor` usage in 3.0 documents (3.1 addition) * support 3.1-style numeric `exclusiveMinimum` / `exclusiveMaximum` in value validation (standalone bound, not a Boolean modifier on `minimum` / `maximum`) * support `type: "null"` (3.1 primitive) in value validation * support root-level `webhooks` (OpenAPI 3.1) in the parse layer