2023-09-07 14:36:06 +00:00
|
|
|
package jsonschema
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
2023-09-08 12:07:22 +00:00
|
|
|
"slices"
|
2023-09-07 14:36:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Load a JSON document and validate it against the JSON schema. Instance here
|
|
|
|
// refers to a JSON document. see: https://json-schema.org/draft/2020-12/json-schema-core.html#name-instance
|
|
|
|
func (s *Schema) LoadInstance(path string) (map[string]any, error) {
|
|
|
|
instance := make(map[string]any)
|
|
|
|
b, err := os.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
err = json.Unmarshal(b, &instance)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// The default JSON unmarshaler parses untyped number values as float64.
|
|
|
|
// We convert integer properties from float64 to int64 here.
|
|
|
|
for name, v := range instance {
|
|
|
|
propertySchema, ok := s.Properties[name]
|
2024-01-25 10:09:42 +00:00
|
|
|
if !ok || propertySchema.Type != IntegerType {
|
2023-09-07 14:36:06 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
integerValue, err := toInteger(v)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to parse property %s: %w", name, err)
|
|
|
|
}
|
|
|
|
instance[name] = integerValue
|
|
|
|
}
|
|
|
|
return instance, s.ValidateInstance(instance)
|
|
|
|
}
|
|
|
|
|
2023-10-19 07:08:36 +00:00
|
|
|
// Validate an instance against the schema
|
2023-09-07 14:36:06 +00:00
|
|
|
func (s *Schema) ValidateInstance(instance map[string]any) error {
|
2023-10-19 07:08:36 +00:00
|
|
|
validations := []func(map[string]any) error{
|
2023-09-08 12:07:22 +00:00
|
|
|
s.validateAdditionalProperties,
|
|
|
|
s.validateEnum,
|
|
|
|
s.validateRequired,
|
|
|
|
s.validateTypes,
|
2023-09-25 09:53:38 +00:00
|
|
|
s.validatePattern,
|
2024-01-25 10:09:42 +00:00
|
|
|
s.validateConst,
|
|
|
|
s.validateAnyOf,
|
2023-10-19 07:08:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, fn := range validations {
|
2023-09-08 12:07:22 +00:00
|
|
|
err := fn(instance)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-09-07 14:36:06 +00:00
|
|
|
}
|
2023-09-08 12:07:22 +00:00
|
|
|
return nil
|
2023-09-07 14:36:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If additional properties is set to false, this function validates instance only
|
|
|
|
// contains properties defined in the schema.
|
|
|
|
func (s *Schema) validateAdditionalProperties(instance map[string]any) error {
|
|
|
|
// Note: AdditionalProperties has the type any.
|
|
|
|
if s.AdditionalProperties != false {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
for k := range instance {
|
|
|
|
_, ok := s.Properties[k]
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("property %s is not defined in the schema", k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// This function validates that all require properties in the schema have values
|
|
|
|
// in the instance.
|
|
|
|
func (s *Schema) validateRequired(instance map[string]any) error {
|
|
|
|
for _, name := range s.Required {
|
|
|
|
if _, ok := instance[name]; !ok {
|
|
|
|
return fmt.Errorf("no value provided for required property %s", name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validates the types of all input properties values match their types defined in the schema
|
|
|
|
func (s *Schema) validateTypes(instance map[string]any) error {
|
|
|
|
for k, v := range instance {
|
|
|
|
fieldInfo, ok := s.Properties[k]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
err := validateType(v, fieldInfo.Type)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("incorrect type for property %s: %w", k, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2023-09-08 12:07:22 +00:00
|
|
|
|
|
|
|
func (s *Schema) validateEnum(instance map[string]any) error {
|
|
|
|
for k, v := range instance {
|
|
|
|
fieldInfo, ok := s.Properties[k]
|
2024-01-25 10:09:42 +00:00
|
|
|
if !ok || fieldInfo.Enum == nil {
|
2023-09-08 12:07:22 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
if !slices.Contains(fieldInfo.Enum, v) {
|
|
|
|
return fmt.Errorf("expected value of property %s to be one of %v. Found: %v", k, fieldInfo.Enum, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2023-09-25 09:53:38 +00:00
|
|
|
|
|
|
|
func (s *Schema) validatePattern(instance map[string]any) error {
|
|
|
|
for k, v := range instance {
|
|
|
|
fieldInfo, ok := s.Properties[k]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
2023-11-06 15:05:17 +00:00
|
|
|
err := validatePatternMatch(k, v, fieldInfo)
|
2023-10-24 15:56:54 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-09-25 09:53:38 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2024-01-25 10:09:42 +00:00
|
|
|
|
|
|
|
func (s *Schema) validateConst(instance map[string]any) error {
|
|
|
|
for k, v := range instance {
|
|
|
|
fieldInfo, ok := s.Properties[k]
|
|
|
|
if !ok || fieldInfo.Const == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if v != fieldInfo.Const {
|
|
|
|
return fmt.Errorf("expected value of property %s to be %v. Found: %v", k, fieldInfo.Const, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validates that the instance matches at least one of the schemas in anyOf
|
|
|
|
// For more information, see https://json-schema.org/understanding-json-schema/reference/combining#anyof.
|
|
|
|
func (s *Schema) validateAnyOf(instance map[string]any) error {
|
|
|
|
if s.AnyOf == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// According to the JSON schema RFC, anyOf must contain at least one schema.
|
|
|
|
// https://json-schema.org/draft/2020-12/json-schema-core
|
|
|
|
if len(s.AnyOf) == 0 {
|
|
|
|
return fmt.Errorf("anyOf must contain at least one schema")
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, anyOf := range s.AnyOf {
|
|
|
|
err := anyOf.ValidateInstance(instance)
|
|
|
|
if err == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return fmt.Errorf("instance does not match any of the schemas in anyOf")
|
|
|
|
}
|