Skip to content

feat: add OpenAPI 3.1 support#1125

Merged
fenollp merged 46 commits into
getkin:masterfrom
oasdiff:feat/openapi-3.1-support
Apr 24, 2026
Merged

feat: add OpenAPI 3.1 support#1125
fenollp merged 46 commits into
getkin:masterfrom
oasdiff:feat/openapi-3.1-support

Conversation

@reuvenharrison
Copy link
Copy Markdown
Contributor

@reuvenharrison reuvenharrison commented Feb 9, 2026

Summary

Comprehensive OpenAPI 3.1 / JSON Schema 2020-12 support. This consolidates and supersedes PRs #1102, #1114, #1118#1124.

Core features

  • Type arrays with null support (e.g., ["string", "null"])
  • JSON Schema 2020-12 keywords: const, examples, prefixItems, contains, minContains, maxContains, patternProperties, dependentSchemas, propertyNames, unevaluatedItems, unevaluatedProperties
  • Conditional keywords: if/then/else, dependentRequired
  • Identity/referencing keywords: $id, $anchor, $dynamicRef, $dynamicAnchor
  • Content vocabulary: contentMediaType, contentEncoding, contentSchema
  • Core keywords: $schema, $comment, $defs
  • ExclusiveBound union type: supports both boolean (3.0) and numeric (3.1) exclusive bounds
  • BoolSchema type for unevaluatedItems and unevaluatedProperties: supports both boolean (false) and schema object forms, reusing the same pattern as AdditionalProperties
  • Webhooks with $ref resolution
  • Version detection: IsOpenAPI3_0(), IsOpenAPI3_1()
  • Info.Summary and License.Identifier fields

Validation

  • JSON Schema 2020-12 validator via EnableJSONSchema2020() (per-field) and EnableJSONSchema2020Validation() (document-level)
  • Auto-detection: doc.Validate() automatically enables JSON Schema 2020-12 mode for OpenAPI 3.1 documents
  • Built-in validator: const keyword enforcement, IsEmpty() awareness of all new fields
  • $ref resolution in loader for all new schema fields (including $defs)
  • Schema.validate() recursion into all new sub-schema fields
  • transformOpenAPIToJSONSchema recursion into all nested schemas (including $defs)
  • Allow $ref alongside other keywords (legal in 3.1)
  • Handle "null" type in schema validation

Backward compatibility

  • 100% backward compatible with OpenAPI 3.0
  • ExclusiveBound transparently handles both boolean and numeric forms
  • BoolSchema reuses the existing AdditionalProperties pattern (type AdditionalProperties = BoolSchema alias preserves backward compatibility)
  • openapi2conv updated to convert between ExclusiveBound and boolean, preserving numeric bound values

