Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions jsonschema/json_pointer.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,22 @@ func lookupSchemaField(v reflect.Value, name string) reflect.Value {
// for pointer dereference. So we use FieldByName to get the DependencySchemas map.
return v.FieldByName("DependencySchemas")
}
if name == "exclusiveMinimum" {
// The "exclusiveMinimum" keyword refers to the "union type" that is either a float64 or a boolean.
// Implemented using the ExclusiveMinimum representing the float64 and ExclusiveMinimumBoolean for the boolean.
if items := v.FieldByName("ExclusiveMinimum"); items.IsValid() && !items.IsNil() {
return items
}
return v.FieldByName("ExclusiveMinimumBoolean")
}
if name == "exclusiveMaximum" {
// The "exclusiveMaximum" keyword refers to the "union type" that is either a float64 or a boolean.
// Implemented using the ExclusiveMaximum representing the float64 and ExclusiveMaximumBoolean for the boolean.
if items := v.FieldByName("ExclusiveMaximum"); items.IsValid() && !items.IsNil() {
return items
}
return v.FieldByName("ExclusiveMaximumBoolean")
}
if sf, ok := schemaFieldMap[name]; ok {
return v.FieldByIndex(sf.Index)
}
Expand Down
114 changes: 88 additions & 26 deletions jsonschema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,17 @@ type Schema struct {
Types []string `json:"-"`
Enum []any `json:"enum,omitempty"`
// Const is *any because a JSON null (Go nil) is a valid value.
Const *any `json:"const,omitempty"`
MultipleOf *float64 `json:"multipleOf,omitempty"`
Minimum *float64 `json:"minimum,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
ExclusiveMinimum *float64 `json:"exclusiveMinimum,omitempty"`
ExclusiveMaximum *float64 `json:"exclusiveMaximum,omitempty"`
MinLength *int `json:"minLength,omitempty"`
MaxLength *int `json:"maxLength,omitempty"`
Pattern string `json:"pattern,omitempty"`
Const *any `json:"const,omitempty"`
MultipleOf *float64 `json:"multipleOf,omitempty"`
Minimum *float64 `json:"minimum,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
ExclusiveMinimum *float64 `json:"-"` // draft 6 and newer
ExclusiveMinimumBoolean *bool `json:"-"` // draft 5 and older
ExclusiveMaximum *float64 `json:"-"` // draft 6 and newer
ExclusiveMaximumBoolean *bool `json:"-"` // draft 5 and older
MinLength *int `json:"minLength,omitempty"`
MaxLength *int `json:"maxLength,omitempty"`
Pattern string `json:"pattern,omitempty"`

// arrays
PrefixItems []*Schema `json:"prefixItems,omitempty"`
Expand Down Expand Up @@ -228,6 +230,12 @@ func (s *Schema) basicChecks() error {
return fmt.Errorf("dependency key %q cannot be defined as both a schema and a string array", key)
}
}
if s.ExclusiveMinimum != nil && s.ExclusiveMinimumBoolean != nil {
return errors.New("both ExclusiveMinimum and ExclusiveMinimumBoolean are set; at most one should be")
}
if s.ExclusiveMaximum != nil && s.ExclusiveMaximumBoolean != nil {
return errors.New("both ExclusiveMaximum and ExclusiveMaximumBoolean are set; at most one should be")
}
return nil
}

Expand Down Expand Up @@ -273,16 +281,36 @@ func (s Schema) MarshalJSON() ([]byte, error) {
}
}

var exMin any
switch {
case s.ExclusiveMinimum != nil:
exMin = s.ExclusiveMinimum
case s.ExclusiveMinimumBoolean != nil:
exMin = s.ExclusiveMinimumBoolean
}

var exMax any
switch {
case s.ExclusiveMaximum != nil:
exMax = s.ExclusiveMaximum
case s.ExclusiveMaximumBoolean != nil:
exMax = s.ExclusiveMaximumBoolean
}

