Skip to content
Draft
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 @@ -5,8 +5,10 @@
* `XmlAttributeDeprecation` / `XmlWrappedDeprecation`: detect XML Object `attribute: true` / `wrapped: true` in 3.2 documents (deprecated in 3.2 in favor of `nodeType`)
* `SecuritySchemeFieldsBefore32`: detect Security Scheme `deprecated` / `oauth2MetadataUrl` and the `deviceAuthorization` OAuth flow in pre-3.2 documents (3.2 additions)
* `MediaTypesBefore32`: detect `components.mediaTypes` usage in pre-3.2 documents (3.2 addition)
* `StreamingFieldsBefore32`: detect Media Type `itemSchema` / `itemEncoding` / `prefixEncoding` and nested Encoding Object `encoding` / `itemEncoding` / `prefixEncoding` usage in pre-3.2 documents (3.2 additions)
* add `allow_3_2_features` config to apply OpenAPI 3.2 runtime behavior to documents that declare an earlier version (default `false`: pre-3.2 documents behave as before)
* support `components.mediaTypes` (OpenAPI 3.2), resolving `$ref`s in request body and response `content` in 3.2 documents; an unresolved one accepts any body unless `strict_reference_validation` is set
* support `itemSchema` (OpenAPI 3.2) in the parse layer; it is not yet used to validate sequential media type bodies

## 2.5.0 (2026-10-21)
* add OpenAPI 3.2 `SpecValidator` rules
Expand Down
4 changes: 4 additions & 0 deletions lib/openapi_parser/schemas/media_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ class MediaType < Base
# @return [Schema, nil] OpenAPI3 Schema object
openapi_attr_object :schema, Schema, reference: true

# @!attribute [r] item_schema
# @return [Schema, nil] schema for each item of a sequential media type (OpenAPI 3.2+); not yet used for validation
openapi_attr_object :item_schema, Schema, reference: true, schema_key: :itemSchema

# validate params by schema definitions
# @param [Hash] params
# @param [OpenAPIParser::SchemaValidator::Options] options
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 @@ -26,6 +26,7 @@
require_relative 'spec_validator/rules/xml_wrapped_deprecation'
require_relative 'spec_validator/rules/security_scheme_fields_before_32'
require_relative 'spec_validator/rules/media_types_before_32'
require_relative 'spec_validator/rules/streaming_fields_before_32'

module OpenAPIParser
class SpecViolationError < OpenAPIError
Expand Down Expand Up @@ -99,6 +100,7 @@ def rules
Rules::XmlWrappedDeprecation,
Rules::SecuritySchemeFieldsBefore32,
Rules::MediaTypesBefore32,
Rules::StreamingFieldsBefore32,
]
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module OpenAPIParser
class SpecValidator
module Rules
# 3.2 adds `itemSchema`, `itemEncoding`, and `prefixEncoding` to the
# Media Type Object for sequential media types (SSE, JSON Lines,
# JSON Sequences), and lets an Encoding Object nest `encoding`,
# `itemEncoding`, and `prefixEncoding`. The parse layer accepts
# `itemSchema` permissively; this rule reports the version mismatch.
# Media Types under a Parameter or Header `content` are not modeled,
# so fields there are not reached.
class StreamingFieldsBefore32 < Rule
NEW_FIELDS = %w[itemSchema itemEncoding prefixEncoding].freeze
NEW_ENCODING_FIELDS = %w[encoding itemEncoding prefixEncoding].freeze

def check(root)
return [] unless version_before?('3.2')

violations = []
each_node(root, OpenAPIParser::Schemas::MediaType) do |node|
raw = node.raw_schema
next unless raw.is_a?(Hash)

NEW_FIELDS.each do |field|
next unless raw.key?(field)

violations << violation(
path: "#{node.object_reference}/#{field}",
message: "`#{field}` on a Media Type Object is a 3.2 addition; earlier documents have no such field",
)
end

violations.concat(encoding_violations(node, raw['encoding']))
end
violations
end

private

def encoding_violations(node, encodings)
return [] unless encodings.is_a?(Hash)

encodings.flat_map do |property, encoding|
next [] unless encoding.is_a?(Hash)

NEW_ENCODING_FIELDS.select { |field| encoding.key?(field) }.map do |field|
violation(
path: "#{node.object_reference}/encoding/#{escape_reference(property)}/#{field}",
message: "`#{field}` on an Encoding Object is a 3.2 addition; earlier documents have no such field",
)
end
end
end
end
end
end
end
7 changes: 7 additions & 0 deletions sig/openapi_parser/spec_validator.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ module OpenAPIParser
class MediaTypesBefore32 < Rule
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
end

class StreamingFieldsBefore32 < Rule
NEW_FIELDS: Array[String]
NEW_ENCODING_FIELDS: Array[String]
private def encoding_violations: (untyped node, untyped encodings) -> Array[SpecValidator::SpecViolation]
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
end
end
end