Commit history

  1. feat: add OpenAPI 3.1 support — base implementation (Types, Schema fields, Webhooks, JSON Schema validator, version helpers)
  2. fix: resolve $ref in OpenAPI 3.1 schema fields — loader handles refs in prefixItems, contains, patternProperties, etc.
  3. fix: recurse into OpenAPI 3.1 fields in transformOpenAPIToJSONSchema — transform handles all nested schemas
  4. fix: validate sub-schemas in OpenAPI 3.1 schema fields — Schema.validate() recurses into new fields
  5. feat: add const keyword validation to built-in validator — const enforcement without EnableJSONSchema2020()
  6. fix: improve OpenAPI 3.1 spec compliance — paths optional in 3.1, mutualTLS, license url/identifier exclusivity, auto-enable JSON Schema 2020-12 in openapi3filter
  7. feat: add remaining JSON Schema 2020-12 keywords — $id, $anchor, $dynamicRef, $dynamicAnchor, contentMediaType, contentEncoding, contentSchema, discriminator for anyOf
  8. style: fix go fmt formatting
  9. fix: resolve 8 correctness issues — IsEmpty(), JSONLookup(), validate() array items, transform bugs, MarshalYAML, const comparison, webhooks ordering
  10. fix: resolve 8 additional issues — $comment keyword, PrefixItems type consistency, exclusiveBoundToBool data loss, auto-enable 3.1 validation, test coverage gaps, README breaking changes
  11. feat: add $schema, $defs keywords and fix remaining issues — per-schema dialect, local schema definitions, jsonSchemaDialect URI validation, discriminator refactor
  12. fix: support boolean form for unevaluatedProperties and unevaluatedItems — introduce BoolSchema type (with AdditionalProperties kept as alias), handle unevaluatedProperties: false and unevaluatedItems: false (fixes [3.1] Failed to unmarshal unevaluatedProperties: false (boolean form) oasdiff/oasdiff#844)

Open issues for complete 3.1 support

The following items are known gaps that can be addressed in follow-up PRs:

Missing features

  • $schema keyword — Added per-schema dialect declaration.
  • $defs keyword — Added with full support (struct, marshal, unmarshal, IsEmpty, JSONLookup, validate, loader, transform).
  • $comment keyword — Added as first-class field.
  • $dynamicRef / $dynamicAnchor not resolved — Parsed and serialized but the loader does not resolve them. Schemas using $dynamicRef for recursive references will not work.
  • Built-in validator ignores most 3.1 keywords — Only validates const. Other 3.1 keywords are silently ignored without EnableJSONSchema2020().
  • contentMediaType / contentEncoding not validated at runtime — Spec-compliant (annotation-only in 2020-12), but some users may expect validation.
  • Missing pathItems in Components struct — OAS 3.1 added components/pathItems. Completely absent — silently dropped during parsing.

Bugs / correctness

  • Schema.JSONLookup() missing all new 3.1 fields — All fields added.
  • IsEmpty() missing checks for most new 3.1 fields — All fields added.
  • validate() requires items for type array even in 3.1 — Relaxed.
  • transformOpenAPIToJSONSchema does not clean up exclusiveMinimum: false — Fixed.
  • transformOpenAPIToJSONSchema drops nullable: true without type — Fixed.
  • MarshalYAML unconditionally emits "paths": null for 3.1 docs — Fixed.
  • visitConstOperation uses == for json.Number — Fixed.
  • Webhooks validation iterates non-deterministically — Fixed.
  • exclusiveBoundToBool loses constraint data — Fixed.
  • doc.Validate() does not auto-enable 3.1 mode — Fixed.
  • jsonSchemaDialect URI validation is a no-op — Now requires scheme.
  • unevaluatedProperties: false / unevaluatedItems: false fails to unmarshal — Fixed with BoolSchema type.
  • Built-in validator applies items to ALL array elements, ignoring prefixItems — In 3.1, items applies only beyond the prefixItems tuple.
  • patternProperties omission changes additionalProperties semanticsadditionalProperties: false incorrectly rejects properties matching pattern properties.
  • Silent fallback in visitJSONWithJSONSchema — Silently falls back to built-in validator when JSON Schema compilation fails.
  • Const: nil cannot express "value must be null" — Go nil is the zero value for any.

Breaking API changes

  • ExclusiveMin/ExclusiveMax type changed — Documented in README.
  • PrefixItems type changed — Documented in README.
  • UnevaluatedItems/UnevaluatedProperties type changed from *SchemaRef to BoolSchemaAdditionalProperties kept as type alias for backward compatibility.

Code quality

  • PrefixItems type inconsistency — Changed to SchemaRefs.
  • Discriminator logic duplicated for anyOf — Refactored into resolveDiscriminatorRef helper.

Test coverage

  • No ref resolution test for ContentSchema — Added.
  • No ref resolution test for If/Then/Else — Added.
  • No transformOpenAPIToJSONSchema test for ContentSchema — Added.
  • No Schema.validate() test for ContentSchema — Added.

Test plan

  • Type arrays: serialization, deserialization, helper methods, backward compatibility
  • Webhooks: marshal/unmarshal, validation, JSONLookup
  • Version detection: IsOpenAPI3_0, IsOpenAPI3_1, migration scenarios
  • JSON Schema 2020-12 validator: type, number, object, array, oneOf/anyOf/allOf/not, exclusive bounds, const, nullable
  • if/then/else: conditional validation, marshal round-trip, unresolved ref detection
  • dependentRequired: conditional property requirements
  • Loader: $ref resolution in all 3.1 schema fields + if/then/else + contentSchema
  • Schema.validate(): unresolved ref detection in all new sub-schema fields + contentSchema
  • Const: string, number, boolean, null, object, type+const, multiError
  • BoolSchema: boolean unmarshal for unevaluatedProperties/unevaluatedItems, mutual exclusion validation
  • Full test suite passes (go test ./...)

Supersedes

Closes #1102, closes #1114, closes #1118, closes #1119, closes #1120, closes #1121, closes #1122, closes #1123, closes #1124

🤖 Generated with Claude Code

Co-Authored-By: Chance Kirsch <>
Co-Authored-By: RobbertDM <>

@reuvenharrison
Copy link
Copy Markdown
Contributor Author

Open Issues Status Update

22 of 30 issues resolved across commits 9–11.

Resolved issues (22)

Bugs / correctness (11 fixed)

  • Schema.JSONLookup() — all 23 missing JSON Schema 2020-12 fields added
  • IsEmpty() — missing checks for PrefixItems, Contains, MinContains, MaxContains, PatternProperties, DependentSchemas, PropertyNames, UnevaluatedItems, UnevaluatedProperties, Examples
  • validate() array items — relaxed when jsonSchema2020ValidationEnabled or prefixItems present
  • transformOpenAPIToJSONSchema exclusiveMinimum:false — boolean false now cleaned up
  • transformOpenAPIToJSONSchema nullable without type — now sets type: ["null"]
  • MarshalYAML paths:null — only emits paths when non-nil
  • visitConstOperation json.Number — uses reflect.DeepEqual
  • ✅ Webhooks validation ordering — uses componentNames() for deterministic iteration
  • exclusiveBoundToBool data loss — numeric bound value preserved in OAS 2.0 conversion
  • doc.Validate() auto-enable 3.1 — auto-enables EnableJSONSchema2020Validation() for 3.1 docs
  • jsonSchemaDialect URI validation — now requires a scheme

Missing features (3 added)

  • $schema keyword — per-schema dialect declaration
  • $comment keyword — first-class field for round-trip fidelity
  • $defs keyword — local reusable schema definitions with full ref resolution

Code quality (2 fixed)

  • PrefixItems type — changed from []*SchemaRef to SchemaRefs
  • ✅ Discriminator duplication — refactored into resolveDiscriminatorRef helper

Breaking API changes (2 documented)

  • ExclusiveMin/ExclusiveMax type change — documented in README v0.132.0
  • PrefixItems type change — documented in README v0.132.0

Test coverage (4 added)

  • ✅ Ref resolution test for ContentSchema
  • ✅ Ref resolution test for If/Then/Else
  • transformOpenAPIToJSONSchema test for ContentSchema
  • Schema.validate() test for ContentSchema

Remaining open issues (8)

These are larger-scope items for follow-up PRs:

# Issue Category
1 $dynamicRef/$dynamicAnchor not resolved by loader Missing feature
2 Built-in validator ignores most 3.1 keywords (needs EnableJSONSchema2020()) Missing feature
3 pathItems missing from Components struct Missing feature
4 Built-in validator applies items to ALL elements, ignoring prefixItems Bug
5 patternProperties omission changes additionalProperties semantics Bug
6 Silent fallback in visitJSONWithJSONSchema when compilation fails Bug
7 Const: nil cannot express "value must be null" Bug
8 contentMediaType/contentEncoding not validated at runtime (spec-compliant) Missing feature

@reuvenharrison
Copy link
Copy Markdown
Contributor Author

Coverage vs Pierre Fenoll's Task List

Assessment of this PR against the tasks outlined by Pierre for complete OpenAPI 3.1 support:

Task Status Notes
De/serialization Complete All 3.1 fields with full round-trip (marshal/unmarshal/JSONLookup/IsEmpty)
Document validation Complete All fields validated, auto-detect 3.1, $ref resolution in loader
JSON Schema validation Mostly complete External lib (santhosh-tekuri/jsonschema/v6) chosen; 3.1 keywords delegated to it. Built-in validator only handles const.
3.0 ↔ 3.1 conversion Minimal Only ExclusiveBound conversion in openapi2conv. No general downgrade/upgrade path.
openapi3gen (reflection) Not started Zero changes to openapi3gen/
openapi3filter (middleware) Minimal Auto-enables JSON Schema 2020-12 mode for 3.1 specs in ValidateParameter, ValidateRequestBody, ValidateResponse
3.0/3.1 coexistence Complete IsOpenAPI3_0()/IsOpenAPI3_1(), ExclusiveBound union type, omitempty on all 3.1 fields
Tests Comprehensive 7 new test files, 2 testdata files

What this PR delivers (~60-70% of the full task list)

  • Full data model for all JSON Schema 2020-12 keywords (26+ new Schema fields)
  • Document-level features (Webhooks, JSONSchemaDialect, Info.Summary, License.Identifier, mutualTLS)
  • External JSON Schema 2020-12 validator with automatic 3.1 detection
  • 100% backward compatible with OpenAPI 3.0

Remaining items for follow-up PRs

  1. openapi3gen — Reflection-based generation of 3.1 schemas from Go types (largest gap)
  2. 3.0 ↔ 3.1 conversion — Downgrade path (prefixItems→items, type arrays→nullable, $defs→definitions) and upgrade path
  3. openapi3filter — Deeper middleware integration beyond auto-enabling 3.1 mode
  4. Built-in validator — Currently only validates const; other 3.1 keywords require EnableJSONSchema2020()
  5. $dynamicRef resolution — Parsed/serialized but loader does not resolve dynamic references
  6. components/pathItems — OAS 3.1 added this field; currently silently dropped during parsing

@reuvenharrison reuvenharrison marked this pull request as ready for review February 10, 2026 19:36
reuvenharrison referenced this pull request in oasdiff/yaml Mar 13, 2026
Pass source file path to yaml3 decoder so origin locations include
the file they were parsed from.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
reuvenharrison added a commit to oasdiff/kin-openapi that referenced this pull request Mar 15, 2026
Open issues are tracked in the PR getkin#1125 description instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@reuvenharrison reuvenharrison force-pushed the feat/openapi-3.1-support branch from da004e9 to 4e8f560 Compare March 15, 2026 17:32
AlinsRan pushed a commit to AlinsRan/kin-openapi that referenced this pull request Mar 25, 2026
Open issues are tracked in the PR getkin#1125 description instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AlinsRan pushed a commit to AlinsRan/kin-openapi that referenced this pull request Mar 26, 2026
Open issues are tracked in the PR getkin#1125 description instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
reuvenharrison added a commit to oasdiff/kin-openapi that referenced this pull request Apr 6, 2026
Open issues are tracked in the PR getkin#1125 description instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@reuvenharrison reuvenharrison force-pushed the feat/openapi-3.1-support branch from e308c1f to 76c0438 Compare April 6, 2026 15:04
reuvenharrison added a commit to oasdiff/kin-openapi that referenced this pull request Apr 6, 2026
Open issues are tracked in the PR getkin#1125 description instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@stavros-k
Copy link
Copy Markdown

Does this branch solve/allow this?

#1114 (comment)

@reuvenharrison
Copy link
Copy Markdown
Contributor Author

Yes! This is now supported and tested. I just added a commit that:

  1. Parses sibling keywords alongside $ref when loading an OAS 3.1 document
  2. Applies them onto a copy of the resolved schema after $ref resolution

So your spec is now handled correctly:

status:
  deprecated: true   # sibling keyword — valid in OAS 3.1
  $ref: '#/components/schemas/PingStatus'

After loading, statusRef.Value.Deprecated == true and the $ref is still fully resolved.

The fix is gated on the document version (3.1.x) so OAS 3.0 behaviour is unchanged.

See TestOAS31_RefSiblingKeyword in openapi3/loader_31_schema_refs_test.go for the full test.

reuvenharrison added a commit to oasdiff/kin-openapi that referenced this pull request Apr 11, 2026
Open issues are tracked in the PR getkin#1125 description instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@reuvenharrison reuvenharrison force-pushed the feat/openapi-3.1-support branch from a280a48 to cdd3f1b Compare April 11, 2026 15:09
fenollp and others added 17 commits April 23, 2026 18:37
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
…idationOption) error

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
…TLS for v3.0

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
Pierre's FIXME on PR #15: the helper previously covered only 8 fields
(deprecated, description, title, readOnly, writeOnly, example,
externalDocs, default). Extend to every field in the Schema struct so
any sibling keyword alongside $ref in an OAS 3.1 document is correctly
overlaid onto the resolved $ref target.