ms := struct {
Type any `json:"type,omitempty"`
Properties json.Marshaler `json:"properties,omitempty"`
Dependencies map[string]any `json:"dependencies,omitempty"`
Items any `json:"items,omitempty"`
Type any `json:"type,omitempty"`
Properties json.Marshaler `json:"properties,omitempty"`
Dependencies map[string]any `json:"dependencies,omitempty"`
Items any `json:"items,omitempty"`
ExclusiveMinimum any `json:"exclusiveMinimum,omitempty"`
ExclusiveMaximum any `json:"exclusiveMaximum,omitempty"`
*schemaWithoutMethods
}{
Type: typ,
Dependencies: dep,
Items: items,
ExclusiveMinimum: exMin,
ExclusiveMaximum: exMax,
schemaWithoutMethods: (*schemaWithoutMethods)(&s),
}
// Marshal properties, even if the empty map (but not nil).
Expand Down Expand Up @@ -392,18 +420,20 @@ func (s *Schema) UnmarshalJSON(data []byte) error {
}

ms := struct {
Type json.RawMessage `json:"type,omitempty"`
Dependencies map[string]json.RawMessage `json:"dependencies,omitempty"`
Items json.RawMessage `json:"items,omitempty"`
Const json.RawMessage `json:"const,omitempty"`
MinLength *integer `json:"minLength,omitempty"`
MaxLength *integer `json:"maxLength,omitempty"`
MinItems *integer `json:"minItems,omitempty"`
MaxItems *integer `json:"maxItems,omitempty"`
MinProperties *integer `json:"minProperties,omitempty"`
MaxProperties *integer `json:"maxProperties,omitempty"`
MinContains *integer `json:"minContains,omitempty"`
MaxContains *integer `json:"maxContains,omitempty"`
Type json.RawMessage `json:"type,omitempty"`
Dependencies map[string]json.RawMessage `json:"dependencies,omitempty"`
Items json.RawMessage `json:"items,omitempty"`
Const json.RawMessage `json:"const,omitempty"`
ExclusiveMinimum json.RawMessage `json:"exclusiveMinimum,omitempty"`
ExclusiveMaximum json.RawMessage `json:"exclusiveMaximum,omitempty"`
MinLength *integer `json:"minLength,omitempty"`
MaxLength *integer `json:"maxLength,omitempty"`
MinItems *integer `json:"minItems,omitempty"`
MaxItems *integer `json:"maxItems,omitempty"`
MinProperties *integer `json:"minProperties,omitempty"`
MaxProperties *integer `json:"maxProperties,omitempty"`
MinContains *integer `json:"minContains,omitempty"`
MaxContains *integer `json:"maxContains,omitempty"`

*schemaWithoutMethods
}{
Expand Down Expand Up @@ -471,6 +501,17 @@ func (s *Schema) UnmarshalJSON(data []byte) error {
}
}

// Unmarshal "exclusiveMinimum" as a float64 or boolean
s.ExclusiveMinimum, s.ExclusiveMinimumBoolean, err = unmarshalValueOrBoolean[float64](ms.ExclusiveMinimum)
if err != nil {
return err
}
// Unmarshal "exclusiveMaximum" as a float64 or boolean
s.ExclusiveMaximum, s.ExclusiveMaximumBoolean, err = unmarshalValueOrBoolean[float64](ms.ExclusiveMaximum)
if err != nil {
return err
}

