Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions lib/openapi_parser/schema_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion lib/openapi_parser/schemas/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ class Schema < Base
:type,
:nullable,
:example,
:deprecated
:deprecated,
:const

# @!attribute [r] read_only
# @return [Boolean, nil]
Expand Down
2 changes: 2 additions & 0 deletions lib/openapi_parser/spec_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,6 +64,7 @@ def rules
Rules::ExampleSingularDeprecation,
Rules::TypeNullIn30,
Rules::WebhooksIn30,
Rules::ConstIn30,
]
end
end
Expand Down
25 changes: 25 additions & 0 deletions lib/openapi_parser/spec_validator/rules/const_in_30.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions sig/openapi_parser/spec_validator.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 26 additions & 0 deletions spec/data/openapi_3_1/const_30.yaml
Original file line number Diff line number Diff line change
@@ -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'
26 changes: 26 additions & 0 deletions spec/data/openapi_3_1/const_31.yaml
Original file line number Diff line number Diff line change
@@ -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'
27 changes: 27 additions & 0 deletions spec/openapi_parser/schema_validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1020,4 +1020,31 @@ 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' 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' do
expect do
OpenAPIParser::SchemaValidator.validate('different', schema, options)
end.to raise_error(OpenAPIParser::ValidateError)
end
end
end
end
13 changes: 13 additions & 0 deletions spec/openapi_parser/spec_validator/integration_3_1_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
64 changes: 64 additions & 0 deletions spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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' 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' 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' 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' 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)' do
root = doc_with_const('4.0.0')
expect(run_rule_for(root)).to eq []
end
end
end