Files
oikos/vendor/github.com/getkin/kin-openapi/openapi3/schema.go
dtoro febc153b7f
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
fix: add involves edge from task to agent:nomos at creation
Plus sync vendor directory for Docker build compatibility.
2026-08-11 22:03:12 +02:00

3183 lines
84 KiB
Go

package openapi3
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"math"
"math/big"
"reflect"
"slices"
"strconv"
"strings"
"sync"
"unicode/utf16"
"github.com/go-openapi/jsonpointer"
)
const (
TypeArray = "array"
TypeBoolean = "boolean"
TypeInteger = "integer"
TypeNumber = "number"
TypeObject = "object"
TypeString = "string"
TypeNull = "null"
)
var (
// SchemaErrorDetailsDisabled disables printing of details about schema errors.
SchemaErrorDetailsDisabled = false
errSchema = errors.New("input does not match the schema")
// ErrOneOfConflict is the SchemaError Origin when data matches more than one oneOf schema
ErrOneOfConflict = errors.New("input matches more than one oneOf schemas")
// ErrSchemaInputNaN may be returned when validating a number
ErrSchemaInputNaN = errors.New("floating point NaN is not allowed")
// ErrSchemaInputInf may be returned when validating a number
ErrSchemaInputInf = errors.New("floating point Inf is not allowed")
compiledPatterns sync.Map
)
// NewSchemaRef simply builds a SchemaRef
func NewSchemaRef(ref string, value *Schema) *SchemaRef {
return &SchemaRef{
Ref: ref,
Value: value,
}
}
type SchemaRefs []*SchemaRef
var _ jsonpointer.JSONPointable = (*SchemaRefs)(nil)
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
func (s SchemaRefs) JSONLookup(token string) (any, error) {
i, err := strconv.ParseUint(token, 10, 64)
if err != nil {
return nil, err
}
if i >= uint64(len(s)) {
return nil, fmt.Errorf("index out of range: %d", i)
}
ref := s[i]
if ref == nil || ref.Ref != "" {
return &Ref{Ref: ref.Ref}, nil
}
return ref.Value, nil
}
// Schema is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object
// and https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#schema-object
type Schema struct {
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"-" yaml:"-"`
OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"`
AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"`
AllOf SchemaRefs `json:"allOf,omitempty" yaml:"allOf,omitempty"`
Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"`
Type *Types `json:"type,omitempty" yaml:"type,omitempty"`
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Format string `json:"format,omitempty" yaml:"format,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"`
Default any `json:"default,omitempty" yaml:"default,omitempty"`
Example any `json:"example,omitempty" yaml:"example,omitempty"`
ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
// Array-related, here for struct compactness
UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"`
// Number-related, here for struct compactness
ExclusiveMin ExclusiveBound `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` // Number for v3.1+ otherwise boolean
ExclusiveMax ExclusiveBound `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` // Number for v3.1+ otherwise boolean
// Properties
Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"`
WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"`
AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"`
Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
XML *XML `json:"xml,omitempty" yaml:"xml,omitempty"`
// Number
Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"`
Max *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"`
MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"`
// String
MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"`
MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"`
Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"`
// Array
MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"`
MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"`
Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"`
// Object
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"`
MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
AdditionalProperties AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
Const any `json:"const,omitempty" yaml:"const,omitempty"` // OpenAPI >=3.1
Examples []any `json:"examples,omitempty" yaml:"examples,omitempty"` // OpenAPI >=3.1
PrefixItems SchemaRefs `json:"prefixItems,omitempty" yaml:"prefixItems,omitempty"` // OpenAPI >=3.1
Contains *SchemaRef `json:"contains,omitempty" yaml:"contains,omitempty"` // OpenAPI >=3.1
MinContains *uint64 `json:"minContains,omitempty" yaml:"minContains,omitempty"` // OpenAPI >=3.1
MaxContains *uint64 `json:"maxContains,omitempty" yaml:"maxContains,omitempty"` // OpenAPI >=3.1
PatternProperties Schemas `json:"patternProperties,omitempty" yaml:"patternProperties,omitempty"` // OpenAPI >=3.1
DependentSchemas Schemas `json:"dependentSchemas,omitempty" yaml:"dependentSchemas,omitempty"` // OpenAPI >=3.1
PropertyNames *SchemaRef `json:"propertyNames,omitempty" yaml:"propertyNames,omitempty"` // OpenAPI >=3.1
UnevaluatedItems BoolSchema `json:"unevaluatedItems,omitempty" yaml:"unevaluatedItems,omitempty"` // OpenAPI >=3.1
UnevaluatedProperties BoolSchema `json:"unevaluatedProperties,omitempty" yaml:"unevaluatedProperties,omitempty"` // OpenAPI >=3.1
If *SchemaRef `json:"if,omitempty" yaml:"if,omitempty"` // OpenAPI >=3.1
Then *SchemaRef `json:"then,omitempty" yaml:"then,omitempty"` // OpenAPI >=3.1
Else *SchemaRef `json:"else,omitempty" yaml:"else,omitempty"` // OpenAPI >=3.1
DependentRequired map[string][]string `json:"dependentRequired,omitempty" yaml:"dependentRequired,omitempty"` // OpenAPI >=3.1
Defs Schemas `json:"$defs,omitempty" yaml:"$defs,omitempty"` // OpenAPI >=3.1
SchemaDialect string `json:"$schema,omitempty" yaml:"$schema,omitempty"` // OpenAPI >=3.1
Comment string `json:"$comment,omitempty" yaml:"$comment,omitempty"` // OpenAPI >=3.1
SchemaID string `json:"$id,omitempty" yaml:"$id,omitempty"` // OpenAPI >=3.1
Anchor string `json:"$anchor,omitempty" yaml:"$anchor,omitempty"` // OpenAPI >=3.1
DynamicRef string `json:"$dynamicRef,omitempty" yaml:"$dynamicRef,omitempty"` // OpenAPI >=3.1
DynamicAnchor string `json:"$dynamicAnchor,omitempty" yaml:"$dynamicAnchor,omitempty"` // OpenAPI >=3.1
ContentMediaType string `json:"contentMediaType,omitempty" yaml:"contentMediaType,omitempty"` // OpenAPI >=3.1
ContentEncoding string `json:"contentEncoding,omitempty" yaml:"contentEncoding,omitempty"` // OpenAPI >=3.1
ContentSchema *SchemaRef `json:"contentSchema,omitempty" yaml:"contentSchema,omitempty"` // OpenAPI >=3.1
}
// Types represents the type(s) of a schema.
//
// In OpenAPI 3.0, this is typically a single type (e.g., "string").
// In OpenAPI 3.1, it can be an array of types (e.g., ["string", "null"]).
//
// Serialization behavior:
// - Single type: serializes as a string (e.g., "string")
// - Multiple types: serializes as an array (e.g., ["string", "null"])
// - Accepts both string and array formats when unmarshaling
//
// Example OpenAPI 3.0 (single type):
//
// schema := &Schema{Type: &Types{"string"}}
// // JSON: {"type": "string"}
//
// Example OpenAPI 3.1 (type array):
//
// schema := &Schema{Type: &Types{"string", "null"}}
// // JSON: {"type": ["string", "null"]}
type Types []string
// Is returns true if the schema has exactly one type and it matches the given type.
// This is useful for OpenAPI 3.0 style single-type checks.
//
// Example:
//
// types := &Types{"string"}
// types.Is("string") // true
// types.Is("number") // false
//
// types = &Types{"string", "null"}
// types.Is("string") // false (multiple types)
func (types *Types) Is(typ string) bool {
return types != nil && len(*types) == 1 && (*types)[0] == typ
}
// Slice returns the types as a string slice.
// Returns nil if types is nil.
//
// Example:
//
// types := &Types{"string", "null"}
// slice := types.Slice() // []string{"string", "null"}
func (types *Types) Slice() []string {
if types == nil {
return nil
}
return *types
}
// Includes returns true if the given type is included in the type array.
// Returns false if types is nil.
//
// Example:
//
// types := &Types{"string", "null"}
// types.Includes("string") // true
// types.Includes("null") // true
// types.Includes("number") // false
func (pTypes *Types) Includes(typ string) bool {
if pTypes == nil {
return false
}
return slices.Contains(*pTypes, typ)
}
// Permits returns true if the given type is permitted.
// Returns true if types is nil (any type allowed), otherwise checks if the type is included.
//
// Example:
//
// var nilTypes *Types
// nilTypes.Permits("anything") // true (nil permits everything)
//
// types := &Types{"string"}
// types.Permits("string") // true
// types.Permits("number") // false
func (types *Types) Permits(typ string) bool {
if types == nil {
return true
}
return types.Includes(typ)
}
// IncludesNull returns true if the type array includes "null".
// This is useful for OpenAPI 3.1 where null is a first-class type.
//
// Example:
//
// types := &Types{"string", "null"}
// types.IncludesNull() // true
//
// types = &Types{"string"}
// types.IncludesNull() // false
func (types *Types) IncludesNull() bool {
return types.Includes(TypeNull)
}
// IsMultiple returns true if multiple types are specified.
// This is an OpenAPI 3.1 feature that enables type arrays.
//
// Example:
//
// types := &Types{"string"}
// types.IsMultiple() // false
//
// types = &Types{"string", "null"}
// types.IsMultiple() // true
func (types *Types) IsMultiple() bool {
return types != nil && len(*types) > 1
}
// IsSingle returns true if exactly one type is specified.
//
// Example:
//
// types := &Types{"string"}
// types.IsSingle() // true
//
// types = &Types{"string", "null"}
// types.IsSingle() // false
func (types *Types) IsSingle() bool {
return types != nil && len(*types) == 1
}
// IsEmpty returns true if no types are specified (nil or empty array).
// When a schema has no type specified, it permits any type.
//
// Example:
//
// var nilTypes *Types
// nilTypes.IsEmpty() // true
//
// types := &Types{}
// types.IsEmpty() // true
//
// types = &Types{"string"}
// types.IsEmpty() // false
func (types *Types) IsEmpty() bool {
return types == nil || len(*types) == 0
}
func (pTypes *Types) MarshalJSON() ([]byte, error) {
x, err := pTypes.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
func (pTypes *Types) MarshalYAML() (any, error) {
if pTypes == nil {
return nil, nil
}
types := *pTypes
switch len(types) {
case 0:
return nil, nil
case 1:
return types[0], nil
default:
return []string(types), nil
}
}
func (types *Types) UnmarshalJSON(data []byte) error {
var strings []string
if err := json.Unmarshal(data, &strings); err != nil {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return unmarshalError(err)
}
strings = []string{s}
}
*types = strings
return nil
}
// BoolSchema represents a JSON Schema keyword that can be either a boolean or a schema object.
// Used for additionalProperties, unevaluatedProperties, and unevaluatedItems.
type BoolSchema struct {
Has *bool
Schema *SchemaRef
}
// AdditionalProperties is a type alias for BoolSchema, kept for backward compatibility.
type AdditionalProperties = BoolSchema
// MarshalYAML returns the YAML encoding of BoolSchema.
func (bs BoolSchema) MarshalYAML() (any, error) {
if x := bs.Has; x != nil {
if *x {
return true, nil
}
return false, nil
}
if x := bs.Schema; x != nil {
return x.MarshalYAML()
}
return nil, nil
}
// MarshalJSON returns the JSON encoding of BoolSchema.
func (bs BoolSchema) MarshalJSON() ([]byte, error) {
x, err := bs.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// UnmarshalJSON sets BoolSchema to a copy of data.
func (bs *BoolSchema) UnmarshalJSON(data []byte) error {
var x any
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
switch y := x.(type) {
case nil:
case bool:
bs.Has = &y
case map[string]any:
if len(y) == 0 {
bs.Schema = &SchemaRef{Value: &Schema{}}
} else {
buf := new(bytes.Buffer)
_ = json.NewEncoder(buf).Encode(y)
if err := json.NewDecoder(buf).Decode(&bs.Schema); err != nil {
return err
}
}
default:
return errors.New("cannot unmarshal: value must be either a schema object or a boolean")
}
return nil
}
// ExclusiveBound represents exclusiveMinimum/exclusiveMaximum which changed type between OpenAPI versions.
// In OpenAPI 3.0 (JSON Schema draft-04): boolean that modifies minimum/maximum
// In OpenAPI 3.1 (JSON Schema 2020-12): number representing the actual exclusive bound
type ExclusiveBound struct {
Bool *bool // For OpenAPI 3.0 style (modifier for min/max)
Value *float64 // For OpenAPI 3.1 style (actual bound value)
}
// IsSet returns true if either Bool or Value is set.
func (eb ExclusiveBound) IsSet() bool {
return eb.Bool != nil || eb.Value != nil
}
// IsTrue returns true if the bound is set as a boolean true (OpenAPI 3.0 style).
func (eb ExclusiveBound) IsTrue() bool {
return eb.Bool != nil && *eb.Bool
}
// MarshalYAML returns the YAML encoding of ExclusiveBound.
func (eb ExclusiveBound) MarshalYAML() (any, error) {
if eb.Value != nil {
return *eb.Value, nil
}
if eb.Bool != nil {
return *eb.Bool, nil
}
return nil, nil
}
// MarshalJSON returns the JSON encoding of ExclusiveBound.
func (eb ExclusiveBound) MarshalJSON() ([]byte, error) {
x, err := eb.MarshalYAML()
if err != nil {
return nil, err
}
if x == nil {
return nil, nil
}
return json.Marshal(x)
}
// UnmarshalJSON sets ExclusiveBound to a copy of data.
func (eb *ExclusiveBound) UnmarshalJSON(data []byte) error {
var x any
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
switch y := x.(type) {
case nil:
// nothing to do
case bool:
eb.Bool = &y
case float64:
eb.Value = &y
default:
return errors.New("cannot unmarshal exclusiveMinimum/exclusiveMaximum: value must be either a number or a boolean")
}
return nil
}
var _ jsonpointer.JSONPointable = (*Schema)(nil)
func NewSchema() *Schema {
return &Schema{}
}
// MarshalJSON returns the JSON encoding of Schema.
func (schema Schema) MarshalJSON() ([]byte, error) {
m, err := schema.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(m)
}
// MarshalYAML returns the YAML encoding of Schema.
func (schema Schema) MarshalYAML() (any, error) {
m := make(map[string]any, 61+len(schema.Extensions))
maps.Copy(m, schema.Extensions)
if x := schema.OneOf; len(x) != 0 {
m["oneOf"] = x
}
if x := schema.AnyOf; len(x) != 0 {
m["anyOf"] = x
}
if x := schema.AllOf; len(x) != 0 {
m["allOf"] = x
}
if x := schema.Not; x != nil {
m["not"] = x
}
if x := schema.Type; x != nil {
m["type"] = x
}
if x := schema.Title; len(x) != 0 {
m["title"] = x
}
if x := schema.Format; len(x) != 0 {
m["format"] = x
}
if x := schema.Description; len(x) != 0 {
m["description"] = x
}
if x := schema.Enum; len(x) != 0 {
m["enum"] = x
}
if x := schema.Default; x != nil {
m["default"] = x
}
if x := schema.Example; x != nil {
m["example"] = x
}
if x := schema.ExternalDocs; x != nil {
m["externalDocs"] = x
}
// Array-related
if x := schema.UniqueItems; x {
m["uniqueItems"] = x
}
// Number-related
if x := schema.ExclusiveMin; x.IsSet() {
m["exclusiveMinimum"] = x
}
if x := schema.ExclusiveMax; x.IsSet() {
m["exclusiveMaximum"] = x
}
// Properties
if x := schema.Nullable; x {
m["nullable"] = x
}
if x := schema.ReadOnly; x {
m["readOnly"] = x
}
if x := schema.WriteOnly; x {
m["writeOnly"] = x
}
if x := schema.AllowEmptyValue; x {
m["allowEmptyValue"] = x
}
if x := schema.Deprecated; x {
m["deprecated"] = x
}
if x := schema.XML; x != nil {
m["xml"] = x
}
// Number
if x := schema.Min; x != nil {
m["minimum"] = x
}
if x := schema.Max; x != nil {
m["maximum"] = x
}
if x := schema.MultipleOf; x != nil {
m["multipleOf"] = x
}
// String
if x := schema.MinLength; x != 0 {
m["minLength"] = x
}
if x := schema.MaxLength; x != nil {
m["maxLength"] = x
}
if x := schema.Pattern; x != "" {
m["pattern"] = x
}
// Array
if x := schema.MinItems; x != 0 {
m["minItems"] = x
}
if x := schema.MaxItems; x != nil {
m["maxItems"] = x
}
if x := schema.Items; x != nil {
m["items"] = x
}
// Object
if x := schema.Required; len(x) != 0 {
m["required"] = x
}
if x := schema.Properties; len(x) != 0 {
m["properties"] = x
}
if x := schema.MinProps; x != 0 {
m["minProperties"] = x
}
if x := schema.MaxProps; x != nil {
m["maxProperties"] = x
}
if x := schema.AdditionalProperties; x.Has != nil || x.Schema != nil {
m["additionalProperties"] = &x
}
if x := schema.Discriminator; x != nil {
m["discriminator"] = x
}
// OpenAPI 3.1 / JSON Schema 2020-12 fields
if x := schema.Const; x != nil {
m["const"] = x
}
if x := schema.Examples; len(x) != 0 {
m["examples"] = x
}
if x := schema.PrefixItems; len(x) != 0 {
m["prefixItems"] = x
}
if x := schema.Contains; x != nil {
m["contains"] = x
}
if x := schema.MinContains; x != nil {
m["minContains"] = x
}
if x := schema.MaxContains; x != nil {
m["maxContains"] = x
}
if x := schema.PatternProperties; len(x) != 0 {
m["patternProperties"] = x
}
if x := schema.DependentSchemas; len(x) != 0 {
m["dependentSchemas"] = x
}
if x := schema.PropertyNames; x != nil {
m["propertyNames"] = x
}
if x := schema.UnevaluatedItems; x.Has != nil || x.Schema != nil {
m["unevaluatedItems"] = &x
}
if x := schema.UnevaluatedProperties; x.Has != nil || x.Schema != nil {
m["unevaluatedProperties"] = &x
}
if x := schema.If; x != nil {
m["if"] = x
}
if x := schema.Then; x != nil {
m["then"] = x
}
if x := schema.Else; x != nil {
m["else"] = x
}
if x := schema.DependentRequired; len(x) != 0 {
m["dependentRequired"] = x
}
if x := schema.Defs; len(x) != 0 {
m["$defs"] = x
}
if x := schema.SchemaDialect; x != "" {
m["$schema"] = x
}
if x := schema.Comment; x != "" {
m["$comment"] = x
}
if x := schema.SchemaID; x != "" {
m["$id"] = x
}
if x := schema.Anchor; x != "" {
m["$anchor"] = x
}
if x := schema.DynamicRef; x != "" {
m["$dynamicRef"] = x
}
if x := schema.DynamicAnchor; x != "" {
m["$dynamicAnchor"] = x
}
if x := schema.ContentMediaType; x != "" {
m["contentMediaType"] = x
}
if x := schema.ContentEncoding; x != "" {
m["contentEncoding"] = x
}
if x := schema.ContentSchema; x != nil {
m["contentSchema"] = x
}
return m, nil
}
// UnmarshalJSON sets Schema to a copy of data.
func (schema *Schema) UnmarshalJSON(data []byte) error {
type SchemaBis Schema
var x SchemaBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, "oneOf")
delete(x.Extensions, "anyOf")
delete(x.Extensions, "allOf")
delete(x.Extensions, "not")
delete(x.Extensions, "type")
delete(x.Extensions, "title")
delete(x.Extensions, "format")
delete(x.Extensions, "description")
delete(x.Extensions, "enum")
delete(x.Extensions, "default")
delete(x.Extensions, "example")
delete(x.Extensions, "externalDocs")
// Array-related
delete(x.Extensions, "uniqueItems")
// Number-related
delete(x.Extensions, "exclusiveMinimum")
delete(x.Extensions, "exclusiveMaximum")
// Properties
delete(x.Extensions, "nullable")
delete(x.Extensions, "readOnly")
delete(x.Extensions, "writeOnly")
delete(x.Extensions, "allowEmptyValue")
delete(x.Extensions, "deprecated")
delete(x.Extensions, "xml")
// Number
delete(x.Extensions, "minimum")
delete(x.Extensions, "maximum")
delete(x.Extensions, "multipleOf")
// String
delete(x.Extensions, "minLength")
delete(x.Extensions, "maxLength")
delete(x.Extensions, "pattern")
// Array
delete(x.Extensions, "minItems")
delete(x.Extensions, "maxItems")
delete(x.Extensions, "items")
// Object
delete(x.Extensions, "required")
delete(x.Extensions, "properties")
delete(x.Extensions, "minProperties")
delete(x.Extensions, "maxProperties")
delete(x.Extensions, "additionalProperties")
delete(x.Extensions, "discriminator")
// OpenAPI 3.1 / JSON Schema 2020-12 fields
delete(x.Extensions, "const")
delete(x.Extensions, "examples")
delete(x.Extensions, "prefixItems")
delete(x.Extensions, "contains")
delete(x.Extensions, "minContains")
delete(x.Extensions, "maxContains")
delete(x.Extensions, "patternProperties")
delete(x.Extensions, "dependentSchemas")
delete(x.Extensions, "propertyNames")
delete(x.Extensions, "unevaluatedItems")
delete(x.Extensions, "unevaluatedProperties")
delete(x.Extensions, "if")
delete(x.Extensions, "then")
delete(x.Extensions, "else")
delete(x.Extensions, "dependentRequired")
delete(x.Extensions, "$defs")
delete(x.Extensions, "$schema")
delete(x.Extensions, "$comment")
delete(x.Extensions, "$id")
delete(x.Extensions, "$anchor")
delete(x.Extensions, "$dynamicRef")
delete(x.Extensions, "$dynamicAnchor")
delete(x.Extensions, "contentMediaType")
delete(x.Extensions, "contentEncoding")
delete(x.Extensions, "contentSchema")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*schema = Schema(x)
return nil
}
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
func (schema Schema) JSONLookup(token string) (any, error) {
switch token {
case "additionalProperties":
if addProps := schema.AdditionalProperties.Has; addProps != nil {
return *addProps, nil
}
if addProps := schema.AdditionalProperties.Schema; addProps != nil {
if addProps.Ref != "" {
return &Ref{Ref: addProps.Ref}, nil
}
return addProps.Value, nil
}
case "not":
if schema.Not != nil {
if schema.Not.Ref != "" {
return &Ref{Ref: schema.Not.Ref}, nil
}
return schema.Not.Value, nil
}
case "items":
if schema.Items != nil {
if schema.Items.Ref != "" {
return &Ref{Ref: schema.Items.Ref}, nil
}
return schema.Items.Value, nil
}
case "oneOf":
return schema.OneOf, nil
case "anyOf":
return schema.AnyOf, nil
case "allOf":
return schema.AllOf, nil
case "type":
return schema.Type, nil
case "title":
return schema.Title, nil
case "format":
return schema.Format, nil
case "description":
return schema.Description, nil
case "enum":
return schema.Enum, nil
case "default":
return schema.Default, nil
case "example":
return schema.Example, nil
case "externalDocs":
return schema.ExternalDocs, nil
case "uniqueItems":
return schema.UniqueItems, nil
case "exclusiveMin":
return schema.ExclusiveMin, nil
case "exclusiveMax":
return schema.ExclusiveMax, nil
case "nullable":
return schema.Nullable, nil
case "readOnly":
return schema.ReadOnly, nil
case "writeOnly":
return schema.WriteOnly, nil
case "allowEmptyValue":
return schema.AllowEmptyValue, nil
case "xml":
return schema.XML, nil
case "deprecated":
return schema.Deprecated, nil
case "min":
return schema.Min, nil
case "max":
return schema.Max, nil
case "multipleOf":
return schema.MultipleOf, nil
case "minLength":
return schema.MinLength, nil
case "maxLength":
return schema.MaxLength, nil
case "pattern":
return schema.Pattern, nil
case "minItems":
return schema.MinItems, nil
case "maxItems":
return schema.MaxItems, nil
case "required":
return schema.Required, nil
case "properties":
return schema.Properties, nil
case "minProps":
return schema.MinProps, nil
case "maxProps":
return schema.MaxProps, nil
case "discriminator":
return schema.Discriminator, nil
// OpenAPI 3.1 / JSON Schema 2020-12 fields
case "const":
return schema.Const, nil
case "examples":
return schema.Examples, nil
case "prefixItems":
return schema.PrefixItems, nil
case "contains":
if schema.Contains != nil {
if schema.Contains.Ref != "" {
return &Ref{Ref: schema.Contains.Ref}, nil
}
return schema.Contains.Value, nil
}
case "minContains":
return schema.MinContains, nil
case "maxContains":
return schema.MaxContains, nil
case "patternProperties":
return schema.PatternProperties, nil
case "dependentSchemas":
return schema.DependentSchemas, nil
case "propertyNames":
if schema.PropertyNames != nil {
if schema.PropertyNames.Ref != "" {
return &Ref{Ref: schema.PropertyNames.Ref}, nil
}
return schema.PropertyNames.Value, nil
}
case "unevaluatedItems":
if ui := schema.UnevaluatedItems.Has; ui != nil {
return *ui, nil
}
if ui := schema.UnevaluatedItems.Schema; ui != nil {
if ui.Ref != "" {
return &Ref{Ref: ui.Ref}, nil
}
return ui.Value, nil
}
case "unevaluatedProperties":
if up := schema.UnevaluatedProperties.Has; up != nil {
return *up, nil
}
if up := schema.UnevaluatedProperties.Schema; up != nil {
if up.Ref != "" {
return &Ref{Ref: up.Ref}, nil
}
return up.Value, nil
}
case "if":
if schema.If != nil {
if schema.If.Ref != "" {
return &Ref{Ref: schema.If.Ref}, nil
}
return schema.If.Value, nil
}
case "then":
if schema.Then != nil {
if schema.Then.Ref != "" {
return &Ref{Ref: schema.Then.Ref}, nil
}
return schema.Then.Value, nil
}
case "else":
if schema.Else != nil {
if schema.Else.Ref != "" {
return &Ref{Ref: schema.Else.Ref}, nil
}
return schema.Else.Value, nil
}
case "dependentRequired":
return schema.DependentRequired, nil
case "$defs":
return schema.Defs, nil
case "$schema":
return schema.SchemaDialect, nil
case "$comment":
return schema.Comment, nil
case "$id":
return schema.SchemaID, nil
case "$anchor":
return schema.Anchor, nil
case "$dynamicRef":
return schema.DynamicRef, nil
case "$dynamicAnchor":
return schema.DynamicAnchor, nil
case "contentMediaType":
return schema.ContentMediaType, nil
case "contentEncoding":
return schema.ContentEncoding, nil
case "contentSchema":
if schema.ContentSchema != nil {
if schema.ContentSchema.Ref != "" {
return &Ref{Ref: schema.ContentSchema.Ref}, nil
}
return schema.ContentSchema.Value, nil
}
}
v, _, err := jsonpointer.GetForToken(schema.Extensions, token)
return v, err
}
func (schema *Schema) NewRef() *SchemaRef {
return &SchemaRef{
Value: schema,
}
}
func NewOneOfSchema(schemas ...*Schema) *Schema {
refs := make([]*SchemaRef, 0, len(schemas))
for _, schema := range schemas {
refs = append(refs, &SchemaRef{Value: schema})
}
return &Schema{
OneOf: refs,
}
}
func NewAnyOfSchema(schemas ...*Schema) *Schema {
refs := make([]*SchemaRef, 0, len(schemas))
for _, schema := range schemas {
refs = append(refs, &SchemaRef{Value: schema})
}
return &Schema{
AnyOf: refs,
}
}
func NewAllOfSchema(schemas ...*Schema) *Schema {
refs := make([]*SchemaRef, 0, len(schemas))
for _, schema := range schemas {
refs = append(refs, &SchemaRef{Value: schema})
}
return &Schema{
AllOf: refs,
}
}
func NewBoolSchema() *Schema {
return &Schema{
Type: &Types{TypeBoolean},
}
}
func NewFloat64Schema() *Schema {
return &Schema{
Type: &Types{TypeNumber},
}
}
func NewIntegerSchema() *Schema {
return &Schema{
Type: &Types{TypeInteger},
}
}
func NewInt32Schema() *Schema {
return &Schema{
Type: &Types{TypeInteger},
Format: "int32",
}
}
func NewInt64Schema() *Schema {
return &Schema{
Type: &Types{TypeInteger},
Format: "int64",
}
}
func NewStringSchema() *Schema {
return &Schema{
Type: &Types{TypeString},
}
}
func NewDateTimeSchema() *Schema {
return &Schema{
Type: &Types{TypeString},
Format: "date-time",
}
}
func NewUUIDSchema() *Schema {
return &Schema{
Type: &Types{TypeString},
Format: "uuid",
}
}
func NewBytesSchema() *Schema {
return &Schema{
Type: &Types{TypeString},
Format: "byte",
}
}
func NewArraySchema() *Schema {
return &Schema{
Type: &Types{TypeArray},
}
}
func NewObjectSchema() *Schema {
return &Schema{
Type: &Types{TypeObject},
Properties: make(Schemas),
}
}
func (schema *Schema) WithNullable() *Schema {
schema.Nullable = true
return schema
}
func (schema *Schema) WithMin(value float64) *Schema {
schema.Min = &value
return schema
}
func (schema *Schema) WithMax(value float64) *Schema {
schema.Max = &value
return schema
}
// WithExclusiveMin sets exclusiveMinimum as a boolean (OpenAPI 3.0 style).
func (schema *Schema) WithExclusiveMin(value bool) *Schema {
schema.ExclusiveMin = ExclusiveBound{Bool: &value}
return schema
}
// WithExclusiveMax sets exclusiveMaximum as a boolean (OpenAPI 3.0 style).
func (schema *Schema) WithExclusiveMax(value bool) *Schema {
schema.ExclusiveMax = ExclusiveBound{Bool: &value}
return schema
}
// WithExclusiveMinValue sets exclusiveMinimum as a number (OpenAPI 3.1 style).
func (schema *Schema) WithExclusiveMinValue(value float64) *Schema {
schema.ExclusiveMin = ExclusiveBound{Value: &value}
return schema
}
// WithExclusiveMaxValue sets exclusiveMaximum as a number (OpenAPI 3.1 style).
func (schema *Schema) WithExclusiveMaxValue(value float64) *Schema {
schema.ExclusiveMax = ExclusiveBound{Value: &value}
return schema
}
func (schema *Schema) WithEnum(values ...any) *Schema {
schema.Enum = values
return schema
}
func (schema *Schema) WithDefault(defaultValue any) *Schema {
schema.Default = defaultValue
return schema
}
func (schema *Schema) WithFormat(value string) *Schema {
schema.Format = value
return schema
}
func (schema *Schema) WithLength(i int64) *Schema {
n := uint64(i)
schema.MinLength = n
schema.MaxLength = &n
return schema
}
func (schema *Schema) WithMinLength(i int64) *Schema {
n := uint64(i)
schema.MinLength = n
return schema
}
func (schema *Schema) WithMaxLength(i int64) *Schema {
n := uint64(i)
schema.MaxLength = &n
return schema
}
func (schema *Schema) WithLengthDecodedBase64(i int64) *Schema {
n := uint64(i)
v := (n*8 + 5) / 6
schema.MinLength = v
schema.MaxLength = &v
return schema
}
func (schema *Schema) WithMinLengthDecodedBase64(i int64) *Schema {
n := uint64(i)
schema.MinLength = (n*8 + 5) / 6
return schema
}
func (schema *Schema) WithMaxLengthDecodedBase64(i int64) *Schema {
n := uint64(i)
schema.MinLength = (n*8 + 5) / 6
return schema
}
func (schema *Schema) WithPattern(pattern string) *Schema {
schema.Pattern = pattern
return schema
}
func (schema *Schema) WithItems(value *Schema) *Schema {
schema.Items = &SchemaRef{
Value: value,
}
return schema
}
func (schema *Schema) WithMinItems(i int64) *Schema {
n := uint64(i)
schema.MinItems = n
return schema
}
func (schema *Schema) WithMaxItems(i int64) *Schema {
n := uint64(i)
schema.MaxItems = &n
return schema
}
func (schema *Schema) WithUniqueItems(unique bool) *Schema {
schema.UniqueItems = unique
return schema
}
func (schema *Schema) WithProperty(name string, propertySchema *Schema) *Schema {
return schema.WithPropertyRef(name, &SchemaRef{
Value: propertySchema,
})
}
func (schema *Schema) WithPropertyRef(name string, ref *SchemaRef) *Schema {
if schema.Properties == nil {
schema.Properties = make(Schemas)
}
schema.Properties[name] = ref
return schema
}
func (schema *Schema) WithProperties(properties map[string]*Schema) *Schema {
result := make(Schemas, len(properties))
for k, v := range properties {
result[k] = &SchemaRef{
Value: v,
}
}
schema.Properties = result
return schema
}
func (schema *Schema) WithRequired(required []string) *Schema {
schema.Required = required
return schema
}
func (schema *Schema) WithMinProperties(i int64) *Schema {
n := uint64(i)
schema.MinProps = n
return schema
}
func (schema *Schema) WithMaxProperties(i int64) *Schema {
n := uint64(i)
schema.MaxProps = &n
return schema
}
func (schema *Schema) WithAnyAdditionalProperties() *Schema {
schema.AdditionalProperties = AdditionalProperties{Has: Ptr(true)}
return schema
}
func (schema *Schema) WithoutAdditionalProperties() *Schema {
schema.AdditionalProperties = AdditionalProperties{Has: Ptr(false)}
return schema
}
func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema {
schema.AdditionalProperties = AdditionalProperties{}
if v != nil {
schema.AdditionalProperties.Schema = &SchemaRef{Value: v}
}
return schema
}
func (schema *Schema) PermitsNull() bool {
return schema.Nullable || schema.Type.IncludesNull()
}
// IsEmpty tells whether schema is equivalent to the empty schema `{}`.
func (schema *Schema) IsEmpty() bool {
if schema.Type != nil || schema.Format != "" || len(schema.Enum) != 0 ||
schema.UniqueItems || schema.ExclusiveMin.IsSet() || schema.ExclusiveMax.IsSet() ||
schema.Nullable || schema.ReadOnly || schema.WriteOnly || schema.AllowEmptyValue ||
schema.Min != nil || schema.Max != nil || schema.MultipleOf != nil ||
schema.MinLength != 0 || schema.MaxLength != nil || schema.Pattern != "" ||
schema.MinItems != 0 || schema.MaxItems != nil ||
len(schema.Required) != 0 ||
schema.MinProps != 0 || schema.MaxProps != nil ||
schema.Const != nil {
return false
}
if n := schema.Not; n != nil && n.Value != nil && !n.Value.IsEmpty() {
return false
}
if ap := schema.AdditionalProperties.Schema; ap != nil && ap.Value != nil && !ap.Value.IsEmpty() {
return false
}
if apa := schema.AdditionalProperties.Has; apa != nil && !*apa {
return false
}
if items := schema.Items; items != nil && items.Value != nil && !items.Value.IsEmpty() {
return false
}
for _, s := range schema.PrefixItems {
if ss := s.Value; ss != nil && !ss.IsEmpty() {
return false
}
}
if c := schema.Contains; c != nil && c.Value != nil && !c.Value.IsEmpty() {
return false
}
if schema.MinContains != nil || schema.MaxContains != nil {
return false
}
for _, s := range schema.Properties {
if ss := s.Value; ss != nil && !ss.IsEmpty() {
return false
}
}
for _, s := range schema.PatternProperties {
if ss := s.Value; ss != nil && !ss.IsEmpty() {
return false
}
}
for _, s := range schema.DependentSchemas {
if ss := s.Value; ss != nil && !ss.IsEmpty() {
return false
}
}
if pn := schema.PropertyNames; pn != nil && pn.Value != nil && !pn.Value.IsEmpty() {
return false
}
if ui := schema.UnevaluatedItems.Schema; ui != nil && ui.Value != nil && !ui.Value.IsEmpty() {
return false
}
if uih := schema.UnevaluatedItems.Has; uih != nil && !*uih {
return false
}
if up := schema.UnevaluatedProperties.Schema; up != nil && up.Value != nil && !up.Value.IsEmpty() {
return false
}
if uph := schema.UnevaluatedProperties.Has; uph != nil && !*uph {
return false
}
if len(schema.Examples) != 0 {
return false
}
for _, s := range schema.OneOf {
if ss := s.Value; ss != nil && !ss.IsEmpty() {
return false
}
}
for _, s := range schema.AnyOf {
if ss := s.Value; ss != nil && !ss.IsEmpty() {
return false
}
}
for _, s := range schema.AllOf {
if ss := s.Value; ss != nil && !ss.IsEmpty() {
return false
}
}
if f := schema.If; f != nil && f.Value != nil && !f.Value.IsEmpty() {
return false
}
if t := schema.Then; t != nil && t.Value != nil && !t.Value.IsEmpty() {
return false
}
if e := schema.Else; e != nil && e.Value != nil && !e.Value.IsEmpty() {
return false
}
if len(schema.DependentRequired) != 0 {
return false
}
if len(schema.Defs) != 0 {
return false
}
if schema.SchemaDialect != "" || schema.Comment != "" {
return false
}
if schema.SchemaID != "" || schema.Anchor != "" || schema.DynamicRef != "" || schema.DynamicAnchor != "" {
return false
}
if schema.ContentMediaType != "" || schema.ContentEncoding != "" {
return false
}
if cs := schema.ContentSchema; cs != nil && cs.Value != nil && !cs.Value.IsEmpty() {
return false
}
return true
}
// Validate returns an error if Schema does not comply with the OpenAPI spec.
func (schema *Schema) Validate(ctx context.Context, opts ...ValidationOption) error {
// Apply document-level validation options to the context
ctx = WithValidationOptions(ctx, opts...)
// Perform schema validation with the options in context
_, err := schema.validate(ctx, []*Schema{})
return err
}
// returns the updated stack and an error if Schema does not comply with the OpenAPI spec.
func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema, error) {
if slices.Contains(stack, schema) {
return stack, nil
}
validationOpts := getValidationOptions(ctx)
stack = append(stack, schema)
if schema.ReadOnly && schema.WriteOnly {
return stack, newSchemaReadOnlyWriteOnlyExclusive(schema.Origin)
}
// Reject fields that only exist in OAS 3.1 / JSON Schema 2020-12 when the
// document is OAS 3.0. Fields explicitly allowed via AllowExtraSiblingFields
// are skipped (opt-in escape hatch for 3.0 docs that reference external
// JSON Schema documents). Once 3.0 moves to its own schema validator this
// block becomes a no-op.
if !validationOpts.isOpenAPI31OrLater {
allowed := validationOpts.extraSiblingFieldsAllowed
reject := func(field string) error {
if _, ok := allowed[field]; ok {
return nil
}
return errFieldFor31Plus(field, schema.Origin)
}
if schema.Const != nil {
if err := reject("const"); err != nil {
return stack, err
}
}
if len(schema.Examples) != 0 {
if err := reject("examples"); err != nil {
return stack, err
}
}
if len(schema.PrefixItems) != 0 {
if err := reject("prefixItems"); err != nil {
return stack, err
}
}
if schema.Contains != nil {
if err := reject("contains"); err != nil {
return stack, err
}
}
if schema.MinContains != nil {
if err := reject("minContains"); err != nil {
return stack, err
}
}
if schema.MaxContains != nil {
if err := reject("maxContains"); err != nil {
return stack, err
}
}
if len(schema.PatternProperties) != 0 {
if err := reject("patternProperties"); err != nil {
return stack, err
}
}
if len(schema.DependentSchemas) != 0 {
if err := reject("dependentSchemas"); err != nil {
return stack, err
}
}
if schema.PropertyNames != nil {
if err := reject("propertyNames"); err != nil {
return stack, err
}
}
if schema.UnevaluatedItems.Has != nil || schema.UnevaluatedItems.Schema != nil {
if err := reject("unevaluatedItems"); err != nil {
return stack, err
}
}
if schema.UnevaluatedProperties.Has != nil || schema.UnevaluatedProperties.Schema != nil {
if err := reject("unevaluatedProperties"); err != nil {
return stack, err
}
}
if schema.If != nil {
if err := reject("if"); err != nil {
return stack, err
}
}
if schema.Then != nil {
if err := reject("then"); err != nil {
return stack, err
}
}
if schema.Else != nil {
if err := reject("else"); err != nil {
return stack, err
}
}
if len(schema.DependentRequired) != 0 {
if err := reject("dependentRequired"); err != nil {
return stack, err
}
}
if len(schema.Defs) != 0 {
if err := reject("$defs"); err != nil {
return stack, err
}
}
if schema.SchemaDialect != "" {
if err := reject("$schema"); err != nil {
return stack, err
}
}
if schema.Comment != "" {
if err := reject("$comment"); err != nil {
return stack, err
}
}
if schema.SchemaID != "" {
if err := reject("$id"); err != nil {
return stack, err
}
}
if schema.Anchor != "" {
if err := reject("$anchor"); err != nil {
return stack, err
}
}
if schema.DynamicRef != "" {
if err := reject("$dynamicRef"); err != nil {
return stack, err
}
}
if schema.DynamicAnchor != "" {
if err := reject("$dynamicAnchor"); err != nil {
return stack, err
}
}
if schema.ContentMediaType != "" {
if err := reject("contentMediaType"); err != nil {
return stack, err
}
}
if schema.ContentEncoding != "" {
if err := reject("contentEncoding"); err != nil {
return stack, err
}
}
if schema.ContentSchema != nil {
if err := reject("contentSchema"); err != nil {
return stack, err
}
}
}
for _, item := range schema.OneOf {
v := item.Value
if v == nil {
return stack, newUnresolvedRef(item.Ref, item.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
for _, item := range schema.AnyOf {
v := item.Value
if v == nil {
return stack, newUnresolvedRef(item.Ref, item.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
for _, item := range schema.AllOf {
v := item.Value
if v == nil {
return stack, newUnresolvedRef(item.Ref, item.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
if ref := schema.Not; ref != nil {
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
if ref := schema.If; ref != nil {
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
if ref := schema.Then; ref != nil {
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
if ref := schema.Else; ref != nil {
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
for _, schemaType := range schema.Type.Slice() {
switch schemaType {
case TypeBoolean:
case TypeNumber:
if format := schema.Format; len(format) > 0 {
switch format {
case "float", "double":
default:
if _, ok := SchemaNumberFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled {
return stack, unsupportedFormat(format)
}
}
}
case TypeInteger:
if format := schema.Format; len(format) > 0 {
switch format {
case "int32", "int64":
default:
if _, ok := SchemaIntegerFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled {
return stack, unsupportedFormat(format)
}
}
}
case TypeString:
if format := schema.Format; len(format) > 0 {
switch format {
// Supported by OpenAPIv3.0.3:
// https://spec.openapis.org/oas/v3.0.3
case "byte", "binary", "date", "date-time", "password":
// In JSON Draft-07 (not validated yet though):
// https://json-schema.org/draft-07/json-schema-release-notes.html#formats
case "iri", "iri-reference", "uri-template", "idn-email", "idn-hostname":
case "json-pointer", "relative-json-pointer", "regex", "time":
// In JSON Draft 2019-09 (not validated yet though):
// https://json-schema.org/draft/2019-09/release-notes.html#format-vocabulary
case "duration", "uuid":
// Defined in some other specification
case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference":
default:
if _, ok := SchemaStringFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled {
return stack, unsupportedFormat(format)
}
}
}
if !validationOpts.schemaPatternValidationDisabled && schema.Pattern != "" {
if _, err := schema.compilePattern(validationOpts.regexCompilerFunc); err != nil {
return stack, err
}
}
case TypeArray:
if schema.Items == nil && !validationOpts.jsonSchema2020ValidationEnabled && len(schema.PrefixItems) == 0 {
return stack, newSchemaItemsRequired(schema.Origin)
}
case TypeObject:
case TypeNull:
if !validationOpts.jsonSchema2020ValidationEnabled {
return stack, newSchemaTypeError(schemaType, schema.Origin)
}
default:
return stack, newSchemaTypeError(schemaType, schema.Origin)
}
}
if ref := schema.Items; ref != nil {
if err := ref.validateExtras(ctx); err != nil {
return stack, err
}
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
for _, name := range componentNames(schema.Properties) {
ref := schema.Properties[name]
if err := ref.validateExtras(ctx); err != nil {
return stack, err
}
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
if schema.AdditionalProperties.Has != nil && schema.AdditionalProperties.Schema != nil {
return stack, newSchemaAdditionalPropertiesBothForms(schema.Origin)
}
if ref := schema.AdditionalProperties.Schema; ref != nil {
if err := ref.validateExtras(ctx); err != nil {
return stack, err
}
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
// OpenAPI 3.1 / JSON Schema 2020-12 sub-schemas
for _, ref := range schema.PrefixItems {
if err := ref.validateExtras(ctx); err != nil {
return stack, err
}
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
if ref := schema.Contains; ref != nil {
if err := ref.validateExtras(ctx); err != nil {
return stack, err
}
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
for _, name := range componentNames(schema.PatternProperties) {
ref := schema.PatternProperties[name]
if err := ref.validateExtras(ctx); err != nil {
return stack, err
}
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
for _, name := range componentNames(schema.DependentSchemas) {
ref := schema.DependentSchemas[name]
if err := ref.validateExtras(ctx); err != nil {
return stack, err
}
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
for _, name := range componentNames(schema.Defs) {
ref := schema.Defs[name]
if err := ref.validateExtras(ctx); err != nil {
return stack, err
}
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
if ref := schema.PropertyNames; ref != nil {
if err := ref.validateExtras(ctx); err != nil {
return stack, err
}
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
if schema.UnevaluatedItems.Has != nil && schema.UnevaluatedItems.Schema != nil {
return stack, newSchemaUnevaluatedItemsBothForms(schema.Origin)
}
if ref := schema.UnevaluatedItems.Schema; ref != nil {
if err := ref.validateExtras(ctx); err != nil {
return stack, err
}
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
if schema.UnevaluatedProperties.Has != nil && schema.UnevaluatedProperties.Schema != nil {
return stack, newSchemaUnevaluatedPropertiesBothForms(schema.Origin)
}
if ref := schema.UnevaluatedProperties.Schema; ref != nil {
if err := ref.validateExtras(ctx); err != nil {
return stack, err
}
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
if ref := schema.ContentSchema; ref != nil {
if err := ref.validateExtras(ctx); err != nil {
return stack, err
}
v := ref.Value
if v == nil {
return stack, newUnresolvedRef(ref.Ref, ref.Origin)
}
var err error
if stack, err = v.validate(ctx, stack); err != nil {
return stack, err
}
}
if v := schema.ExternalDocs; v != nil {
if err := v.Validate(ctx); err != nil {
return stack, &SectionValidationError{Section: "external docs", Cause: err}
}
}
if v := schema.Default; v != nil && !validationOpts.schemaDefaultsValidationDisabled {
if err := validateExampleValue(ctx, v, schema); err != nil {
return stack, newSchemaValueError("default", err, schema.Origin)
}
}
if x := schema.Example; x != nil && !validationOpts.examplesValidationDisabled {
if err := validateExampleValue(ctx, x, schema); err != nil {
return stack, newSchemaValueError("example", err, schema.Origin)
}
}
return stack, validateExtensions(ctx, schema.Extensions, schema.Origin)
}
func (schema *Schema) IsMatching(value any) bool {
settings := newSchemaValidationSettings(FailFast())
return schema.visitJSON(settings, value) == nil
}
func (schema *Schema) IsMatchingJSONBoolean(value bool) bool {
settings := newSchemaValidationSettings(FailFast())
return schema.visitJSON(settings, value) == nil
}
func (schema *Schema) IsMatchingJSONNumber(value float64) bool {
settings := newSchemaValidationSettings(FailFast())
return schema.visitJSON(settings, value) == nil
}
func (schema *Schema) IsMatchingJSONString(value string) bool {
settings := newSchemaValidationSettings(FailFast())
return schema.visitJSON(settings, value) == nil
}
func (schema *Schema) IsMatchingJSONArray(value []any) bool {
settings := newSchemaValidationSettings(FailFast())
return schema.visitJSON(settings, value) == nil
}
func (schema *Schema) IsMatchingJSONObject(value map[string]any) bool {
settings := newSchemaValidationSettings(FailFast())
return schema.visitJSON(settings, value) == nil
}
// VisitJSON applies a Schema to the given data, considering opts.
// To validate data against an OpenAPIv3.1+ schema, be sure to pass the EnableJSONSchema2020() option.
func (schema *Schema) VisitJSON(value any, opts ...SchemaValidationOption) error {
settings := newSchemaValidationSettings(opts...)
if settings.useJSONSchema2020 {
return schema.useJSONSchema2020(settings, value)
}
return schema.visitJSON(settings, value)
}
func (schema *Schema) visitJSON(settings *schemaValidationSettings, value any) (err error) {
switch value := value.(type) {
case nil:
// Don't use VisitJSONNull, as we still want to reach 'visitXOFOperations', since
// those could allow for a nullable value even though this one doesn't
if schema.PermitsNull() {
return
}
case float64:
if math.IsNaN(value) {
return ErrSchemaInputNaN
}
if math.IsInf(value, 0) {
return ErrSchemaInputInf
}
}
if schema.IsEmpty() {
switch value.(type) {
case nil:
return schema.visitJSONNull(settings)
default:
return
}
}
if err = schema.visitNotOperation(settings, value); err != nil {
return
}
var run bool
if err, run = schema.visitXOFOperations(settings, value); err != nil || !run {
return
}
if err = schema.visitEnumOperation(settings, value); err != nil {
return
}
if err = schema.visitConstOperation(settings, value); err != nil {
return
}
switch value := value.(type) {
case nil:
return schema.visitJSONNull(settings)
case bool:
return schema.visitJSONBoolean(settings, value)
case json.Number:
valueFloat64, err := value.Float64()
if err != nil {
return &SchemaError{
Value: value,
Schema: schema,
SchemaField: "type",
Reason: "cannot convert json.Number to float64",
customizeMessageError: settings.customizeMessageError,
Origin: err,
}
}
return schema.visitJSONNumber(settings, valueFloat64)
case int:
return schema.visitJSONNumber(settings, float64(value))
case int32:
return schema.visitJSONNumber(settings, float64(value))
case int64:
return schema.visitJSONNumber(settings, float64(value))
case float64:
return schema.visitJSONNumber(settings, value)
case string:
return schema.visitJSONString(settings, value)
case []any:
return schema.visitJSONArray(settings, value)
case map[string]any:
return schema.visitJSONObject(settings, value)
case map[any]any: // for YAML cf. issue https://github.com/getkin/kin-openapi/issues/444
values := make(map[string]any, len(value))
for key, v := range value {
if k, ok := key.(string); ok {
values[k] = v
}
}
if len(value) == len(values) {
return schema.visitJSONObject(settings, values)
}
}
// Catch slice of non-empty interface type
if reflect.TypeOf(value).Kind() == reflect.Slice {
valueR := reflect.ValueOf(value)
newValue := make([]any, 0, valueR.Len())
for i := 0; i < valueR.Len(); i++ {
newValue = append(newValue, valueR.Index(i).Interface())
}
return schema.visitJSONArray(settings, newValue)
}
return &SchemaError{
Value: value,
Schema: schema,
SchemaField: "type",
Reason: fmt.Sprintf("unhandled value of type %T", value),
customizeMessageError: settings.customizeMessageError,
}
}
func (schema *Schema) visitEnumOperation(settings *schemaValidationSettings, value any) (err error) {
if enum := schema.Enum; len(enum) != 0 {
for _, v := range enum {
switch c := value.(type) {
case json.Number:
var f float64
if f, err = strconv.ParseFloat(c.String(), 64); err != nil {
return err
}
if v == f {
return
}
case int64:
if v == float64(c) {
return
}
default:
if reflect.DeepEqual(v, value) {
return
}
}
}
if settings.failfast {
return errSchema
}
allowedValues, _ := json.Marshal(enum)
return &SchemaError{
Value: value,
Schema: schema,
SchemaField: "enum",
Reason: "value is not one of the allowed values " + string(allowedValues),
customizeMessageError: settings.customizeMessageError,
}
}
return
}
func (schema *Schema) visitConstOperation(settings *schemaValidationSettings, value any) (err error) {
if schema.Const == nil {
return
}
var match bool
switch c := value.(type) {
case json.Number:
var f float64
if f, err = strconv.ParseFloat(c.String(), 64); err != nil {
return err
}
match = reflect.DeepEqual(schema.Const, f)
case int64:
match = reflect.DeepEqual(schema.Const, float64(c))
default:
match = reflect.DeepEqual(schema.Const, value)
}
if !match {
if settings.failfast {
return errSchema
}
constVal, _ := json.Marshal(schema.Const)
return &SchemaError{
Value: value,
Schema: schema,
SchemaField: "const",
Reason: "value must be " + string(constVal),
customizeMessageError: settings.customizeMessageError,
}
}
return
}
func (schema *Schema) visitNotOperation(settings *schemaValidationSettings, value any) (err error) {
if ref := schema.Not; ref != nil {
v := ref.Value
if v == nil {
return newUnresolvedRef(ref.Ref, ref.Origin)
}
if err := v.visitJSON(settings, value); err == nil {
if settings.failfast {
return errSchema
}
return &SchemaError{
Value: value,
Schema: schema,
SchemaField: "not",
customizeMessageError: settings.customizeMessageError,
}
}
}
return
}
// If the XOF operations pass successfully, abort further run of validation, as they will already be satisfied (unless the schema
// itself is badly specified
// resolveDiscriminatorRef resolves the discriminator reference for oneOf/anyOf validation.
// Returns the discriminator ref string and any error encountered during resolution.
func (schema *Schema) resolveDiscriminatorRef(value any) (string, error) {
if schema.Discriminator == nil {
return "", nil
}
pn := schema.Discriminator.PropertyName
valuemap, okcheck := value.(map[string]any)
if !okcheck {
return "", nil
}
discriminatorVal, okcheck := valuemap[pn]
if !okcheck {
return "", &SchemaError{
Schema: schema,
SchemaField: "discriminator",
Reason: fmt.Sprintf("input does not contain the discriminator property %q", pn),
}
}
discriminatorValString, okcheck := discriminatorVal.(string)
if !okcheck {
return "", &SchemaError{
Value: discriminatorVal,
Schema: schema,
SchemaField: "discriminator",
Reason: fmt.Sprintf("value of discriminator property %q is not a string", pn),
}
}
if discriminatorRef, okcheck := schema.Discriminator.Mapping[discriminatorValString]; len(schema.Discriminator.Mapping) > 0 && !okcheck {
return "", &SchemaError{
Value: discriminatorVal,
Schema: schema,
SchemaField: "discriminator",
Reason: fmt.Sprintf("discriminator property %q has invalid value", pn),
}
} else {
return discriminatorRef.Ref, nil
}
}
func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, value any) (err error, run bool) {
var visitedOneOf, visitedAnyOf, visitedAllOf bool
if v := schema.OneOf; len(v) > 0 {
discriminatorRef, err := schema.resolveDiscriminatorRef(value)
if err != nil {
return err, false
}
var (
ok = 0
validationErrors = multiErrorForOneOf{}
matchedOneOfIndices = make([]int, 0)
tempValue = value
)
for idx, item := range v {
v := item.Value
if v == nil {
return newUnresolvedRef(item.Ref, item.Origin), false
}
if discriminatorRef != "" && discriminatorRef != item.Ref {
continue
}
// make a deep copy to protect origin value from being injected default value that defined in mismatched oneOf schema
if settings.asreq || settings.asrep {
tempValue = deepCopyJSONValue(value)
}
if err := v.visitJSON(settings, tempValue); err != nil {
validationErrors = append(validationErrors, err)
continue
}
matchedOneOfIndices = append(matchedOneOfIndices, idx)
ok++
}
if ok != 1 {
if settings.failfast {
return errSchema, false
}
e := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "oneOf",
customizeMessageError: settings.customizeMessageError,
}
if ok > 1 {
e.Origin = ErrOneOfConflict
e.Reason = fmt.Sprintf(`value matches more than one schema from "oneOf" (matches schemas at indices %v)`, matchedOneOfIndices)
} else {
e.Origin = fmt.Errorf("doesn't match schema due to: %w", validationErrors)
e.Reason = `value doesn't match any schema from "oneOf"`
}
return e, false
}
// run again to inject default value that defined in matched oneOf schema
if settings.asreq || settings.asrep {
_ = v[matchedOneOfIndices[0]].Value.visitJSON(settings, value)
}
visitedOneOf = true
}
if v := schema.AnyOf; len(v) > 0 {
discriminatorRef, err := schema.resolveDiscriminatorRef(value)
if err != nil {
return err, false
}
var (
ok = false
matchedAnyOfIdx = 0
tempValue = value
)
for idx, item := range v {
v := item.Value
if v == nil {
return newUnresolvedRef(item.Ref, item.Origin), false
}
if discriminatorRef != "" && discriminatorRef != item.Ref {
continue
}
// make a deep copy to protect origin value from being injected default value that defined in mismatched anyOf schema
if settings.asreq || settings.asrep {
tempValue = deepCopyJSONValue(value)
}
if err := v.visitJSON(settings, tempValue); err == nil {
ok = true
matchedAnyOfIdx = idx
break
}
}
if !ok {
if settings.failfast {
return errSchema, false
}
return &SchemaError{
Value: value,
Schema: schema,
SchemaField: "anyOf",
Reason: `doesn't match any schema from "anyOf"`,
customizeMessageError: settings.customizeMessageError,
}, false
}
_ = v[matchedAnyOfIdx].Value.visitJSON(settings, value)
visitedAnyOf = true
}
validationErrors := multiErrorForAllOf{}
for _, item := range schema.AllOf {
v := item.Value
if v == nil {
return newUnresolvedRef(item.Ref, item.Origin), false
}
if err := v.visitJSON(settings, value); err != nil {
if settings.failfast {
return errSchema, false
}
validationErrors = append(validationErrors, err)
}
visitedAllOf = true
}
if len(validationErrors) > 0 {
return &SchemaError{
Value: value,
Schema: schema,
SchemaField: "allOf",
Reason: `doesn't match all schemas from "allOf"`,
Origin: fmt.Errorf("doesn't match schema due to: %w", validationErrors),
customizeMessageError: settings.customizeMessageError,
}, false
}
run = !((visitedOneOf || visitedAnyOf || visitedAllOf) && value == nil)
return
}
// The value is not considered in visitJSONNull because according to the spec
// "null is not supported as a type" unless `nullable` is also set to true
// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#data-types
// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object
func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err error) {
if schema.PermitsNull() {
return
}
if settings.failfast {
return errSchema
}
return &SchemaError{
Value: nil,
Schema: schema,
SchemaField: "nullable",
Reason: "Value is not nullable",
customizeMessageError: settings.customizeMessageError,
}
}
func (schema *Schema) VisitJSONBoolean(value bool) error {
settings := newSchemaValidationSettings()
return schema.visitJSONBoolean(settings, value)
}
func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) {
if !schema.Type.Permits(TypeBoolean) {
return schema.expectedType(settings, value)
}
return
}
func (schema *Schema) VisitJSONNumber(value float64) error {
settings := newSchemaValidationSettings()
return schema.visitJSONNumber(settings, value)
}
func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) error {
var me MultiError
schemaType := schema.Type
requireInteger := false
if schemaType.Permits(TypeInteger) && !schemaType.Permits(TypeNumber) {
requireInteger = true
if bigFloat := big.NewFloat(value); !bigFloat.IsInt() {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "type",
Reason: "value must be an integer",
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
} else if !(schemaType.Permits(TypeInteger) || schemaType.Permits(TypeNumber)) {
return schema.expectedType(settings, value)
}
// formats
var formatStrErr string
var formatErr error
format := schema.Format
if format != "" {
if requireInteger {
// Check per-validation validators first, then fall back to global
f, ok := settings.integerFormats[format]
if !ok {
f, ok = SchemaIntegerFormats[format]
}
if ok {
if err := f.Validate(int64(value)); err != nil {
var reason string
schemaErr := &SchemaError{}
if errors.As(err, &schemaErr) {
reason = schemaErr.Reason
} else {
reason = err.Error()
}
formatStrErr = fmt.Sprintf(`integer doesn't match the format %q (%v)`, format, reason)
formatErr = fmt.Errorf("integer doesn't match the format %q: %w", format, err)
}
}
} else {
// Check per-validation validators first, then fall back to global
f, ok := settings.numberFormats[format]
if !ok {
f, ok = SchemaNumberFormats[format]
}
if ok {
if err := f.Validate(value); err != nil {
var reason string
schemaErr := &SchemaError{}
if errors.As(err, &schemaErr) {
reason = schemaErr.Reason
} else {
reason = err.Error()
}
formatStrErr = fmt.Sprintf(`number doesn't match the format %q (%v)`, format, reason)
formatErr = fmt.Errorf("number doesn't match the format %q: %w", format, err)
}
}
}
}
if formatStrErr != "" || formatErr != nil {
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "format",
Reason: formatStrErr,
Origin: formatErr,
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
// "exclusiveMinimum"
// OpenAPI 3.0: boolean modifier for minimum
// OpenAPI 3.1: number representing the actual exclusive bound
if eb := schema.ExclusiveMin; eb.IsSet() {
var exclusiveMinBound float64
var valid bool
if eb.Value != nil {
// OpenAPI 3.1 style: exclusiveMinimum is the bound itself
exclusiveMinBound = *eb.Value
valid = value > exclusiveMinBound
} else if eb.Bool != nil && *eb.Bool && schema.Min != nil {
// OpenAPI 3.0 style: exclusiveMinimum modifies minimum
exclusiveMinBound = *schema.Min
valid = value > exclusiveMinBound
} else {
valid = true
}
if !valid {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "exclusiveMinimum",
Reason: fmt.Sprintf("number must be more than %g", exclusiveMinBound),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
}
// "exclusiveMaximum"
// OpenAPI 3.0: boolean modifier for maximum
// OpenAPI 3.1: number representing the actual exclusive bound
if eb := schema.ExclusiveMax; eb.IsSet() {
var exclusiveMaxBound float64
var valid bool
if eb.Value != nil {
// OpenAPI 3.1 style: exclusiveMaximum is the bound itself
exclusiveMaxBound = *eb.Value
valid = value < exclusiveMaxBound
} else if eb.Bool != nil && *eb.Bool && schema.Max != nil {
// OpenAPI 3.0 style: exclusiveMaximum modifies maximum
exclusiveMaxBound = *schema.Max
valid = value < exclusiveMaxBound
} else {
valid = true
}
if !valid {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "exclusiveMaximum",
Reason: fmt.Sprintf("number must be less than %g", exclusiveMaxBound),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
}
// "minimum"
if v := schema.Min; v != nil && !(*v <= value) {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "minimum",
Reason: fmt.Sprintf("number must be at least %g", *v),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
// "maximum"
if v := schema.Max; v != nil && !(*v >= value) {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "maximum",
Reason: fmt.Sprintf("number must be at most %g", *v),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
// "multipleOf"
if v := schema.MultipleOf; v != nil {
// "A numeric instance is valid only if division by this keyword's
// value results in an integer."
numRat, denRat := &big.Rat{}, &big.Rat{}
numRat.SetString(fmt.Sprintf("%.10f", value))
denRat.SetString(fmt.Sprintf("%.10f", *v))
if !(&big.Rat{}).Quo(numRat, denRat).IsInt() {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "multipleOf",
Reason: fmt.Sprintf("number must be a multiple of %g", *v),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
}
if len(me) > 0 {
return me
}
return nil
}
func (schema *Schema) VisitJSONString(value string) error {
settings := newSchemaValidationSettings()
return schema.visitJSONString(settings, value)
}
func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error {
if !schema.Type.Permits(TypeString) {
return schema.expectedType(settings, value)
}
var me MultiError
// "minLength" and "maxLength"
minLength := schema.MinLength
maxLength := schema.MaxLength
if minLength != 0 || maxLength != nil {
// JSON schema string lengths are UTF-16, not UTF-8!
length := int64(0)
for _, r := range value {
if utf16.IsSurrogate(r) {
length += 2
} else {
length++
}
}
if minLength != 0 && length < int64(minLength) {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "minLength",
Reason: fmt.Sprintf("minimum string length is %d", minLength),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
if maxLength != nil && length > int64(*maxLength) {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "maxLength",
Reason: fmt.Sprintf("maximum string length is %d", *maxLength),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
}
// "pattern"
if !settings.patternValidationDisabled && schema.Pattern != "" {
cpiface, _ := compiledPatterns.Load(schema.Pattern)
cp, _ := cpiface.(RegexMatcher)
if cp == nil {
var err error
if cp, err = schema.compilePattern(settings.regexCompiler); err != nil {
if !settings.multiError {
return err
}
me = append(me, err)
}
}
if !cp.MatchString(value) {
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "pattern",
Reason: fmt.Sprintf(`string doesn't match the regular expression "%s"`, schema.Pattern),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
}
// "format"
var formatStrErr string
var formatErr error
if format := schema.Format; format != "" {
// Check per-validation validators first, then fall back to global
f, ok := settings.stringFormats[format]
if !ok {
f, ok = SchemaStringFormats[format]
}
if ok {
if err := f.Validate(value); err != nil {
var reason string
schemaErr := &SchemaError{}
if errors.As(err, &schemaErr) {
reason = schemaErr.Reason
} else {
reason = err.Error()
}
formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%v)`, format, reason)
formatErr = fmt.Errorf("string doesn't match the format %q: %w", format, err)
}
}
}
if formatStrErr != "" || formatErr != nil {
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "format",
Reason: formatStrErr,
Origin: formatErr,
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
if len(me) > 0 {
return me
}
return nil
}
func (schema *Schema) VisitJSONArray(value []any) error {
settings := newSchemaValidationSettings()
return schema.visitJSONArray(settings, value)
}
func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []any) error {
if !schema.Type.Permits(TypeArray) {
return schema.expectedType(settings, value)
}
var me MultiError
lenValue := int64(len(value))
// "minItems"
if v := schema.MinItems; v != 0 && lenValue < int64(v) {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "minItems",
Reason: fmt.Sprintf("minimum number of items is %d", v),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
// "maxItems"
if v := schema.MaxItems; v != nil && lenValue > int64(*v) {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "maxItems",
Reason: fmt.Sprintf("maximum number of items is %d", *v),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
// "uniqueItems"
if sliceUniqueItemsChecker == nil {
sliceUniqueItemsChecker = isSliceOfUniqueItems
}
if v := schema.UniqueItems; v && !sliceUniqueItemsChecker(value) {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "uniqueItems",
Reason: "duplicate items found",
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
// "items"
if itemSchemaRef := schema.Items; itemSchemaRef != nil {
itemSchema := itemSchemaRef.Value
if itemSchema == nil {
return newUnresolvedRef(itemSchemaRef.Ref, itemSchemaRef.Origin)
}
for i, item := range value {
if err := itemSchema.visitJSON(settings, item); err != nil {
err = markSchemaErrorIndex(err, i)
if !settings.multiError {
return err
}
if itemMe, ok := err.(MultiError); ok {
me = append(me, itemMe...)
} else {
me = append(me, err)
}
}
}
}
if len(me) > 0 {
return me
}
return nil
}
func (schema *Schema) VisitJSONObject(value map[string]any) error {
settings := newSchemaValidationSettings()
return schema.visitJSONObject(settings, value)
}
func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]any) error {
if !schema.Type.Permits(TypeObject) {
return schema.expectedType(settings, value)
}
var me MultiError
if settings.asreq || settings.asrep {
for _, propName := range componentNames(schema.Properties) {
propSchema := schema.Properties[propName]
reqRO := settings.asreq && propSchema.Value.ReadOnly && !settings.readOnlyValidationDisabled
repWO := settings.asrep && propSchema.Value.WriteOnly && !settings.writeOnlyValidationDisabled
if f := settings.defaultsSet; f != nil && value[propName] == nil {
if dflt := propSchema.Value.Default; dflt != nil && !reqRO && !repWO {
value[propName] = dflt
settings.onceSettingDefaults.Do(f)
}
}
if value[propName] != nil {
if reqRO {
me = append(me, fmt.Errorf("readOnly property %q in request", propName))
} else if repWO {
me = append(me, fmt.Errorf("writeOnly property %q in response", propName))
}
}
}
}
// "properties"
properties := schema.Properties
lenValue := int64(len(value))
// "minProperties"
if v := schema.MinProps; v != 0 && lenValue < int64(v) {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "minProperties",
Reason: fmt.Sprintf("there must be at least %d properties", v),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
// "maxProperties"
if v := schema.MaxProps; v != nil && lenValue > int64(*v) {
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "maxProperties",
Reason: fmt.Sprintf("there must be at most %d properties", *v),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
// "additionalProperties"
var additionalProperties *Schema
if ref := schema.AdditionalProperties.Schema; ref != nil {
additionalProperties = ref.Value
}
for _, k := range componentNames(value) {
v := value[k]
if properties != nil {
propertyRef := properties[k]
if propertyRef != nil {
p := propertyRef.Value
if p == nil {
return newUnresolvedRef(propertyRef.Ref, propertyRef.Origin)
}
if err := p.visitJSON(settings, v); err != nil {
if settings.failfast {
return errSchema
}
err = markSchemaErrorKey(err, k)
if !settings.multiError {
return err
}
if v, ok := err.(MultiError); ok {
me = append(me, v...)
continue
}
me = append(me, err)
}
continue
}
}
if allowed := schema.AdditionalProperties.Has; allowed == nil || *allowed {
if additionalProperties != nil {
if err := additionalProperties.visitJSON(settings, v); err != nil {
if settings.failfast {
return errSchema
}
err = markSchemaErrorKey(err, k)
if !settings.multiError {
return err
}
if v, ok := err.(MultiError); ok {
me = append(me, v...)
continue
}
me = append(me, err)
}
}
continue
}
if settings.failfast {
return errSchema
}
err := &SchemaError{
Value: value,
Schema: schema,
SchemaField: "properties",
Reason: fmt.Sprintf("property %q is unsupported", k),
customizeMessageError: settings.customizeMessageError,
}
if !settings.multiError {
return err
}
me = append(me, err)
}
// "required"
for _, k := range schema.Required {
if _, ok := value[k]; !ok {
if s := schema.Properties[k]; s != nil && s.Value.ReadOnly && settings.asreq {
continue
}
if s := schema.Properties[k]; s != nil && s.Value.WriteOnly && settings.asrep {
continue
}
if settings.failfast {
return errSchema
}
err := markSchemaErrorKey(&SchemaError{
Value: value,
Schema: schema,
SchemaField: "required",
Reason: fmt.Sprintf("property %q is missing", k),
customizeMessageError: settings.customizeMessageError,
}, k)
if !settings.multiError {
return err
}
me = append(me, err)
}
}
if len(me) > 0 {
return me
}
return nil
}
func (schema *Schema) expectedType(settings *schemaValidationSettings, value any) error {
if settings.failfast {
return errSchema
}
a := "a"
var x string
schemaTypes := (*schema.Type)
if len(schemaTypes) == 1 {
x = schemaTypes[0]
switch x {
case TypeArray, TypeObject, TypeInteger:
a = "an"
}
} else {
a = "one of"
x = strings.Join(schemaTypes, ", ")
}
return &SchemaError{
Value: value,
Schema: schema,
SchemaField: "type",
Reason: fmt.Sprintf("value must be %s %s", a, x),
customizeMessageError: settings.customizeMessageError,
}
}
// SchemaError is an error that occurs during schema validation.
type SchemaError struct {
// Value is the value that failed validation.
Value any
// reversePath is the path to the value that failed validation.
reversePath []string
// Schema is the schema that failed validation.
Schema *Schema
// SchemaField is the field of the schema that failed validation.
SchemaField string
// Reason is a human-readable message describing the error.
// The message should never include the original value to prevent leakage of potentially sensitive inputs in error messages.
Reason string
// Origin is the original error that caused this error.
Origin error
// customizeMessageError is a function that can be used to customize the error message.
customizeMessageError func(err *SchemaError) string
}
var _ interface{ Unwrap() error } = SchemaError{}
func markSchemaErrorKey(err error, key string) error {
if v, ok := err.(*SchemaError); ok {
v.reversePath = append(v.reversePath, key)
if v.Origin != nil {
if unwrapped := errors.Unwrap(v.Origin); unwrapped != nil {
if me, ok := unwrapped.(multiErrorForOneOf); ok {
_ = markSchemaErrorKey(MultiError(me), key)
}
if me, ok := unwrapped.(multiErrorForAllOf); ok {
_ = markSchemaErrorKey(MultiError(me), key)
}
}
}
return v
}
if v, ok := err.(MultiError); ok {
for _, e := range v {
_ = markSchemaErrorKey(e, key)
}
return v
}
return err
}
func markSchemaErrorIndex(err error, index int) error {
return markSchemaErrorKey(err, strconv.FormatInt(int64(index), 10))
}
func (err *SchemaError) JSONPointer() []string {
reversePath := err.reversePath
path := append([]string(nil), reversePath...)
for left, right := 0, len(path)-1; left < right; left, right = left+1, right-1 {
path[left], path[right] = path[right], path[left]
}
return path
}
func (err *SchemaError) Error() string {
if err.customizeMessageError != nil {
if msg := err.customizeMessageError(err); msg != "" {
return msg
}
}
buf := bytes.NewBuffer(make([]byte, 0, 256))
if len(err.reversePath) > 0 {
buf.WriteString(`Error at "`)
reversePath := err.reversePath
for _, r := range slices.Backward(reversePath) {
buf.WriteByte('/')
buf.WriteString(r)
}
buf.WriteString(`": `)
}
if err.Origin != nil {
buf.WriteString(err.Origin.Error())
return buf.String()
}
reason := err.Reason
if reason == "" {
buf.WriteString(`Doesn't match schema "`)
buf.WriteString(err.SchemaField)
buf.WriteString(`"`)
} else {
buf.WriteString(reason)
}
if !SchemaErrorDetailsDisabled {
buf.WriteString("\nSchema:\n ")
encoder := json.NewEncoder(buf)
encoder.SetIndent(" ", " ")
if err := encoder.Encode(err.Schema); err != nil {
panic(err)
}
buf.WriteString("\nValue:\n ")
if err := encoder.Encode(err.Value); err != nil {
panic(err)
}
}
return buf.String()
}
func (err SchemaError) Unwrap() error {
return err.Origin
}
func isSliceOfUniqueItems(xs []any) bool {
s := len(xs)
m := make(map[string]struct{}, s)
for _, x := range xs {
// The input slice is converted from a JSON string, there shall
// have no error when convert it back.
key, _ := json.Marshal(&x)
m[string(key)] = struct{}{}
}
return s == len(m)
}
// SliceUniqueItemsChecker is an function used to check if an given slice
// have unique items.
type SliceUniqueItemsChecker func(items []any) bool
// By default using predefined func isSliceOfUniqueItems which make use of
// json.Marshal to generate a key for map used to check if a given slice
// have unique items.
var sliceUniqueItemsChecker SliceUniqueItemsChecker = isSliceOfUniqueItems
// RegisterArrayUniqueItemsChecker is used to register a customized function
// used to check if JSON array have unique items.
func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) {
sliceUniqueItemsChecker = fn
}
func unsupportedFormat(format string) error {
return fmt.Errorf("unsupported 'format' value %q", format)
}
func deepCopyJSONValue(v any) any {
switch v := v.(type) {
case map[string]any:
cp := make(map[string]any, len(v))
for k, val := range v {
cp[k] = deepCopyJSONValue(val)
}
return cp
case []any:
cp := make([]any, len(v))
for i, val := range v {
cp[i] = deepCopyJSONValue(val)
}
return cp
default:
return v // string, float64, bool, nil — all immutable
}
}
// UnmarshalJSON sets Schemas to a copy of data.
func (schemas *Schemas) UnmarshalJSON(data []byte) (err error) {
*schemas, err = unmarshalStringMapP[SchemaRef](data)
return
}