3183 lines
84 KiB
Go
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
|
|
}
|