Section comments dropped: the entire function runs only when the caller
has already checked IsOpenAPI31OrLater(), so splitting cases by OAS
version inside the switch was misleading.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Split the $ref sibling-fields check out of (*Ref).Validate into a
dedicated validateExtras method, generated from refs.tmpl for every
Ref type. This lets subsequent patches invoke validateExtras on
SchemaRefs referenced from inside a schema tree — i.e. at the sites
where Schema.validate recurses into nested SchemaRefs — without
duplicating the check logic.

Template update + `go generate` regeneration. No behaviour change:
Validate still returns the same error for the same inputs; the check
is just reachable from additional call sites.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…ression

TestSchemaRefSiblingKeyword/{3.0 false false} (added by @fenollp in
this PR) exposed that $ref sibling fields (e.g. description,
deprecated) were accepted on nested property schemas in OAS 3.0 when
they should have been rejected. Root cause: SchemaRef.validateExtras
was invoked only from SchemaRef.Validate, but nested schema
validation in Schema.validate recursed directly into
ref.Value.Validate, bypassing the ref-level extras check.

Invoke ref.validateExtras(ctx) at every nested SchemaRef site: items,
properties, additionalProperties, prefixItems, contains,
patternProperties, dependentSchemas, $defs, propertyNames,
unevaluatedItems, unevaluatedProperties, contentSchema, if/then/else.