Expand Down
27 changes: 27 additions & 0 deletions spec/data/openapi_3_2/streaming_fields_31.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
openapi: 3.1.0
info:
title: Event API
version: '1.0'
paths:
/events:
get:
summary: Stream events
responses:
'200':
description: OK
content:
text/event-stream:
# `itemSchema`, `itemEncoding`, and `prefixEncoding` on a
# Media Type Object are 3.2 additions for sequential media
# types; a 3.1 document has no such fields, so their use here
# is a spec violation.
itemSchema:
type: object
properties:
event:
type: string
itemEncoding:
event:
contentType: text/plain
prefixEncoding:
- contentType: application/json
26 changes: 26 additions & 0 deletions spec/data/openapi_3_2/streaming_fields_32.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
openapi: 3.2.0
info:
title: Event API
version: '1.0'
paths:
/events:
get:
summary: Stream events
responses:
'200':
description: OK
content:
text/event-stream:
# `itemSchema`, `itemEncoding`, and `prefixEncoding` are
# legitimate Media Type Object fields under 3.2, so no
# violation is expected here.
itemSchema:
type: object
properties:
event:
type: string
itemEncoding:
event:
contentType: text/plain
prefixEncoding:
- contentType: application/json
10 changes: 10 additions & 0 deletions spec/openapi_parser/schemas/media_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,14 @@
expect(subject.schema.class).to eq OpenAPIParser::Schemas::Reference
end
end

describe 'itemSchema (OpenAPI 3.2)' do
let(:root) { OpenAPIParser.parse(load_yaml_file('./spec/data/openapi_3_2/streaming_fields_32.yaml'), {}) }

it 'is parsed as a Schema' do
media_type = root.find_object('#/paths/~1events/get/responses/200/content/text~1event-stream')
expect(media_type.item_schema.class).to eq OpenAPIParser::Schemas::Schema
expect(media_type.item_schema.type).to eq 'object'
end
end
end
14 changes: 14 additions & 0 deletions spec/openapi_parser/spec_validator/integration_3_2_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,18 @@ def expect_clean(file)
expect_clean('media_types_32.yaml')
end
end

describe 'Media Type itemSchema/itemEncoding/prefixEncoding (3.2 additions)' do
it 'warns on the version-mismatched document under :warn' do
expect_mismatch_warns('streaming_fields_31.yaml', [:streaming_fields_before32, :streaming_fields_before32, :streaming_fields_before32])
end

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

it 'stays clean on the correctly-versioned document' do
expect_clean('streaming_fields_32.yaml')
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
require_relative '../../../spec_helper'

RSpec.describe 'OpenAPIParser::SpecValidator::Rules::StreamingFieldsBefore32' do
def base_doc(openapi_version_string, media_type)
{
'openapi' => openapi_version_string,
'info' => { 'title' => 'test', 'version' => '1.0' },
'paths' => {
'/events' => {
'get' => {
'responses' => {
'200' => {
'description' => 'OK',
'content' => { 'text/event-stream' => media_type },
},
},
},
},
},
}
end

def doc_with_streaming_fields(openapi_version_string)
media_type = {
'itemSchema' => { 'type' => 'object' },
'itemEncoding' => { 'event' => { 'contentType' => 'text/plain' } },
'prefixEncoding' => [{ 'contentType' => 'application/json' }],
}
OpenAPIParser.parse(base_doc(openapi_version_string, media_type), strict_reference_validation: false)
end

def doc_without_streaming_fields(openapi_version_string)
media_type = { 'schema' => { 'type' => 'string' } }
OpenAPIParser.parse(base_doc(openapi_version_string, media_type), strict_reference_validation: false)
end

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

context 'with a 3.2 document using the streaming fields' do
it 'reports no violation' do
root = doc_with_streaming_fields('3.2.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.2 document without the streaming fields' do
it 'reports no violation' do
root = doc_without_streaming_fields('3.2.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.1 document using the streaming fields' do
it 'reports one violation per offending field pointing at the media type' do
root = doc_with_streaming_fields('3.1.0')
violations = run_rule_for(root)
expect(violations.size).to eq 3
base = '#/paths/~1events/get/responses/200/content/text~1event-stream'
expect(violations.map(&:path)).to match_array [
"#{base}/itemSchema",
"#{base}/itemEncoding",
"#{base}/prefixEncoding",
]
expect(violations.first.rule_name).to eq :streaming_fields_before32
end
end

context 'with a 3.1 document without the streaming fields' do
it 'reports no violation' do
root = doc_without_streaming_fields('3.1.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.0 document using the streaming fields' do
it 'reports violations' do
root = doc_with_streaming_fields('3.0.0')
expect(run_rule_for(root).size).to eq 3
end
end

context 'with a 3.1 document nesting encodings inside an Encoding Object' do
it 'reports each nested field' do
media_type = {
'schema' => { 'type' => 'object' },
'encoding' => {
'parts' => {
'contentType' => 'multipart/mixed',
'itemEncoding' => { 'contentType' => 'application/json' },
'prefixEncoding' => [{ 'contentType' => 'text/plain' }],
'encoding' => { 'meta' => { 'contentType' => 'application/json' } },
},
'plain' => { 'contentType' => 'text/plain' },
},
}
root = OpenAPIParser.parse(base_doc('3.1.0', media_type), strict_reference_validation: false)
expect(run_rule_for(root).map(&:path)).to match_array %w[itemEncoding prefixEncoding encoding].map { |field|
"#/paths/~1events/get/responses/200/content/text~1event-stream/encoding/parts/#{field}"
}
end
end

context 'with a document whose openapi field is not a version' do
it 'reports no violation (rule skipped)' do
root = doc_with_streaming_fields('not-a-version')
expect(run_rule_for(root)).to eq []
end
end
end