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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## [Unreleased]

### Features

- Add OpenAPI 3.2.0 support. Fixes #228.
- Version acceptance: `3.2.x` documents are accepted and routed through the 3.1 codepath (same JSON Schema dialect).
- Document schema: all new 3.2.0 structural fields are recognized by `openapi.valid?` — `$self` (OpenAPI Object), `name` (Server), `query` and `additionalOperations` (Path Item), `mediaTypes` (Components), `dataValue` and `serializedValue` (Example), `querystring` (Parameter `in` value).
- Schema validation: works identically to 3.1 (same dialect, same codepath).

## [2.5.0] - 2025-12-08

### Bug Fixes
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# JSONSchemer

JSON Schema validator. Supports drafts 4, 6, 7, 2019-09, 2020-12, OpenAPI 3.0, and OpenAPI 3.1.
JSON Schema validator. Supports drafts 4, 6, 7, 2019-09, 2020-12, OpenAPI 3.0, OpenAPI 3.1, and OpenAPI 3.2.

## Installation

Expand Down Expand Up @@ -447,6 +447,8 @@ In the example above, custom error messsages are looked up using the following k

## OpenAPI

Supports OpenAPI 3.0, 3.1, and 3.2. OpenAPI 3.2 uses the same JSON Schema dialect as 3.1, so schema validation uses the same codepath. All 3.2.0 structural additions (`$self`, Server `name`, `query` method, `additionalOperations`, `mediaTypes`, Example `dataValue`/`serializedValue`, Parameter `querystring`) are recognized by document validation.