Drop the t.Skip on TestSchemaRefSiblingKeyword now that all three
subcases pass.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…o $ref

lxkns.yaml is OAS 3.0.2 and uses `description` as a sibling to $ref in
many places, which is invalid in OAS 3.0 ($ref replaces the enclosing
object; siblings are ignored). Previously this was silently accepted
because validateExtras was only invoked on top-level component refs.
Now that nested SchemaRefs are validated too, those siblings surface
as a validation error and mask the example-type mismatch this test
actually targets.

Pass AllowExtraSiblingFields("description") so validation proceeds to
the example-type assertion the test was written for (same pattern
already used in issue513_test.go:264).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Per Pierre's PR #15 review: Schema.validate accepted OAS 3.1 / JSON
Schema 2020-12 fields (prefixItems, contains, if/then/else, $defs,
$schema, patternProperties, unevaluatedItems, etc.) on OAS 3.0
documents. Add a gate at the top of Schema.validate that rejects
each 3.1-only field in 3.0 mode via errFieldFor31Plus.

The gate honours AllowExtraSiblingFields so 3.0 documents that resolve
external JSON Schema refs (draft-04, draft-07) can opt in to letting
$id/$schema/etc. through. This matches the existing opt-in mechanism
already used by TestExtraSiblingsInRemoteRef.

