diff --git a/jsonschema/json_pointer.go b/jsonschema/json_pointer.go index 4a9db2e..a358224 100644 --- a/jsonschema/json_pointer.go +++ b/jsonschema/json_pointer.go @@ -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) } diff --git a/jsonschema/schema.go b/jsonschema/schema.go index 243048a..f3dcaea 100644 --- a/jsonschema/schema.go +++ b/jsonschema/schema.go @@ -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"` @@ -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 } @@ -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). @@ -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 }{ @@ -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 @@ -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 { @@ -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. @@ -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"}) } } } diff --git a/jsonschema/validate.go b/jsonschema/validate.go index 03fe409..e374181 100644 --- a/jsonschema/validate.go +++ b/jsonschema/validate.go @@ -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) } } }