```ruby
document = JSONSchemer.openapi({
'openapi' => '3.1.0',
Expand Down
2 changes: 1 addition & 1 deletion lib/json_schemer/openapi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def initialize(document, **options)

version = document['openapi']
case version
when /\A3\.1\.\d+\z/
when /\A3\.[12]\.\d+\z/
@document_schema = JSONSchemer.openapi31_document
meta_schema = document.fetch('jsonSchemaDialect') { OpenAPI31::BASE_URI.to_s }
when /\A3\.0\.\d+\z/
Comment on lines 7 to 12
Expand Down
45 changes: 43 additions & 2 deletions lib/json_schemer/openapi31/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,11 @@ def self.dialect_schema(dialect)
'properties' => {
'openapi' => {
'type' => 'string',
'pattern' => '^3\.1\.\d+(-.+)?$'
'pattern' => '^3\.[12]\.\d+(-.+)?$'
},
'$self' => {
'type' => 'string',
'format' => 'uri-reference'
},
'info' => {
'$ref' => '#/$defs/info'
Expand Down Expand Up @@ -272,6 +276,9 @@ def self.dialect_schema(dialect)
'url' => {
'type' => 'string'
},
'name' => {
'type' => 'string'
},
'description' => {
'type' => 'string'
},
Expand Down Expand Up @@ -375,10 +382,16 @@ def self.dialect_schema(dialect)
'additionalProperties' => {
'$ref' => '#/$defs/path-item-or-reference'
}
},
'mediaTypes' => {
'type' => 'object',
'additionalProperties' => {
'$ref' => '#/$defs/media-type-or-reference'
}
}
},
'patternProperties' => {
'^(schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems)$' => {
'^(schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems|mediaTypes)$' => {
'$comment' => 'Enumerating all of the property names in the regex above is necessary for unevaluatedProperties to work as expected',
'propertyNames' => {
'pattern' => '^[a-zA-Z0-9._-]+$'
Expand Down Expand Up @@ -444,6 +457,15 @@ def self.dialect_schema(dialect)
},
'trace' => {
'$ref' => '#/$defs/operation'
},
'query' => {
'$ref' => '#/$defs/operation'
},
'additionalOperations' => {
'type' => 'object',
'additionalProperties' => {
'$ref' => '#/$defs/operation'
}
}
},
'$ref' => '#/$defs/specification-extensions',
Expand Down Expand Up @@ -551,6 +573,7 @@ def self.dialect_schema(dialect)
'in' => {
'enum' => [
'query',
'querystring',
'header',
'path',
'cookie'
Expand Down Expand Up @@ -858,6 +881,20 @@ def self.dialect_schema(dialect)
],
'unevaluatedProperties' => false
},
'media-type-or-reference' => {
'if' => {
'type' => 'object',
'required' => [
'$ref'
]
},
'then' => {
'$ref' => '#/$defs/reference'
},
'else' => {
'$ref' => '#/$defs/media-type'
}
},
'encoding' => {
'$comment' => 'https://spec.openapis.org/oas/v3.1.0#encoding-object',
'type' => 'object',
Expand Down Expand Up @@ -1023,6 +1060,10 @@ def self.dialect_schema(dialect)
'externalValue' => {
'type' => 'string',
'format' => 'uri'
},
'dataValue' => true,
'serializedValue' => {
'type' => 'string'
}
},
'not' => {
Expand Down
158 changes: 158 additions & 0 deletions test/open_api_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,164 @@ class OpenAPITest < Minitest::Test
'hungry' => 'kinda'
}

def test_openapi_3_2_accepted
openapi = JSONSchemer.openapi({
'openapi' => '3.2.0',
'info' => { 'title' => 'Test', 'version' => '1.0' },
'paths' => {},
'components' => {
'schemas' => {
'Widget' => {
'type' => 'object',
'properties' => {
'name' => { 'type' => 'string' }
}
}
}
}
})
schema = openapi.schema('Widget')
assert(schema.valid?({ 'name' => 'hello' }))
refute(schema.valid?({ 'name' => 42 }))
assert(openapi.valid?, 'OpenAPI 3.2.0 document should pass meta schema validation')
end

def test_openapi_3_2_1_accepted
openapi = JSONSchemer.openapi({
'openapi' => '3.2.1',
'info' => { 'title' => 'Test', 'version' => '1.0' },
'paths' => {}
})
assert(openapi.valid?, 'OpenAPI 3.2.1 document should pass validation')
end

def test_openapi_3_3_rejected
assert_raises(JSONSchemer::UnsupportedOpenAPIVersion) do
JSONSchemer.openapi({
'openapi' => '3.3.0',
'info' => { 'title' => 'Test', 'version' => '1.0' },
'paths' => {}
})
end
end

def test_openapi_3_2_document_with_self_field
openapi = JSONSchemer.openapi({
'openapi' => '3.2.0',
'$self' => 'https://example.com/api/openapi.json',
'info' => { 'title' => 'Test', 'version' => '1.0' },
'paths' => {}
})
assert(openapi.valid?, 'OpenAPI 3.2 document with $self should pass validation')
end

def test_openapi_3_2_server_with_name
openapi = JSONSchemer.openapi({
'openapi' => '3.2.0',
'info' => { 'title' => 'Test', 'version' => '1.0' },
'servers' => [
{ 'url' => 'https://api.example.com', 'name' => 'production' }
],
'paths' => {}
})
assert(openapi.valid?, 'Server with name field should pass validation')
end

def test_openapi_3_2_path_item_with_query_and_additional_operations
openapi = JSONSchemer.openapi({
'openapi' => '3.2.0',
'info' => { 'title' => 'Test', 'version' => '1.0' },
'paths' => {
'/search' => {
'query' => {
'operationId' => 'searchQuery',
'responses' => { '200' => { 'description' => 'OK' } }
},
'additionalOperations' => {
'COPY' => {
'operationId' => 'copySearch',
'responses' => { '200' => { 'description' => 'Copied' } }
}
}
}
}
})
assert(openapi.valid?, 'Path item with query and additionalOperations should pass validation')
end

def test_openapi_3_2_components_with_media_types
openapi = JSONSchemer.openapi({
'openapi' => '3.2.0',
'info' => { 'title' => 'Test', 'version' => '1.0' },
'components' => {
'mediaTypes' => {
'jsonBody' => {
'schema' => { 'type' => 'object' }
}
}
}
})
assert(openapi.valid?, 'Components with mediaTypes should pass validation')
end

def test_openapi_3_2_example_with_data_value_and_serialized_value
openapi = JSONSchemer.openapi({
'openapi' => '3.2.0',
'info' => { 'title' => 'Test', 'version' => '1.0' },
'paths' => {
'/items' => {
'get' => {
'operationId' => 'listItems',
'responses' => {
'200' => {
'description' => 'OK',
'content' => {
'application/json' => {
'examples' => {
'sample' => {
'summary' => 'A sample',
'dataValue' => { 'id' => 1 },
'serializedValue' => '{"id":1}'
}
}
}
}
}
}
}
}
}
})
assert(openapi.valid?, 'Example with dataValue and serializedValue should pass validation')
end

def test_openapi_3_2_parameter_with_querystring
openapi = JSONSchemer.openapi({
'openapi' => '3.2.0',
'info' => { 'title' => 'Test', 'version' => '1.0' },
'paths' => {
'/search' => {
'get' => {
'operationId' => 'search',
'parameters' => [
{
'name' => 'qs',
'in' => 'querystring',
'content' => {
'application/x-www-form-urlencoded' => {
'schema' => { 'type' => 'string' }
}
}
}
],
'responses' => { '200' => { 'description' => 'OK' } }
}
}
}
})
assert(openapi.valid?, 'Parameter with in=querystring should pass validation')
end

def test_discriminator_specification_example
openapi = {
'openapi' => '3.1.0',
Expand Down