This whole block becomes a no-op once OAS 3.0 gets its own standalone
schema validator.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Three tests need adjustment now that OAS 3.1-only schema fields are
rejected under OAS 3.0 validation:

- TestSchemaIfThenElse_Validate and TestSchemaValidate31SubSchemas
  exercise 3.1 features (If/Then/Else, PrefixItems) directly via
  Schema.Validate without a doc, so the isOpenAPI31OrLater flag is
  never set from a doc version. Pass IsOpenAPI31OrLater() explicitly.

- TestIssue495WithDraft04{,Bis} load OAS 3.0 documents that $ref
  external JSON Schema draft-04 meta-schemas (which contain $id and
  $schema). Add AllowExtraSiblingFields("$id", "$schema") so the
  tests assertion about the unresolved inner "#" ref surfaces as
  intended, matching the pattern already used in issue513_test.go.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
@fenollp
Copy link
Copy Markdown
Collaborator

fenollp commented Apr 23, 2026

Alright! oasdiff#15 is rebased and ready to merge in here.

@reuvenharrison
Copy link
Copy Markdown
Contributor Author

Merged #15 into feat/openapi-3.1-support. Tests green on the PR. Over to you for the final merge.

@fenollp fenollp merged commit 959d1c6 into getkin:master Apr 24, 2026
5 checks passed
fenollp pushed a commit that referenced this pull request Apr 24, 2026
Open issues are tracked in the PR #1125 description instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fenollp pushed a commit that referenced this pull request Apr 24, 2026
Rename the v0.132.0 entry to v0.136.0 (the upcoming release for this
PR), move it above v0.135.0 for chronological order, and add the
UnevaluatedItems/UnevaluatedProperties → BoolSchema type change.

Addresses fenollp's review on #1125.
@fenollp
Copy link
Copy Markdown
Collaborator

fenollp commented Apr 24, 2026

FYI the schema error conversion for 3.1 (formatValidationError) is probably not working as you intended. It returns null schema & value in the error.

From #1155 I get this:

           	--- Expected
           	+++ Actual
           	@@ -1 +1,7 @@
           	+invalid components: parameter "headers": invalid example: validation failed due to: at '': got string, want object
           	+Schema:
           	+  null
          	 
           	+Value:
           	+  null
           	+
Test:       	TestV3ApisGuruOpenapiDirectory/webscraping_ai_3_0_0_openapi_yaml_

fenollp added a commit to fenollp/kin-openapi that referenced this pull request Apr 24, 2026
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
fenollp added a commit that referenced this pull request Apr 24, 2026
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
@reuvenharrison
Copy link
Copy Markdown
Contributor Author

Confirmed. formatValidationError in openapi3/schema_jsonschema_validator.go:162-198 constructs &SchemaError{Reason: ..., Origin: ...} but never sets .Schema or .Value. The underlying jsonschema.ValidationError only carries SchemaURL string and InstanceLocation []string, so populating the fields correctly means threading the original value and owning schema through convertJSONSchemaError and validate so we can walk InstanceLocation to extract the sub-value and resolve SchemaURL to a *Schema.

Not a v0.136.0 blocker in my view since Reason is still correct; only the structured fields are null. I'll open a follow-up PR for a point release. Thanks for catching it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[3.1] Failed to unmarshal unevaluatedProperties: false (boolean form)

3 participants