From 1d3849acda13f62cbbdab9b34aeedfc7ec916955 Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki Date: Wed, 20 May 2026 01:47:32 +0900 Subject: [PATCH 1/4] Add pending specs for const parse + ConstIn30 rule + runtime (3.1 strategy PR12) --- spec/openapi_parser/schema_validator_spec.rb | 21 +++++++++ .../spec_validator/rules/const_in_30_spec.rb | 46 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb diff --git a/spec/openapi_parser/schema_validator_spec.rb b/spec/openapi_parser/schema_validator_spec.rb index dce37d6d..d648d97f 100644 --- a/spec/openapi_parser/schema_validator_spec.rb +++ b/spec/openapi_parser/schema_validator_spec.rb @@ -1020,4 +1020,25 @@ class ValidatableTest it { expect { subject }.to raise_error(StandardError).with_message('implement') } end end + + describe 'const semantic (3.1)' do + let(:options) { ::OpenAPIParser::SchemaValidator::Options.new } + let(:schema) do + raw = { + 'openapi' => '3.1.0', + 'info' => { 'title' => 'test', 'version' => '1.0' }, + 'paths' => {}, + 'components' => { 'schemas' => { 'Fixed' => { 'type' => 'string', 'const' => 'fixed' } } }, + } + OpenAPIParser.parse(raw, strict_reference_validation: false).components.schemas['Fixed'] + end + + context 'when value equals const' do + it 'passes validation' + end + + context 'when value does not equal const' do + it 'raises a validation error' + end + end end diff --git a/spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb new file mode 100644 index 00000000..8b528779 --- /dev/null +++ b/spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb @@ -0,0 +1,46 @@ +require_relative '../../../spec_helper' + +RSpec.describe 'OpenAPIParser::SpecValidator::Rules::ConstIn30' 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_const(openapi_version_string) + raw = base_doc(openapi_version_string, { 'type' => 'string', 'const' => 'fixed' }) + OpenAPIParser.parse(raw, strict_reference_validation: false) + end + + def doc_without_const(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::ConstIn30.new(root.openapi_version).check(root) + end + + context 'with a 3.1 document using const' do + it 'reports no violation' + end + + context 'with a 3.1 document without const' do + it 'reports no violation' + end + + context 'with a 3.0 document using const' do + it 'reports one violation pointing at the offending schema' + end + + context 'with a 3.0 document without const' do + it 'reports no violation' + end + + context 'with an :unknown version document' do + it 'reports no violation (rule skipped)' + end +end From b4f03bcddbcc3038a28dd727e8bd07610d02c3be Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki Date: Wed, 20 May 2026 01:49:01 +0900 Subject: [PATCH 2/4] const parse + ConstIn30 rule + runtime exact-equality (3.1 strategy PR12) - Schema gains a `const` accessor via openapi_attr_values - Rules::ConstIn30 flags the keyword on 3.0 documents - validate_schema short-circuits to a ValidateError when const is present and value != schema.const; uses raw_schema.key? so an intentional const: null is honored --- lib/openapi_parser/schema_validator.rb | 7 +++++ lib/openapi_parser/schemas/schema.rb | 3 +- lib/openapi_parser/spec_validator.rb | 2 ++ .../spec_validator/rules/const_in_30.rb | 25 +++++++++++++++++ sig/openapi_parser/spec_validator.rbs | 4 +++ spec/openapi_parser/schema_validator_spec.rb | 10 +++++-- .../spec_validator/rules/const_in_30_spec.rb | 28 +++++++++++++++---- 7 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 lib/openapi_parser/spec_validator/rules/const_in_30.rb diff --git a/lib/openapi_parser/schema_validator.rb b/lib/openapi_parser/schema_validator.rb index 5ac92db3..7221c444 100644 --- a/lib/openapi_parser/schema_validator.rb +++ b/lib/openapi_parser/schema_validator.rb @@ -75,6 +75,13 @@ def validate_data def validate_schema(value, schema, **keyword_args) return [value, nil] unless schema + # 3.1: `const` pins the value to exactly that constant. Checked before + # type dispatch so it applies uniformly across primitives. Detection + # uses raw_schema so an intentional `const: null` is honored. + if schema.respond_to?(:raw_schema) && schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('const') + return [nil, OpenAPIParser::ValidateError.new(value, "const #{schema.const.inspect}", schema.object_reference)] if value != schema.const + end + if (v = validator(value, schema)) if keyword_args.empty? return v.coerce_and_validate(value, schema) diff --git a/lib/openapi_parser/schemas/schema.rb b/lib/openapi_parser/schemas/schema.rb index 23d7c888..17e57da7 100644 --- a/lib/openapi_parser/schemas/schema.rb +++ b/lib/openapi_parser/schemas/schema.rb @@ -71,7 +71,8 @@ class Schema < Base :type, :nullable, :example, - :deprecated + :deprecated, + :const # @!attribute [r] read_only # @return [Boolean, nil] diff --git a/lib/openapi_parser/spec_validator.rb b/lib/openapi_parser/spec_validator.rb index 6c5c4fb6..8344d92b 100644 --- a/lib/openapi_parser/spec_validator.rb +++ b/lib/openapi_parser/spec_validator.rb @@ -8,6 +8,7 @@ require_relative 'spec_validator/rules/example_singular_deprecation' require_relative 'spec_validator/rules/type_null_in_30' require_relative 'spec_validator/rules/webhooks_in_30' +require_relative 'spec_validator/rules/const_in_30' module OpenAPIParser class SpecViolationError < OpenAPIError @@ -63,6 +64,7 @@ def rules Rules::ExampleSingularDeprecation, Rules::TypeNullIn30, Rules::WebhooksIn30, + Rules::ConstIn30, ] end end diff --git a/lib/openapi_parser/spec_validator/rules/const_in_30.rb b/lib/openapi_parser/spec_validator/rules/const_in_30.rb new file mode 100644 index 00000000..5d68883b --- /dev/null +++ b/lib/openapi_parser/spec_validator/rules/const_in_30.rb @@ -0,0 +1,25 @@ +module OpenAPIParser + class SpecValidator + module Rules + # `const` is a JSON Schema 2020-12 keyword adopted by OpenAPI 3.1. + # 3.0 does not recognize it. Detection inspects raw_schema so a + # literal `const: null` (deliberate) still flags. + class ConstIn30 < 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?('const') + + violations << violation( + path: schema.object_reference, + message: '`const` 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 3eec7d64..9ada0257 100644 --- a/sig/openapi_parser/spec_validator.rbs +++ b/sig/openapi_parser/spec_validator.rbs @@ -68,6 +68,10 @@ module OpenAPIParser class WebhooksIn30 < Rule def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] end + + class ConstIn30 < Rule + def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] + end end end diff --git a/spec/openapi_parser/schema_validator_spec.rb b/spec/openapi_parser/schema_validator_spec.rb index d648d97f..e725d2df 100644 --- a/spec/openapi_parser/schema_validator_spec.rb +++ b/spec/openapi_parser/schema_validator_spec.rb @@ -1034,11 +1034,17 @@ class ValidatableTest end context 'when value equals const' do - it 'passes validation' + it 'passes validation' do + expect(OpenAPIParser::SchemaValidator.validate('fixed', schema, options)).to eq 'fixed' + end end context 'when value does not equal const' do - it 'raises a validation error' + it 'raises a validation error' do + expect do + OpenAPIParser::SchemaValidator.validate('different', schema, options) + end.to raise_error(OpenAPIParser::ValidateError) + end end end end diff --git a/spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb index 8b528779..fb0ad906 100644 --- a/spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb @@ -25,22 +25,40 @@ def run_rule_for(root) end context 'with a 3.1 document using const' do - it 'reports no violation' + it 'reports no violation' do + root = doc_with_const('3.1.0') + expect(run_rule_for(root)).to eq [] + end end context 'with a 3.1 document without const' do - it 'reports no violation' + it 'reports no violation' do + root = doc_without_const('3.1.0') + expect(run_rule_for(root)).to eq [] + end end context 'with a 3.0 document using const' do - it 'reports one violation pointing at the offending schema' + it 'reports one violation pointing at the offending schema' do + root = doc_with_const('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 :const_in30 + end end context 'with a 3.0 document without const' do - it 'reports no violation' + it 'reports no violation' do + root = doc_without_const('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_const('4.0.0') + expect(run_rule_for(root)).to eq [] + end end end From 16cee0151b2c2e2048b3322696f0f2ae336df126 Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki Date: Sat, 30 May 2026 16:46:23 +0900 Subject: [PATCH 3/4] Integration test: const new in 3.1 `const` on a 3.0 document warns and raises (JSON Schema 2020-12 keyword with no 3.0 equivalent); the same keyword on a 3.1 document stays clean. --- spec/data/openapi_3_1/const_30.yaml | 26 +++++++++++++++++++ spec/data/openapi_3_1/const_31.yaml | 26 +++++++++++++++++++ .../spec_validator/integration_3_1_spec.rb | 13 ++++++++++ 3 files changed, 65 insertions(+) create mode 100644 spec/data/openapi_3_1/const_30.yaml create mode 100644 spec/data/openapi_3_1/const_31.yaml diff --git a/spec/data/openapi_3_1/const_30.yaml b/spec/data/openapi_3_1/const_30.yaml new file mode 100644 index 00000000..f0a78c62 --- /dev/null +++ b/spec/data/openapi_3_1/const_30.yaml @@ -0,0 +1,26 @@ +openapi: 3.0.3 +info: + title: Webhook Envelope API + version: '1.0' +paths: + /events: + post: + summary: Receive an event + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope' + responses: + '202': + description: Accepted +components: + schemas: + Envelope: + type: object + properties: + # `const` is a JSON Schema 2020-12 keyword adopted by 3.1; 3.0 has no + # equivalent, so its use on a 3.0 document is a spec violation. + version: + type: string + const: '2020-12' diff --git a/spec/data/openapi_3_1/const_31.yaml b/spec/data/openapi_3_1/const_31.yaml new file mode 100644 index 00000000..4a8d1e87 --- /dev/null +++ b/spec/data/openapi_3_1/const_31.yaml @@ -0,0 +1,26 @@ +openapi: 3.1.0 +info: + title: Webhook Envelope API + version: '1.0' +paths: + /events: + post: + summary: Receive an event + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope' + responses: + '202': + description: Accepted +components: + schemas: + Envelope: + type: object + properties: + # `const` is legitimate under 3.1 (JSON Schema 2020-12), so no + # violation is expected here. + version: + type: string + const: '2020-12' 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 001520fa..562a1659 100644 --- a/spec/openapi_parser/spec_validator/integration_3_1_spec.rb +++ b/spec/openapi_parser/spec_validator/integration_3_1_spec.rb @@ -157,4 +157,17 @@ def expect_clean(file) expect_clean('webhooks_31.yaml') end end + describe 'const (JSON Schema 2020-12 keyword new in 3.1)' do + it 'warns on the version-mismatched document under :warn' do + expect_mismatch_warns('const_30.yaml', [:const_in30]) + end + + it 'raises SpecViolationError on the version-mismatched document under :raise' do + expect_mismatch_raises('const_30.yaml', [:const_in30]) + end + + it 'stays clean on the correctly-versioned document' do + expect_clean('const_31.yaml') + end + end end From 21d55f0f7dc327faaeeabcba013ec3f629ea01b5 Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki Date: Sat, 4 Jul 2026 12:12:39 +0900 Subject: [PATCH 4/4] Add CHANGELOG entries for const support --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 780886ea..498ebbd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,11 @@ * `ExclusiveMinimum` / `ExclusiveMaximum`: detect 3.0 Boolean vs 3.1 numeric form mismatch * `TypeNullIn30`: detect `type: "null"` usage in 3.0 documents (3.1 primitive) * `WebhooksIn30`: detect root-level `webhooks` usage in 3.0 documents (3.1 addition) + * `ConstIn30`: detect `const` 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 +* support `const` (OpenAPI 3.1) with exact-equality value validation ## 2.3.1 (2025-11-14) * add optional date coercion with behavior matching existing datetime coercion