Skip to content
Open
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 @@ -2,7 +2,9 @@
* support `components.pathItems` so `$ref`s into it resolve, unblocking OpenAPI 3.1 documents that use reusable path items
* add `SpecValidator` with `strict_specification_version` config (`:silent` / `:warn` / `:raise`) to detect version mismatches between declared OpenAPI version and actual field usage
* `ExclusiveMinimum` / `ExclusiveMaximum`: detect 3.0 Boolean vs 3.1 numeric form mismatch
* `TypeNullIn30`: detect `type: "null"` usage in 3.0 documents (3.1 primitive)
* 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

## 2.3.1 (2025-11-14)
* add optional date coercion with behavior matching existing datetime coercion
Expand Down
10 changes: 10 additions & 0 deletions lib/openapi_parser/schema_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
require_relative 'schema_validator/all_of_validator'
require_relative 'schema_validator/one_of_validator'
require_relative 'schema_validator/nil_validator'
require_relative 'schema_validator/null_type_validator'
require_relative 'schema_validator/unspecified_type_validator'

class OpenAPIParser::SchemaValidator
Expand Down Expand Up @@ -116,11 +117,20 @@ def validator(value, schema)
object_validator
when 'array'
array_validator
when 'null'
# 3.1: only nil values are valid here. nil is handled earlier in
# this method, so a non-nil value reaching this branch is a type
# mismatch that should fail validation.
null_type_validator
else
unspecified_type_validator
end
end

def null_type_validator
@null_type_validator ||= OpenAPIParser::SchemaValidator::NullTypeValidator.new(self, @coerce_value)
end

def string_validator
@string_validator ||= OpenAPIParser::SchemaValidator::StringValidator.new(self, @allow_empty_date_and_datetime, @coerce_value, @datetime_coerce_class, @date_coerce_class)
end
Expand Down
2 changes: 2 additions & 0 deletions lib/openapi_parser/schema_validator/nil_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ class NilValidator < Base
# @param [OpenAPIParser::Schemas::Schema] schema
def coerce_and_validate(value, schema, **_keyword_args)
return [value, nil] if schema.nullable
# 3.1: `type: "null"` makes nil the only valid value for the schema.
return [value, nil] if schema.type == 'null'

[nil, OpenAPIParser::NotNullError.new(schema.object_reference)]
end
Expand Down
10 changes: 10 additions & 0 deletions lib/openapi_parser/schema_validator/null_type_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class OpenAPIParser::SchemaValidator
# Validates schemas declared as `type: "null"` (3.1) against non-nil
# values. The nil case is short-circuited by NilValidator before this
# validator is even picked up.
class NullTypeValidator < Base
def coerce_and_validate(value, schema, **_keyword_args)
OpenAPIParser::ValidateError.build_error_result(value, schema)
end
end
end
2 changes: 2 additions & 0 deletions lib/openapi_parser/spec_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require_relative 'spec_validator/rules/exclusive_maximum'
require_relative 'spec_validator/rules/nullable_deprecation'
require_relative 'spec_validator/rules/example_singular_deprecation'
require_relative 'spec_validator/rules/type_null_in_30'

module OpenAPIParser
class SpecViolationError < OpenAPIError
Expand Down Expand Up @@ -55,6 +56,7 @@ def rules
Rules::ExclusiveMaximum,
Rules::NullableDeprecation,
Rules::ExampleSingularDeprecation,
Rules::TypeNullIn30,
]
end
end
Expand Down
26 changes: 26 additions & 0 deletions lib/openapi_parser/spec_validator/rules/type_null_in_30.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module OpenAPIParser
class SpecValidator
module Rules
# `type: "null"` was added by 3.1; 3.0 only allowed the original six
# primitive type names. The parse layer accepts the literal anyway,
# so we report the mismatch here. Array-form types like
# `["string", "null"]` are covered by a separate rule.
class TypeNullIn30 < Rule
def check(root)
return [] unless version == :v3_0

violations = []
each_schema(root) do |schema|
next unless schema.type == 'null'

violations << violation(
path: schema.object_reference,
message: '`type: "null"` is a 3.1 addition; 3.0 documents have no such primitive',
)
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 @@ -52,6 +52,10 @@ module OpenAPIParser
class ExampleSingularDeprecation < Rule
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
end

