Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7049f02
feat: add OpenAPI 3.1 support
reuvenharrison Feb 9, 2026
7d3ea87
fix: resolve $ref in OpenAPI 3.1 schema fields (prefixItems, contains…
reuvenharrison Feb 7, 2026
c73ac0a
fix: recurse into OpenAPI 3.1 fields in transformOpenAPIToJSONSchema
reuvenharrison Feb 7, 2026
3c407ec
fix: validate sub-schemas in OpenAPI 3.1 schema fields
reuvenharrison Feb 7, 2026
7b249bb
feat: add const keyword validation to built-in validator
reuvenharrison Feb 9, 2026
e89b52e
fix: improve OpenAPI 3.1 spec compliance
reuvenharrison Feb 9, 2026
c5a6000
feat: add remaining JSON Schema 2020-12 keywords and improvements
reuvenharrison Feb 9, 2026
53c4d1d
style: fix go fmt formatting
reuvenharrison Feb 9, 2026
b0febe0
fix: resolve 8 correctness issues in OpenAPI 3.1 support
reuvenharrison Feb 10, 2026
8b0c802
fix: avoid CI lint match in OPEN_ISSUES.md
reuvenharrison Feb 10, 2026
95f95f5
chore: remove OPEN_ISSUES.md from repo
reuvenharrison Feb 10, 2026
d54085e
fix: resolve 8 additional OpenAPI 3.1 issues
reuvenharrison Feb 10, 2026
9c16203
feat: add $schema, $defs keywords and fix remaining issues
reuvenharrison Feb 10, 2026
1927325
fix: strip __origin__ from Const and Examples in Schema.UnmarshalJSON
reuvenharrison Mar 15, 2026
dd1d7b1
fix: strip __origin__ from Encoding, Variables, and Webhooks maps
reuvenharrison Mar 16, 2026
82a46c2
feat: honour $ref sibling keywords in OAS 3.1 (e.g. deprecated:true)
reuvenharrison Apr 7, 2026
2cc3060
docs: regenerate openapi3.txt after refs.go header update
reuvenharrison Apr 7, 2026
edb81df
fix: support boolean form for unevaluatedProperties and unevaluatedItems
reuvenharrison Apr 14, 2026
17c9d4a
docs: regenerate openapi3.txt after BoolSchema type change
reuvenharrison Apr 14, 2026
16e2dec
chore: bump yaml3 to v0.0.12 for unconditional field origin tracking
reuvenharrison Apr 15, 2026
db4cb84
test: verify origin tracking for mapping-valued schema fields
reuvenharrison Apr 16, 2026
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
275 changes: 261 additions & 14 deletions .github/docs/openapi3.txt

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@ for _, path := range doc.Paths.InMatchingOrder() {
* `openapi3.Location` gained `File` and `Name` fields (`string` type, replacing previous `int`-only struct layout)
* `openapi3.Origin` gained `Sequences` field (`map[string][]Location`, extending previous `map[string]Location`-only struct)

### v0.132.0
* `openapi3.Schema.ExclusiveMin` and `openapi3.Schema.ExclusiveMax` fields changed from `bool` to `ExclusiveBound` (a union type holding `*bool` for OpenAPI 3.0 or `*float64` for OpenAPI 3.1).
* `openapi3.Schema.PrefixItems` field changed from `[]*SchemaRef` to `SchemaRefs`.
### v0.131.0
* No longer `openapi3filter.RegisterBodyDecoder` the `openapi3filter.ZipFileBodyDecoder` by default.

Expand Down
4 changes: 4 additions & 0 deletions cmd/validate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ func main() {
}

var opts []openapi3.ValidationOption
if doc.IsOpenAPI3_1() {
log.Println("Detected OpenAPI 3.1 document, enabling JSON Schema 2020-12 validation")
opts = append(opts, openapi3.EnableJSONSchema2020Validation())
}
if !*defaults {
opts = append(opts, openapi3.DisableSchemaDefaultsValidation())
}
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ require (
github.com/gorilla/mux v1.8.0
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/oasdiff/yaml v0.0.9
github.com/oasdiff/yaml3 v0.0.9
github.com/oasdiff/yaml3 v0.0.12
github.com/perimeterx/marshmallow v1.1.5
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/stretchr/testify v1.9.0
github.com/woodsbury/decimal128 v1.3.0
)
Expand All @@ -20,5 +21,6 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 8 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
Expand All @@ -20,20 +22,24 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48=
github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM=
github.com/oasdiff/yaml3 v0.0.9 h1:rWPrKccrdUm8J0F3sGuU+fuh9+1K/RdJlWF7O/9yw2g=
github.com/oasdiff/yaml3 v0.0.9/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/oasdiff/yaml3 v0.0.12 h1:75urAtPeDg2/iDEWwzNrLOWxI9N/dCh81nTTJtokt2M=
github.com/oasdiff/yaml3 v0.0.12/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
68 changes: 52 additions & 16 deletions openapi2conv/openapi2_conv.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,8 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete
Enum: parameter.Enum,
Min: parameter.Minimum,
Max: parameter.Maximum,
ExclusiveMin: parameter.ExclusiveMin,
ExclusiveMax: parameter.ExclusiveMax,
ExclusiveMin: openapi3.ExclusiveBound{Bool: boolPtr(parameter.ExclusiveMin)},
ExclusiveMax: openapi3.ExclusiveBound{Bool: boolPtr(parameter.ExclusiveMax)},
MinLength: parameter.MinLength,
MaxLength: parameter.MaxLength,
Default: parameter.Default,
Expand Down Expand Up @@ -495,8 +495,8 @@ func ToV3SchemaRef(schema *openapi2.SchemaRef) *openapi3.SchemaRef {
Example: schema.Value.Example,
ExternalDocs: schema.Value.ExternalDocs,
UniqueItems: schema.Value.UniqueItems,
ExclusiveMin: schema.Value.ExclusiveMin,
ExclusiveMax: schema.Value.ExclusiveMax,
ExclusiveMin: openapi3.ExclusiveBound{Bool: boolPtr(schema.Value.ExclusiveMin)},
ExclusiveMax: openapi3.ExclusiveBound{Bool: boolPtr(schema.Value.ExclusiveMax)},
ReadOnly: schema.Value.ReadOnly,
WriteOnly: schema.Value.WriteOnly,
AllowEmptyValue: schema.Value.AllowEmptyValue,
Expand Down Expand Up @@ -882,10 +882,10 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components
Description: schema.Value.Description,
Type: paramType,
Enum: schema.Value.Enum,
Minimum: schema.Value.Min,
Maximum: schema.Value.Max,
ExclusiveMin: schema.Value.ExclusiveMin,
ExclusiveMax: schema.Value.ExclusiveMax,
Minimum: effectiveMin(schema.Value.Min, schema.Value.ExclusiveMin),
Maximum: effectiveMax(schema.Value.Max, schema.Value.ExclusiveMax),
ExclusiveMin: exclusiveBoundToBool(schema.Value.ExclusiveMin),
ExclusiveMax: exclusiveBoundToBool(schema.Value.ExclusiveMax),
MinLength: schema.Value.MinLength,
MaxLength: schema.Value.MaxLength,
Default: schema.Value.Default,
Expand All @@ -912,15 +912,15 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components
Example: schema.Value.Example,
ExternalDocs: schema.Value.ExternalDocs,
UniqueItems: schema.Value.UniqueItems,
ExclusiveMin: schema.Value.ExclusiveMin,
ExclusiveMax: schema.Value.ExclusiveMax,
ExclusiveMin: exclusiveBoundToBool(schema.Value.ExclusiveMin),
ExclusiveMax: exclusiveBoundToBool(schema.Value.ExclusiveMax),
ReadOnly: schema.Value.ReadOnly,
WriteOnly: schema.Value.WriteOnly,
AllowEmptyValue: schema.Value.AllowEmptyValue,
Deprecated: schema.Value.Deprecated,
XML: schema.Value.XML,
Min: schema.Value.Min,
Max: schema.Value.Max,
Min: effectiveMin(schema.Value.Min, schema.Value.ExclusiveMin),
Max: effectiveMax(schema.Value.Max, schema.Value.ExclusiveMax),
MultipleOf: schema.Value.MultipleOf,
MinLength: schema.Value.MinLength,
MaxLength: schema.Value.MaxLength,
Expand Down Expand Up @@ -1046,16 +1046,16 @@ func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameter
In: "formData",
Extensions: stripNonExtensions(val.Extensions),
Enum: val.Enum,
ExclusiveMin: val.ExclusiveMin,
ExclusiveMax: val.ExclusiveMax,
ExclusiveMin: exclusiveBoundToBool(val.ExclusiveMin),
ExclusiveMax: exclusiveBoundToBool(val.ExclusiveMax),
MinLength: val.MinLength,
MaxLength: val.MaxLength,
Default: val.Default,
Items: v2Items,
MinItems: val.MinItems,
MaxItems: val.MaxItems,
Maximum: val.Max,
Minimum: val.Min,
Maximum: effectiveMax(val.Max, val.ExclusiveMax),
Minimum: effectiveMin(val.Min, val.ExclusiveMin),
Pattern: val.Pattern,
// CollectionFormat: val.CollectionFormat,
// Format: val.Format,
Expand Down Expand Up @@ -1352,3 +1352,39 @@ func compareParameters(a, b *openapi2.Parameter) int {
}
return cmp.Compare(a.Ref, b.Ref)
}

// boolPtr returns a pointer to a bool, or nil if the value is false (to avoid storing empty values)
func boolPtr(b bool) *bool {
if !b {
return nil
}
return &b
}

// exclusiveBoundToBool converts an ExclusiveBound to a bool for OpenAPI 2.0 compatibility
// In OpenAPI 2.0, exclusiveMinimum/exclusiveMaximum are boolean modifiers
func exclusiveBoundToBool(eb openapi3.ExclusiveBound) bool {
if eb.Bool != nil {
return *eb.Bool
}
// If it's a number (OpenAPI 3.1 style), we return true to indicate exclusivity
return eb.Value != nil
}

// effectiveMin returns the minimum value for OAS 2.0 conversion, considering ExclusiveBound.
// In OAS 3.1, exclusiveMinimum is a number. In OAS 2.0, it must be in the minimum field.
func effectiveMin(min *float64, eb openapi3.ExclusiveBound) *float64 {
if min != nil {
return min
}
// If OAS 3.1 style numeric exclusive bound with no minimum, use the bound value as minimum
return eb.Value
}

// effectiveMax returns the maximum value for OAS 2.0 conversion, considering ExclusiveBound.
func effectiveMax(max *float64, eb openapi3.ExclusiveBound) *float64 {
if max != nil {
return max
}
return eb.Value
}
23 changes: 22 additions & 1 deletion openapi3/doc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
// Package openapi3 parses and writes OpenAPI 3 specification documents.
//
// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md
// Supports both OpenAPI 3.0 and OpenAPI 3.1:
// - OpenAPI 3.0.x: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md
// - OpenAPI 3.1.x: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md
//
// OpenAPI 3.1 Features:
// - Type arrays with null support (e.g., ["string", "null"])
// - JSON Schema 2020-12 keywords (const, examples, prefixItems, etc.)
// - Webhooks for defining callback operations
// - JSON Schema dialect specification
// - SPDX license identifiers
//
// The implementation maintains 100% backward compatibility with OpenAPI 3.0.
//
// For OpenAPI 3.1 validation, use the JSON Schema 2020-12 validator option:
//
// schema.VisitJSON(value, openapi3.EnableJSONSchema2020())
//
// Version detection is available via helper methods:
//
// if doc.IsOpenAPI3_1() {
// // Handle OpenAPI 3.1 specific features
// }
package openapi3
Loading
Loading