unmarshalAnyPtr := func(p **any, raw json.RawMessage) error {
if len(raw) == 0 {
return nil
Expand Down Expand Up @@ -506,6 +547,23 @@ func (s *Schema) UnmarshalJSON(data []byte) error {
return nil
}

// unmarshalValueOrBoolean unmarshals JSON into a value of type T or a boolean.
func unmarshalValueOrBoolean[T any](data []byte) (*T, *bool, error) {
switch {
case len(data) == 0:
return nil, nil, nil
case bytes.Equal(data, []byte(`true`)):
value := true
return nil, &value, nil
case bytes.Equal(data, []byte(`false`)):
value := false
return nil, &value, nil
default:
var value T
return &value, nil, json.Unmarshal(data, &value)
}
}

type integer int32 // for the integer-valued fields of Schema

func (ip *integer) UnmarshalJSON(data []byte) error {
Expand Down Expand Up @@ -617,7 +675,7 @@ func init() {
if !info.omit {
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, info.name})
} else {
// jsoninfo.name is used to build the info paths. The items and dependencies are ommited,
// jsoninfo.name is used to build the info paths. The items and dependencies are omitted,
// since the original fields are separated to handle the union types supported in json and
// these fields have custom marshalling and unmarshalling logic.
// we still need these fields in schemaFieldInfos for creating schema trees and calculating paths and refs.
Expand All @@ -627,6 +685,10 @@ func init() {
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, "items"})
case "DependencySchemas", "DependencyStrings":
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, "dependencies"})
case "ExclusiveMinimum", "ExclusiveMinimumBoolean":
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, "exclusiveMinimum"})
case "ExclusiveMaximum", "ExclusiveMaximumBoolean":
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, "exclusiveMaximum"})
}
}
}
Expand Down
36 changes: 28 additions & 8 deletions jsonschema/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,20 +166,40 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
}
}

// Initialize optional values for min/max validation
var (
minValue = schema.Minimum
maxValue = schema.Maximum
exMinValue = schema.ExclusiveMinimum
exMaxValue = schema.ExclusiveMaximum
)

// Preserve behavior for draft 5 and older:
// Use minimum/maximum as exclusiveMinimum/exclusiveMaximum if exclusiveMinimum/exclusiveMaximum
// was set as a boolean.
if schema.ExclusiveMinimumBoolean != nil && *schema.ExclusiveMinimumBoolean {
minValue = nil // disable minimum validation
exMinValue = schema.Minimum // enable exclusive minimum validation
}
if schema.ExclusiveMaximumBoolean != nil && *schema.ExclusiveMaximumBoolean {
maxValue = nil // disable maximum validation
exMaxValue = schema.Maximum // enable exclusive maximum validation
}

m := new(big.Rat) // reuse for all of the following
cmp := func(f float64) int { return n.Cmp(m.SetFloat64(f)) }

if schema.Minimum != nil && cmp(*schema.Minimum) < 0 {
return fmt.Errorf("minimum: %s is less than %f", n, *schema.Minimum)
if minValue != nil && cmp(*minValue) < 0 {
return fmt.Errorf("minimum: %s is less than %f", n, *minValue)
}
if schema.Maximum != nil && cmp(*schema.Maximum) > 0 {
return fmt.Errorf("maximum: %s is greater than %f", n, *schema.Maximum)
if maxValue != nil && cmp(*maxValue) > 0 {
return fmt.Errorf("maximum: %s is greater than %f", n, *maxValue)
}
if schema.ExclusiveMinimum != nil && cmp(*schema.ExclusiveMinimum) <= 0 {
return fmt.Errorf("exclusiveMinimum: %s is less than or equal to %f", n, *schema.ExclusiveMinimum)
if exMinValue != nil && cmp(*exMinValue) <= 0 {
return fmt.Errorf("exclusiveMinimum: %s is less than or equal to %f", n, *exMinValue)
}
if schema.ExclusiveMaximum != nil && cmp(*schema.ExclusiveMaximum) >= 0 {
return fmt.Errorf("exclusiveMaximum: %s is greater than or equal to %f", n, *schema.ExclusiveMaximum)
if exMaxValue != nil && cmp(*exMaxValue) >= 0 {
return fmt.Errorf("exclusiveMaximum: %s is greater than or equal to %f", n, *exMaxValue)
}
}
}
Expand Down