class TypeNullIn30 < Rule
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
end
end
end

Expand Down
24 changes: 24 additions & 0 deletions spec/data/openapi_3_1/type_null_30.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: 3.0.3
info:
title: Telemetry API
version: '1.0'
paths:
/readings:
get:
summary: List sensor readings
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Reading'
components:
schemas:
Reading:
type: object
properties:
# 3.1 primitive `type: "null"`. 3.0 only knows the original six
# primitive names, so this is a spec violation on a 3.0 document.
calibration:
type: "null"
24 changes: 24 additions & 0 deletions spec/data/openapi_3_1/type_null_31.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: 3.1.0
info:
title: Telemetry API
version: '1.0'
paths:
/readings:
get:
summary: List sensor readings
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Reading'
components:
schemas:
Reading:
type: object
properties:
# 3.1 introduced `type: "null"` as a primitive, so this is legitimate
# on a 3.1 document and no violation is expected.
calibration:
type: "null"
15 changes: 15 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 @@ -101,4 +101,19 @@ def expect_clean(file)
expect_clean('example_keyword_30.yaml')
end
end

describe 'type: "null" (3.1 primitive absent from 3.0)' do
it 'warns on the version-mismatched document under :warn' do
expect_mismatch_warns('type_null_30.yaml', [:type_null_in30])
end

it 'raises SpecViolationError on the version-mismatched document under :raise' do
expect_mismatch_raises('type_null_30.yaml', [:type_null_in30])
end

it 'stays clean on the correctly-versioned document' do
expect_clean('type_null_31.yaml')
end
end

end
82 changes: 82 additions & 0 deletions spec/openapi_parser/spec_validator/rules/type_null_in_30_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require_relative '../../../spec_helper'

RSpec.describe 'OpenAPIParser::SpecValidator::Rules::TypeNullIn30' do
def schema_with_type_null(openapi_version_string)
raw = {
'openapi' => openapi_version_string,
'info' => { 'title' => 'test', 'version' => '1.0' },
'paths' => {},
'components' => { 'schemas' => { 'Sample' => { 'type' => 'null' } } },
}
OpenAPIParser.parse(raw, strict_reference_validation: false)
end

def run_rule_for(root)
OpenAPIParser::SpecValidator::Rules::TypeNullIn30.new(root.openapi_version).check(root)
end

context 'with a 3.1 document using type: "null"' do
it 'reports no violation' do
root = schema_with_type_null('3.1.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.0 document using type: "null"' do
it 'reports one violation pointing at the offending schema' do
root = schema_with_type_null('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 :type_null_in30
end
end

context 'with a 3.0 document using a normal type' do
it 'reports no violation' do
raw = {
'openapi' => '3.0.0',
'info' => { 'title' => 'test', 'version' => '1.0' },
'paths' => {},
'components' => { 'schemas' => { 'Sample' => { 'type' => 'string' } } },
}
root = OpenAPIParser.parse(raw, strict_reference_validation: false)
expect(run_rule_for(root)).to eq []
end
end

context 'with an :unknown version document using type: "null"' do
it 'reports no violation (rule skipped)' do
root = schema_with_type_null('4.0.0')
expect(run_rule_for(root)).to eq []
end
end
end

RSpec.describe 'runtime: type: "null" semantic' do
let(:options) { ::OpenAPIParser::SchemaValidator::Options.new }
let(:schema) do
raw = {
'openapi' => '3.1.0',
'info' => { 'title' => 'test', 'version' => '1.0' },
'paths' => {},
'components' => { 'schemas' => { 'Nullable' => { 'type' => 'null' } } },
}
OpenAPIParser.parse(raw, strict_reference_validation: false).components.schemas['Nullable']
end

context 'when value is nil' do
it 'passes validation without nullable' do
result = OpenAPIParser::SchemaValidator.validate(nil, schema, options)
expect(result).to eq nil
end
end

context 'when value is not nil' do
it 'raises a type-mismatch error' do
expect do
OpenAPIParser::SchemaValidator.validate('not nil', schema, options)
end.to raise_error(OpenAPIParser::ValidateError)
end
end
end
Loading