├── wercker.yml ├── testdata ├── draft4 │ ├── remotes │ │ ├── integer.json │ │ ├── folder │ │ │ └── folderInteger.json │ │ └── subSchemas.json │ ├── optional │ │ ├── zeroTerminatedFloats.json │ │ ├── bignum.json │ │ └── format.json │ ├── benchmark │ │ ├── invalid.json │ │ ├── schema2.json │ │ ├── valid.json │ │ ├── schema4.json │ │ └── schema3.json │ ├── pattern.json │ ├── minItems.json │ ├── maxItems.json │ ├── minProperties.json │ ├── maxProperties.json │ ├── definitions.json │ ├── minLength.json │ ├── maxLength.json │ ├── required.json │ ├── maximum.json │ ├── minimum.json │ ├── items.json │ ├── multipleOf.json │ ├── anyOf.json │ ├── oneOf.json │ ├── enum.json │ ├── refRemote.json │ ├── additionalProperties.json │ ├── additionalItems.json │ ├── not.json │ ├── uniqueItems.json │ ├── properties.json │ ├── allOf.json │ ├── dependencies.json │ ├── patternProperties.json │ ├── ref.json │ └── type.json └── core.json ├── format_regex.go ├── format_ipv4.go ├── format_ipv6.go ├── format_datetime.go ├── README.md ├── keyword_maxItems.go ├── keyword_maxLength.go ├── keyword_maxProperties.go ├── keyword_not.go ├── keyword_enum.go ├── keyword_format.go ├── keyword_minItems.go ├── keyword_minLength.go ├── keyword_minProperties.go ├── keyword_pattern.go ├── format_uri_reference.go ├── schema.go ├── keyword_definitions.go ├── keyword_multipleOf.go ├── keyword_required.go ├── format_uri.go ├── keyword_minimum.go ├── keyword_maximum.go ├── benchmark_test.go ├── keyword_anyOf.go ├── format_email.go ├── format_hostname.go ├── keyword_oneOf.go ├── keyword_allOf.go ├── keyword_uniqueItems.go ├── keyword_dependencies.go ├── util.go ├── keyword_type.go ├── keyword_items.go ├── context.go ├── env.go ├── keyword_properties.go ├── schema_test.go ├── env_draft4.go └── errors.go /wercker.yml: -------------------------------------------------------------------------------- 1 | box: wercker/golang 2 | -------------------------------------------------------------------------------- /testdata/draft4/remotes/integer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "integer" 3 | } -------------------------------------------------------------------------------- /testdata/draft4/remotes/folder/folderInteger.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "integer" 3 | } -------------------------------------------------------------------------------- /testdata/draft4/remotes/subSchemas.json: -------------------------------------------------------------------------------- 1 | { 2 | "integer": { 3 | "type": "integer" 4 | }, 5 | "refToInteger": { 6 | "$ref": "#/integer" 7 | } 8 | } -------------------------------------------------------------------------------- /format_regex.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | type regexFormat struct{} 8 | 9 | func (*regexFormat) IsValid(x interface{}) bool { 10 | s, ok := x.(string) 11 | if !ok { 12 | return true 13 | } 14 | 15 | _, err := regexp.Compile(s) 16 | return err == nil 17 | } 18 | -------------------------------------------------------------------------------- /format_ipv4.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | type ipv4Format struct{} 8 | 9 | func (*ipv4Format) IsValid(x interface{}) bool { 10 | s, ok := x.(string) 11 | if !ok { 12 | return true 13 | } 14 | 15 | ip := net.ParseIP(s) 16 | return ip != nil && ip.To4() != nil 17 | } 18 | -------------------------------------------------------------------------------- /format_ipv6.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | type ipv6Format struct{} 8 | 9 | func (*ipv6Format) IsValid(x interface{}) bool { 10 | s, ok := x.(string) 11 | if !ok { 12 | return true 13 | } 14 | 15 | ip := net.ParseIP(s) 16 | return ip != nil && ip.To4() == nil 17 | } 18 | -------------------------------------------------------------------------------- /format_datetime.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type datetimeFormat struct{} 8 | 9 | func (*datetimeFormat) IsValid(x interface{}) bool { 10 | s, ok := x.(string) 11 | if !ok { 12 | return true 13 | } 14 | 15 | _, err := time.Parse(time.RFC3339, s) 16 | return err == nil 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `jsonschema` 2 | 3 | [![wercker status](https://app.wercker.com/status/591b52015b939ca1884a064fc566db1c/s "wercker status")](https://app.wercker.com/project/bykey/591b52015b939ca1884a064fc566db1c) [![GoDoc](https://godoc.org/github.com/fd/jsonschema?status.png)](https://godoc.org/github.com/fd/jsonschema) 4 | 5 | A Go implemenation of [json-schema](http://json-schema.org/). 6 | -------------------------------------------------------------------------------- /testdata/draft4/optional/zeroTerminatedFloats.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "some languages do not distinguish between different types of numeric value", 4 | "schema": { 5 | "type": "integer" 6 | }, 7 | "tests": [ 8 | { 9 | "description": "a float is not an integer even without fractional part", 10 | "data": 1.0, 11 | "valid": false 12 | } 13 | ] 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /testdata/draft4/benchmark/invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "fullName" : null, 3 | "age" : -1, 4 | "state" : 47, 5 | "city" : false, 6 | "zip" : [null], 7 | "married" : "yes", 8 | "dozen" : 50, 9 | "dozenOrBakersDozen" : "over 9000", 10 | "favoriteEvenNumber" : 15, 11 | "topThreeFavoriteColors" : [ "red", 5 ], 12 | "favoriteSingleDigitWholeNumbers" : [ 78, 2, 999 ], 13 | "favoriteFiveLetterWord" : "codernaut", 14 | "emailAddresses" : [], 15 | "ipAddresses" : [ "999.0.099.1", "294.48.64.2346", false, "2221409.64214128.42414.235233", "124124.12412412" ] 16 | } 17 | -------------------------------------------------------------------------------- /keyword_maxItems.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type maxItemsValidator struct { 8 | max int 9 | } 10 | 11 | func (v *maxItemsValidator) Setup(builder Builder) error { 12 | if x, found := builder.GetKeyword("maxItems"); found { 13 | i, ok := x.(int64) 14 | if !ok { 15 | return fmt.Errorf("invalid 'maxItems' definition: %#v", x) 16 | } 17 | 18 | v.max = int(i) 19 | } 20 | return nil 21 | } 22 | 23 | func (v *maxItemsValidator) Validate(x interface{}, ctx *Context) { 24 | y, ok := x.([]interface{}) 25 | if !ok || y == nil { 26 | return 27 | } 28 | 29 | if len(y) > v.max { 30 | ctx.Report(&ErrTooLong{v.max, x}) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /testdata/draft4/pattern.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "pattern validation", 4 | "schema": {"pattern": "^a*$"}, 5 | "tests": [ 6 | { 7 | "description": "a matching pattern is valid", 8 | "data": "aaa", 9 | "valid": true 10 | }, 11 | { 12 | "description": "a non-matching pattern is invalid", 13 | "data": "abc", 14 | "valid": false 15 | }, 16 | { 17 | "description": "ignores non-strings", 18 | "data": true, 19 | "valid": true 20 | } 21 | ] 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /keyword_maxLength.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | "unicode/utf8" 6 | ) 7 | 8 | type maxLengthValidator struct { 9 | max int 10 | } 11 | 12 | func (v *maxLengthValidator) Setup(builder Builder) error { 13 | if x, found := builder.GetKeyword("maxLength"); found { 14 | i, ok := x.(int64) 15 | if !ok { 16 | return fmt.Errorf("invalid 'maxLength' definition: %#v", x) 17 | } 18 | 19 | v.max = int(i) 20 | } 21 | return nil 22 | } 23 | 24 | func (v *maxLengthValidator) Validate(x interface{}, ctx *Context) { 25 | y, ok := x.(string) 26 | if !ok { 27 | return 28 | } 29 | 30 | l := utf8.RuneCountInString(y) 31 | 32 | if l > v.max { 33 | ctx.Report(&ErrTooLong{v.max, x}) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /keyword_maxProperties.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type maxPropertiesValidator struct { 8 | max int 9 | } 10 | 11 | func (v *maxPropertiesValidator) Setup(builder Builder) error { 12 | if x, found := builder.GetKeyword("maxProperties"); found { 13 | i, ok := x.(int64) 14 | if !ok { 15 | return fmt.Errorf("invalid 'maxProperties' definition: %#v", x) 16 | } 17 | 18 | v.max = int(i) 19 | } 20 | return nil 21 | } 22 | 23 | func (v *maxPropertiesValidator) Validate(x interface{}, ctx *Context) { 24 | y, ok := x.(map[string]interface{}) 25 | if !ok || y == nil { 26 | return 27 | } 28 | 29 | l := len(y) 30 | 31 | if l > v.max { 32 | ctx.Report(&ErrTooLong{v.max, x}) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /keyword_not.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type notValidator struct { 8 | schema *Schema 9 | } 10 | 11 | func (v *notValidator) Setup(builder Builder) error { 12 | if x, found := builder.GetKeyword("not"); found { 13 | y, ok := x.(map[string]interface{}) 14 | if !ok || y == nil { 15 | return fmt.Errorf("invalid 'not' definition: %#v", x) 16 | } 17 | 18 | schema, err := builder.Build("/not", y) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | v.schema = schema 24 | } 25 | return nil 26 | } 27 | 28 | func (v *notValidator) Validate(x interface{}, ctx *Context) { 29 | _, err := ctx.ValidateSelfWith(v.schema) 30 | if err == nil { 31 | ctx.Report(&ErrNotNot{x, v.schema}) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /keyword_enum.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type enumValidator struct { 8 | enum []interface{} 9 | } 10 | 11 | func (v *enumValidator) Setup(builder Builder) error { 12 | if x, found := builder.GetKeyword("enum"); found { 13 | y, ok := x.([]interface{}) 14 | if !ok || y == nil || len(y) == 0 { 15 | return fmt.Errorf("invalid 'enum' definition: %#v", x) 16 | } 17 | 18 | v.enum = y 19 | } 20 | return nil 21 | } 22 | 23 | func (v *enumValidator) Validate(x interface{}, ctx *Context) { 24 | for _, y := range v.enum { 25 | equal, err := isEqual(x, y) 26 | if err != nil { 27 | ctx.Report(err) 28 | } 29 | if equal { 30 | return 31 | } 32 | } 33 | 34 | ctx.Report(&ErrInvalidEnum{v.enum, x}) 35 | } 36 | -------------------------------------------------------------------------------- /keyword_format.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type formatValidator struct { 8 | name string 9 | format FormatValidator 10 | } 11 | 12 | func (v *formatValidator) Setup(builder Builder) error { 13 | if x, found := builder.GetKeyword("format"); found { 14 | y, ok := x.(string) 15 | if !ok { 16 | return fmt.Errorf("invalid 'format' definition: %#v", x) 17 | } 18 | 19 | format := builder.GetFormatValidator(y) 20 | if format == nil { 21 | return fmt.Errorf("invalid 'format' definition: %#v (unknown format)", x) 22 | } 23 | 24 | v.name = y 25 | v.format = format 26 | } 27 | return nil 28 | } 29 | 30 | func (v *formatValidator) Validate(x interface{}, ctx *Context) { 31 | if !v.format.IsValid(x) { 32 | ctx.Report(&ErrInvalidFormat{x, v.name}) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /testdata/draft4/minItems.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "minItems validation", 4 | "schema": {"minItems": 1}, 5 | "tests": [ 6 | { 7 | "description": "longer is valid", 8 | "data": [1, 2], 9 | "valid": true 10 | }, 11 | { 12 | "description": "exact length is valid", 13 | "data": [1], 14 | "valid": true 15 | }, 16 | { 17 | "description": "too short is invalid", 18 | "data": [], 19 | "valid": false 20 | }, 21 | { 22 | "description": "ignores non-arrays", 23 | "data": "", 24 | "valid": true 25 | } 26 | ] 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /keyword_minItems.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type minItemsValidator struct { 9 | min int 10 | } 11 | 12 | func (v *minItemsValidator) Setup(builder Builder) error { 13 | if x, found := builder.GetKeyword("minItems"); found { 14 | y, ok := x.(json.Number) 15 | if !ok { 16 | return fmt.Errorf("invalid 'minItems' definition: %#v", x) 17 | } 18 | 19 | i, err := y.Int64() 20 | if err != nil { 21 | return fmt.Errorf("invalid 'minItems' definition: %#v (%s)", x, err) 22 | } 23 | 24 | v.min = int(i) 25 | } 26 | return nil 27 | } 28 | 29 | func (v *minItemsValidator) Validate(x interface{}, ctx *Context) { 30 | y, ok := x.([]interface{}) 31 | if !ok || y == nil { 32 | return 33 | } 34 | 35 | if len(y) < v.min { 36 | ctx.Report(&ErrTooShort{v.min, x}) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /testdata/draft4/maxItems.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "maxItems validation", 4 | "schema": {"maxItems": 2}, 5 | "tests": [ 6 | { 7 | "description": "shorter is valid", 8 | "data": [1], 9 | "valid": true 10 | }, 11 | { 12 | "description": "exact length is valid", 13 | "data": [1, 2], 14 | "valid": true 15 | }, 16 | { 17 | "description": "too long is invalid", 18 | "data": [1, 2, 3], 19 | "valid": false 20 | }, 21 | { 22 | "description": "ignores non-arrays", 23 | "data": "foobar", 24 | "valid": true 25 | } 26 | ] 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /testdata/draft4/minProperties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "minProperties validation", 4 | "schema": {"minProperties": 1}, 5 | "tests": [ 6 | { 7 | "description": "longer is valid", 8 | "data": {"foo": 1, "bar": 2}, 9 | "valid": true 10 | }, 11 | { 12 | "description": "exact length is valid", 13 | "data": {"foo": 1}, 14 | "valid": true 15 | }, 16 | { 17 | "description": "too short is invalid", 18 | "data": {}, 19 | "valid": false 20 | }, 21 | { 22 | "description": "ignores non-objects", 23 | "data": "", 24 | "valid": true 25 | } 26 | ] 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /keyword_minLength.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "unicode/utf8" 7 | ) 8 | 9 | type minLengthValidator struct { 10 | min int 11 | } 12 | 13 | func (v *minLengthValidator) Setup(builder Builder) error { 14 | if x, found := builder.GetKeyword("minLength"); found { 15 | y, ok := x.(json.Number) 16 | if !ok { 17 | return fmt.Errorf("invalid 'minLength' definition: %#v", x) 18 | } 19 | 20 | i, err := y.Int64() 21 | if err != nil { 22 | return fmt.Errorf("invalid 'minLength' definition: %#v (%s)", x, err) 23 | } 24 | 25 | v.min = int(i) 26 | } 27 | return nil 28 | } 29 | 30 | func (v *minLengthValidator) Validate(x interface{}, ctx *Context) { 31 | y, ok := x.(string) 32 | if !ok { 33 | return 34 | } 35 | 36 | l := utf8.RuneCountInString(y) 37 | 38 | if l < v.min { 39 | ctx.Report(&ErrTooShort{v.min, x}) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /keyword_minProperties.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type minPropertiesValidator struct { 9 | min int 10 | } 11 | 12 | func (v *minPropertiesValidator) Setup(builder Builder) error { 13 | if x, found := builder.GetKeyword("minProperties"); found { 14 | y, ok := x.(json.Number) 15 | if !ok { 16 | return fmt.Errorf("invalid 'minProperties' definition: %#v", x) 17 | } 18 | 19 | i, err := y.Int64() 20 | if err != nil { 21 | return fmt.Errorf("invalid 'minProperties' definition: %#v (%s)", x, err) 22 | } 23 | 24 | v.min = int(i) 25 | } 26 | return nil 27 | } 28 | 29 | func (v *minPropertiesValidator) Validate(x interface{}, ctx *Context) { 30 | y, ok := x.(map[string]interface{}) 31 | if !ok || y == nil { 32 | return 33 | } 34 | 35 | l := len(y) 36 | 37 | if l < v.min { 38 | ctx.Report(&ErrTooLong{v.min, x}) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /testdata/draft4/maxProperties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "maxProperties validation", 4 | "schema": {"maxProperties": 2}, 5 | "tests": [ 6 | { 7 | "description": "shorter is valid", 8 | "data": {"foo": 1}, 9 | "valid": true 10 | }, 11 | { 12 | "description": "exact length is valid", 13 | "data": {"foo": 1, "bar": 2}, 14 | "valid": true 15 | }, 16 | { 17 | "description": "too long is invalid", 18 | "data": {"foo": 1, "bar": 2, "baz": 3}, 19 | "valid": false 20 | }, 21 | { 22 | "description": "ignores non-objects", 23 | "data": "foobar", 24 | "valid": true 25 | } 26 | ] 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /keyword_pattern.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | type patternValidator struct { 9 | pattern string 10 | regexp *regexp.Regexp 11 | } 12 | 13 | func (v *patternValidator) Setup(builder Builder) error { 14 | if x, found := builder.GetKeyword("pattern"); found { 15 | if y, ok := x.(string); ok { 16 | r, err := regexp.Compile(y) 17 | if err != nil { 18 | return fmt.Errorf("invalid 'pattern' definition: %#v (error: %s)", x, err) 19 | } 20 | v.pattern = y 21 | v.regexp = r 22 | return nil 23 | } 24 | 25 | return fmt.Errorf("invalid 'pattern' definition: %#v", x) 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func (v *patternValidator) Validate(x interface{}, ctx *Context) { 32 | y, ok := x.(string) 33 | if !ok { 34 | return 35 | } 36 | 37 | if !v.regexp.MatchString(y) { 38 | ctx.Report(&ErrInvalidPattern{v.pattern, y}) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /format_uri_reference.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | type uriReferenceFormat struct{} 8 | 9 | func (*uriReferenceFormat) IsValid(x interface{}) bool { 10 | s, ok := x.(string) 11 | if !ok { 12 | return true 13 | } 14 | 15 | u, err := url.Parse(s) 16 | if err != nil { 17 | return false 18 | } 19 | 20 | for i, l := 0, len(u.Path); i < l; i++ { 21 | c := u.Path[i] 22 | if 'a' <= c && c <= 'z' { 23 | continue 24 | } 25 | if 'A' <= c && c <= 'Z' { 26 | continue 27 | } 28 | if '0' <= c && c <= '9' { 29 | continue 30 | } 31 | if '-' == c || '.' == c || '_' == c || '~' == c || '!' == c || 32 | '$' == c || '&' == c || '\'' == c || '(' == c || ')' == c || 33 | '*' == c || '+' == c || ',' == c || ';' == c || '=' == c || 34 | ':' == c || '@' == c || '%' == c || '/' == c { 35 | continue 36 | } 37 | return false 38 | } 39 | 40 | return true 41 | } 42 | -------------------------------------------------------------------------------- /schema.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/url" 7 | ) 8 | 9 | type Schema struct { 10 | Id *url.URL 11 | Ref *url.URL 12 | RefSchema *Schema 13 | Validators []Validator 14 | Definition map[string]interface{} 15 | Subschemas map[string]*Schema 16 | } 17 | 18 | type Validator interface { 19 | Setup(b Builder) error 20 | Validate(interface{}, *Context) 21 | } 22 | 23 | type FormatValidator interface { 24 | IsValid(interface{}) bool 25 | } 26 | 27 | func (s *Schema) Validate(v interface{}) error { 28 | _, err := newContext().ValidateValueWith(v, s) 29 | return err 30 | } 31 | 32 | func (s *Schema) ValidateData(d []byte) error { 33 | var ( 34 | v interface{} 35 | ) 36 | 37 | dec := json.NewDecoder(bytes.NewReader(d)) 38 | dec.UseNumber() 39 | err := dec.Decode(&v) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | return s.Validate(v) 45 | } 46 | -------------------------------------------------------------------------------- /keyword_definitions.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type definitionsValidator struct { 8 | } 9 | 10 | func (v *definitionsValidator) Setup(builder Builder) error { 11 | if x, found := builder.GetKeyword("definitions"); found { 12 | y, ok := x.(map[string]interface{}) 13 | if !ok || y == nil { 14 | return fmt.Errorf("invalid 'definitions' definition: %#v", x) 15 | } 16 | 17 | schemas := make(map[string]*Schema, len(y)) 18 | for name, a := range y { 19 | b, ok := a.(map[string]interface{}) 20 | if !ok { 21 | return fmt.Errorf("invalid 'definitions' definition: %#v", x) 22 | } 23 | 24 | schema, err := builder.Build("/definitions/"+escapeJSONPointer(name), b) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | schemas[name] = schema 30 | } 31 | } 32 | return nil 33 | } 34 | 35 | func (v *definitionsValidator) Validate(x interface{}, ctx *Context) { 36 | } 37 | -------------------------------------------------------------------------------- /testdata/draft4/definitions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "valid definition", 4 | "schema": {"$ref": "http://json-schema.org/draft-04/schema#"}, 5 | "tests": [ 6 | { 7 | "description": "valid definition schema", 8 | "data": { 9 | "definitions": { 10 | "foo": {"type": "integer"} 11 | } 12 | }, 13 | "valid": true 14 | } 15 | ] 16 | }, 17 | { 18 | "description": "invalid definition", 19 | "schema": {"$ref": "http://json-schema.org/draft-04/schema#"}, 20 | "tests": [ 21 | { 22 | "description": "invalid definition schema", 23 | "data": { 24 | "definitions": { 25 | "foo": {"type": 1} 26 | } 27 | }, 28 | "valid": false 29 | } 30 | ] 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /keyword_multipleOf.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | type multipleOfValidator struct { 9 | factor float64 10 | } 11 | 12 | func (v *multipleOfValidator) Setup(builder Builder) error { 13 | if x, found := builder.GetKeyword("multipleOf"); found { 14 | f, ok := x.(float64) 15 | if !ok { 16 | return fmt.Errorf("invalid 'multipleOf' definition: %#v", x) 17 | } 18 | 19 | if f < math.SmallestNonzeroFloat64 { 20 | return fmt.Errorf("invalid 'multipleOf' definition: %#v", x) 21 | } 22 | 23 | v.factor = f 24 | } 25 | return nil 26 | } 27 | 28 | func (v *multipleOfValidator) Validate(x interface{}, ctx *Context) { 29 | f, ok, err := toFloat(x) 30 | if !ok { 31 | return 32 | } 33 | if err != nil { 34 | ctx.Report(err) 35 | return 36 | } 37 | 38 | rem := math.Abs(math.Remainder(f, v.factor)) 39 | rem /= v.factor // normalize rem between 0.0 and 1.0 40 | ok = rem < 0.000000001 41 | 42 | if !ok { 43 | ctx.Report(&ErrNotMultipleOf{v.factor, x}) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /testdata/draft4/minLength.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "minLength validation", 4 | "schema": {"minLength": 2}, 5 | "tests": [ 6 | { 7 | "description": "longer is valid", 8 | "data": "foo", 9 | "valid": true 10 | }, 11 | { 12 | "description": "exact length is valid", 13 | "data": "fo", 14 | "valid": true 15 | }, 16 | { 17 | "description": "too short is invalid", 18 | "data": "f", 19 | "valid": false 20 | }, 21 | { 22 | "description": "ignores non-strings", 23 | "data": 1, 24 | "valid": true 25 | }, 26 | { 27 | "description": "one supplementary Unicode code point is not long enough", 28 | "data": "\uD83D\uDCA9", 29 | "valid": false 30 | } 31 | ] 32 | } 33 | ] 34 | -------------------------------------------------------------------------------- /testdata/draft4/maxLength.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "maxLength validation", 4 | "schema": {"maxLength": 2}, 5 | "tests": [ 6 | { 7 | "description": "shorter is valid", 8 | "data": "f", 9 | "valid": true 10 | }, 11 | { 12 | "description": "exact length is valid", 13 | "data": "fo", 14 | "valid": true 15 | }, 16 | { 17 | "description": "too long is invalid", 18 | "data": "foo", 19 | "valid": false 20 | }, 21 | { 22 | "description": "ignores non-strings", 23 | "data": 100, 24 | "valid": true 25 | }, 26 | { 27 | "description": "two supplementary Unicode code points is long enough", 28 | "data": "\uD83D\uDCA9\uD83D\uDCA9", 29 | "valid": true 30 | } 31 | ] 32 | } 33 | ] 34 | -------------------------------------------------------------------------------- /keyword_required.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type requiredValidator struct { 8 | required []string 9 | } 10 | 11 | func (v *requiredValidator) Setup(builder Builder) error { 12 | if x, found := builder.GetKeyword("required"); found { 13 | switch y := x.(type) { 14 | 15 | case []string: 16 | v.required = y 17 | 18 | case []interface{}: 19 | var z = make([]string, len(y)) 20 | for i, a := range y { 21 | if b, ok := a.(string); ok { 22 | z[i] = b 23 | } else { 24 | return fmt.Errorf("invalid 'required' definition: %#v", x) 25 | } 26 | } 27 | v.required = z 28 | 29 | default: 30 | return fmt.Errorf("invalid 'required' definition: %#v", x) 31 | } 32 | } 33 | return nil 34 | } 35 | 36 | func (v *requiredValidator) Validate(x interface{}, ctx *Context) { 37 | y, ok := x.(map[string]interface{}) 38 | if !ok || y == nil { 39 | return 40 | } 41 | 42 | for _, k := range v.required { 43 | _, found := y[k] 44 | if !found { 45 | ctx.Report(&ErrRequiredProperty{k}) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /testdata/draft4/required.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "required validation", 4 | "schema": { 5 | "properties": { 6 | "foo": {}, 7 | "bar": {} 8 | }, 9 | "required": ["foo"] 10 | }, 11 | "tests": [ 12 | { 13 | "description": "present required property is valid", 14 | "data": {"foo": 1}, 15 | "valid": true 16 | }, 17 | { 18 | "description": "non-present required property is invalid", 19 | "data": {"bar": 1}, 20 | "valid": false 21 | } 22 | ] 23 | }, 24 | { 25 | "description": "required default validation", 26 | "schema": { 27 | "properties": { 28 | "foo": {} 29 | } 30 | }, 31 | "tests": [ 32 | { 33 | "description": "not required by default", 34 | "data": {}, 35 | "valid": true 36 | } 37 | ] 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /format_uri.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | type uriFormat struct{} 8 | 9 | func (*uriFormat) IsValid(x interface{}) bool { 10 | s, ok := x.(string) 11 | if !ok { 12 | return true 13 | } 14 | 15 | u, err := url.Parse(s) 16 | if err != nil { 17 | return false 18 | } 19 | 20 | if u.Scheme == "" { 21 | return false 22 | } 23 | 24 | if u.Host == "" { 25 | return false 26 | } 27 | 28 | // url.Parse() already validated the path 29 | // for i, l := 0, len(u.Path); i < l; i++ { 30 | // c := u.Path[i] 31 | // if 'a' <= c && c <= 'z' { 32 | // continue 33 | // } 34 | // if 'A' <= c && c <= 'Z' { 35 | // continue 36 | // } 37 | // if '0' <= c && c <= '9' { 38 | // continue 39 | // } 40 | // if '-' == c || '.' == c || '_' == c || '~' == c || '!' == c || 41 | // '$' == c || '&' == c || '\'' == c || '(' == c || ')' == c || 42 | // '*' == c || '+' == c || ',' == c || ';' == c || '=' == c || 43 | // ':' == c || '@' == c || '%' == c || '/' == c { 44 | // continue 45 | // } 46 | // return false 47 | // } 48 | 49 | return true 50 | } 51 | -------------------------------------------------------------------------------- /keyword_minimum.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type minimumValidator struct { 8 | min float64 9 | exclusive bool 10 | } 11 | 12 | func (v *minimumValidator) Setup(builder Builder) error { 13 | if x, ok := builder.GetKeyword("exclusiveMinimum"); ok { 14 | y, ok := x.(bool) 15 | 16 | if !ok { 17 | return fmt.Errorf("invalid 'exclusiveMinimum' definition: %#v", x) 18 | } 19 | 20 | v.exclusive = y 21 | } 22 | 23 | if x, found := builder.GetKeyword("minimum"); found { 24 | f, ok, err := toFloat(x) 25 | if !ok { 26 | return fmt.Errorf("invalid 'minimum' definition: %#v", x) 27 | } 28 | if err != nil { 29 | return fmt.Errorf("invalid 'minimum' definition: %#v (%s)", x, err) 30 | } 31 | 32 | v.min = f 33 | } 34 | return nil 35 | } 36 | 37 | func (v *minimumValidator) Validate(x interface{}, ctx *Context) { 38 | f, ok, err := toFloat(x) 39 | if !ok { 40 | return 41 | } 42 | if err != nil { 43 | ctx.Report(err) 44 | return 45 | } 46 | 47 | if v.exclusive { 48 | ok = f > v.min 49 | } else { 50 | ok = f >= v.min 51 | } 52 | 53 | if !ok { 54 | ctx.Report(&ErrTooSmall{v.min, v.exclusive, x}) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /keyword_maximum.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type maximumValidator struct { 8 | max float64 9 | exclusive bool 10 | } 11 | 12 | func (v *maximumValidator) Setup(builder Builder) error { 13 | if x, ok := builder.GetKeyword("exclusiveMaximum"); ok { 14 | y, ok := x.(bool) 15 | 16 | if !ok { 17 | return fmt.Errorf("invalid 'exclusiveMaximum' definition: %#v", x) 18 | } 19 | 20 | v.exclusive = y 21 | } 22 | 23 | if x, found := builder.GetKeyword("maximum"); found { 24 | f, ok, err := toFloat(x) 25 | if !ok { 26 | return fmt.Errorf("invalid 'maximum' definition: %#v", x) 27 | } 28 | if err != nil { 29 | return fmt.Errorf("invalid 'maximum' definition: %#v (%s)", x, err) 30 | } 31 | 32 | v.max = f 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func (v *maximumValidator) Validate(x interface{}, ctx *Context) { 39 | f, ok, err := toFloat(x) 40 | if !ok { 41 | return 42 | } 43 | if err != nil { 44 | ctx.Report(err) 45 | return 46 | } 47 | 48 | if v.exclusive { 49 | ok = f < v.max 50 | } else { 51 | ok = f <= v.max 52 | } 53 | 54 | if !ok { 55 | ctx.Report(&ErrTooLarge{v.max, v.exclusive, x}) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // See: https://github.com/Sembiance/cosmicrealms.com/blob/master/sandbox/benchmark-of-node-dot-js-json-validation-modules-part-2 8 | func BenchmarkValid(b *testing.B) { 9 | env := RootEnv.Clone() 10 | schema, err := env.BuildSchema("", load_test_data("draft4/benchmark/schema4.json")) 11 | if err != nil { 12 | panic(err) 13 | } 14 | 15 | var instance interface{} 16 | load_test_json("draft4/benchmark/valid.json", &instance) 17 | 18 | b.ResetTimer() 19 | b.ReportAllocs() 20 | 21 | for i := 0; i < b.N; i++ { 22 | err := schema.Validate(instance) 23 | if err != nil { 24 | b.Fatalf("error=%s", err) 25 | } 26 | } 27 | } 28 | 29 | func BenchmarkInvalid(b *testing.B) { 30 | env := RootEnv.Clone() 31 | schema, err := env.BuildSchema("", load_test_data("draft4/benchmark/schema4.json")) 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | var instance interface{} 37 | load_test_json("draft4/benchmark/invalid.json", &instance) 38 | 39 | b.ResetTimer() 40 | b.ReportAllocs() 41 | 42 | for i := 0; i < b.N; i++ { 43 | err := schema.Validate(instance) 44 | if err == nil { 45 | b.Fatalf("error=%s", "expected an error") 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /testdata/draft4/maximum.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "maximum validation", 4 | "schema": {"maximum": 3.0}, 5 | "tests": [ 6 | { 7 | "description": "below the maximum is valid", 8 | "data": 2.6, 9 | "valid": true 10 | }, 11 | { 12 | "description": "above the maximum is invalid", 13 | "data": 3.5, 14 | "valid": false 15 | }, 16 | { 17 | "description": "ignores non-numbers", 18 | "data": "x", 19 | "valid": true 20 | } 21 | ] 22 | }, 23 | { 24 | "description": "exclusiveMaximum validation", 25 | "schema": { 26 | "maximum": 3.0, 27 | "exclusiveMaximum": true 28 | }, 29 | "tests": [ 30 | { 31 | "description": "below the maximum is still valid", 32 | "data": 2.2, 33 | "valid": true 34 | }, 35 | { 36 | "description": "boundary point is invalid", 37 | "data": 3.0, 38 | "valid": false 39 | } 40 | ] 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /testdata/draft4/minimum.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "minimum validation", 4 | "schema": {"minimum": 1.1}, 5 | "tests": [ 6 | { 7 | "description": "above the minimum is valid", 8 | "data": 2.6, 9 | "valid": true 10 | }, 11 | { 12 | "description": "below the minimum is invalid", 13 | "data": 0.6, 14 | "valid": false 15 | }, 16 | { 17 | "description": "ignores non-numbers", 18 | "data": "x", 19 | "valid": true 20 | } 21 | ] 22 | }, 23 | { 24 | "description": "exclusiveMinimum validation", 25 | "schema": { 26 | "minimum": 1.1, 27 | "exclusiveMinimum": true 28 | }, 29 | "tests": [ 30 | { 31 | "description": "above the minimum is still valid", 32 | "data": 1.2, 33 | "valid": true 34 | }, 35 | { 36 | "description": "boundary point is invalid", 37 | "data": 1.1, 38 | "valid": false 39 | } 40 | ] 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /keyword_anyOf.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type anyOfValidator struct { 8 | schemas []*Schema 9 | } 10 | 11 | func (v *anyOfValidator) Setup(builder Builder) error { 12 | if x, found := builder.GetKeyword("anyOf"); found { 13 | y, ok := x.([]interface{}) 14 | if !ok || y == nil { 15 | return fmt.Errorf("invalid 'anyOf' definition: %#v", x) 16 | } 17 | 18 | schemas := make([]*Schema, len(y)) 19 | for i, a := range y { 20 | b, ok := a.(map[string]interface{}) 21 | if !ok { 22 | return fmt.Errorf("invalid 'anyOf' definition: %#v", x) 23 | } 24 | 25 | schema, err := builder.Build(fmt.Sprintf("/anyOf/%d", i), b) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | schemas[i] = schema 31 | } 32 | 33 | v.schemas = schemas 34 | } 35 | return nil 36 | } 37 | 38 | func (v *anyOfValidator) Validate(x interface{}, ctx *Context) { 39 | var ( 40 | errors []error 41 | ) 42 | 43 | for i, schema := range v.schemas { 44 | _, err := ctx.ValidateSelfWith(schema) 45 | if err == nil { 46 | return 47 | } 48 | 49 | if errors == nil { 50 | errors = make([]error, len(v.schemas)) 51 | } 52 | 53 | errors[i] = err 54 | } 55 | 56 | ctx.Report(&ErrNotAnyOf{x, v.schemas, errors}) 57 | } 58 | -------------------------------------------------------------------------------- /format_email.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type emailFormat struct { 8 | hostname hostnameFormat 9 | ipv4 ipv4Format 10 | ipv6 ipv6Format 11 | } 12 | 13 | func (f *emailFormat) IsValid(x interface{}) bool { 14 | s, ok := x.(string) 15 | if !ok { 16 | return true 17 | } 18 | 19 | if len(s) == 0 { 20 | return false 21 | } 22 | 23 | idx := 0 24 | if s[0] == '"' { 25 | offset := 1 26 | for { 27 | i := strings.IndexByte(s[offset:], '"') 28 | if i < 0 { 29 | return false 30 | } 31 | offset += i + 1 32 | if s[offset-2] != '\\' { 33 | break 34 | } 35 | } 36 | 37 | idx = strings.IndexByte(s[offset:], '@') 38 | if idx != 0 { 39 | return false 40 | } 41 | idx = offset + idx 42 | } else { 43 | idx = strings.IndexByte(s, '@') 44 | if idx <= 0 { 45 | return false 46 | } 47 | } 48 | 49 | node := s[idx+1:] 50 | if len(node) == 0 { 51 | return false 52 | } 53 | 54 | if strings.HasPrefix(node, "[IPv6:") && strings.HasSuffix(node, "]") { 55 | return f.ipv6.IsValid(node[6 : len(node)-1]) 56 | } 57 | 58 | if strings.HasPrefix(node, "[") && strings.HasSuffix(node, "]") { 59 | return f.ipv6.IsValid(node[1 : len(node)-1]) 60 | } 61 | 62 | return f.hostname.IsValid(node) 63 | } 64 | -------------------------------------------------------------------------------- /format_hostname.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // See: 8 | // http://tools.ietf.org/html/rfc1034#section-3.1 9 | // http://en.wikipedia.org/wiki/Domain_Name_System#Domain_name_syntax 10 | type hostnameFormat struct{} 11 | 12 | func (*hostnameFormat) IsValid(x interface{}) bool { 13 | s, ok := x.(string) 14 | if !ok { 15 | return true 16 | } 17 | 18 | if len(s) > 253 || len(s) == 0 { 19 | return false 20 | } 21 | 22 | for len(s) > 0 { 23 | var ( 24 | label string 25 | idx = strings.IndexByte(s, '.') 26 | ) 27 | 28 | if idx == 0 { 29 | return false 30 | } else if idx < 0 { 31 | label = s 32 | s = "" 33 | } else { 34 | label = s[:idx] 35 | s = s[idx+1:] 36 | } 37 | 38 | if len(label) > 63 || len(label) == 0 { 39 | return false 40 | } 41 | 42 | if strings.HasPrefix(label, "xn--") { 43 | label = label[4:] 44 | } 45 | 46 | last_i := len(label) - 1 47 | for i, char := range label { 48 | if 'a' <= char && char <= 'z' { 49 | continue 50 | } 51 | if 'A' <= char && char <= 'Z' { 52 | continue 53 | } 54 | if '0' <= char && char <= '9' { 55 | continue 56 | } 57 | if i != 0 && i != last_i && char == '-' { 58 | continue 59 | } 60 | return false 61 | } 62 | } 63 | 64 | return true 65 | } 66 | -------------------------------------------------------------------------------- /keyword_oneOf.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type oneOfValidator struct { 8 | schemas []*Schema 9 | } 10 | 11 | func (v *oneOfValidator) Setup(builder Builder) error { 12 | if x, found := builder.GetKeyword("oneOf"); found { 13 | y, ok := x.([]interface{}) 14 | if !ok || y == nil { 15 | return fmt.Errorf("invalid 'oneOf' definition: %#v", x) 16 | } 17 | 18 | schemas := make([]*Schema, len(y)) 19 | for i, a := range y { 20 | b, ok := a.(map[string]interface{}) 21 | if !ok { 22 | return fmt.Errorf("invalid 'oneOf' definition: %#v", x) 23 | } 24 | 25 | schema, err := builder.Build(fmt.Sprintf("/oneOf/%d", i), b) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | schemas[i] = schema 31 | } 32 | 33 | v.schemas = schemas 34 | } 35 | return nil 36 | } 37 | 38 | func (v *oneOfValidator) Validate(x interface{}, ctx *Context) { 39 | var ( 40 | errors []error 41 | passed int 42 | ) 43 | 44 | for i, schema := range v.schemas { 45 | _, err := ctx.ValidateSelfWith(schema) 46 | 47 | if err == nil { 48 | passed++ 49 | } 50 | 51 | if errors == nil { 52 | errors = make([]error, len(v.schemas)) 53 | } 54 | 55 | errors[i] = err 56 | } 57 | 58 | if passed != 1 { 59 | ctx.Report(&ErrNotOneOf{x, v.schemas, errors}) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /testdata/draft4/items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "a schema given for items", 4 | "schema": { 5 | "items": {"type": "integer"} 6 | }, 7 | "tests": [ 8 | { 9 | "description": "valid items", 10 | "data": [ 1, 2, 3 ], 11 | "valid": true 12 | }, 13 | { 14 | "description": "wrong type of items", 15 | "data": [1, "x"], 16 | "valid": false 17 | }, 18 | { 19 | "description": "ignores non-arrays", 20 | "data": {"foo" : "bar"}, 21 | "valid": true 22 | } 23 | ] 24 | }, 25 | { 26 | "description": "an array of schemas for items", 27 | "schema": { 28 | "items": [ 29 | {"type": "integer"}, 30 | {"type": "string"} 31 | ] 32 | }, 33 | "tests": [ 34 | { 35 | "description": "correct types", 36 | "data": [ 1, "foo" ], 37 | "valid": true 38 | }, 39 | { 40 | "description": "wrong types", 41 | "data": [ "foo", 1 ], 42 | "valid": false 43 | } 44 | ] 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /keyword_allOf.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type allOfValidator struct { 8 | schemas []*Schema 9 | } 10 | 11 | func (v *allOfValidator) Setup(builder Builder) error { 12 | if x, found := builder.GetKeyword("allOf"); found { 13 | y, ok := x.([]interface{}) 14 | if !ok || y == nil { 15 | return fmt.Errorf("invalid 'allOf' definition: %#v", x) 16 | } 17 | 18 | schemas := make([]*Schema, len(y)) 19 | for i, a := range y { 20 | b, ok := a.(map[string]interface{}) 21 | if !ok { 22 | return fmt.Errorf("invalid 'allOf' definition: %#v", x) 23 | } 24 | 25 | schema, err := builder.Build(fmt.Sprintf("/allOf/%d", i), b) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | schemas[i] = schema 31 | } 32 | 33 | v.schemas = schemas 34 | } 35 | return nil 36 | } 37 | 38 | func (v *allOfValidator) Validate(x interface{}, ctx *Context) { 39 | var ( 40 | errors []error 41 | failed = false 42 | ) 43 | 44 | for i, schema := range v.schemas { 45 | _, err := ctx.ValidateSelfWith(schema) 46 | 47 | if err != nil { 48 | failed = true 49 | 50 | if errors == nil { 51 | errors = make([]error, len(v.schemas)) 52 | } 53 | 54 | errors[i] = err 55 | } 56 | } 57 | 58 | if failed { 59 | ctx.Report(&ErrNotAllOf{x, ctx.CurrentSchema(), v.schemas, errors}) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /keyword_uniqueItems.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | type uniqueItemsValidator struct { 8 | unique bool 9 | } 10 | 11 | func (v *uniqueItemsValidator) Setup(builder Builder) error { 12 | if x, found := builder.GetKeyword("uniqueItems"); found { 13 | if y, ok := x.(bool); ok && y { 14 | v.unique = true 15 | } 16 | } 17 | return nil 18 | } 19 | 20 | func (v *uniqueItemsValidator) Validate(x interface{}, ctx *Context) { 21 | y, ok := x.([]interface{}) 22 | if !ok || y == nil { 23 | return 24 | } 25 | 26 | var ( 27 | l = len(y) 28 | skipbuf [32]int 29 | skip = skipbuf[:0] 30 | ) 31 | 32 | if l > cap(skip) { 33 | skip = make([]int, 0, l) 34 | } 35 | 36 | for i := 0; i < l; i++ { 37 | if containsInt(skip, i) { 38 | continue 39 | } 40 | for j := i + 1; j < l; j++ { 41 | if containsInt(skip, j) { 42 | continue 43 | } 44 | 45 | a, b := y[i], y[j] 46 | 47 | equal, err := isEqual(a, b) 48 | if err != nil { 49 | skip = append(skip, j) 50 | sort.Ints(skip) 51 | ctx.Report(err) 52 | continue 53 | } 54 | 55 | // other values 56 | if equal { 57 | skip = append(skip, j) 58 | sort.Ints(skip) 59 | ctx.Report(&ErrNotUnique{i, j, a}) 60 | } 61 | } 62 | } 63 | } 64 | 65 | func containsInt(s []int, x int) bool { 66 | idx := sort.SearchInts(s, x) 67 | if idx == len(s) { 68 | return false 69 | } 70 | return s[idx] == x 71 | } 72 | -------------------------------------------------------------------------------- /testdata/draft4/benchmark/schema2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "test", 3 | "type" : "object", 4 | "additionalProperties" : false, 5 | "properties" : 6 | { 7 | "fullName" : { "type" : "string" }, 8 | "age" : { "type" : "integer", "minimum" : 0 }, 9 | "optionalItem" : { "type" : "string", "optional" : true }, 10 | "state" : { "type" : "string", "optional" : true }, 11 | "city" : { "type" : "string", "optional" : true }, 12 | "zip" : { "type" : "integer", "format" : "postal-code" }, 13 | "married" : { "type" : "boolean" }, 14 | "dozen" : { "type" : "integer", "minimum" : 12, "maximum" : 12 }, 15 | "dozenOrBakersDozen" : { "type" : "integer", "minimum" : 12, "maximum" : 13 }, 16 | "favoriteEvenNumber" : { "type" : "integer", "divisibleBy" : 2 }, 17 | "topThreeFavoriteColors" : { "type" : "array", "minItems" : 3, "maxItems" : 3, "uniqueItems" : true, "items" : { "type" : "string", "format" : "color" }}, 18 | "favoriteSingleDigitWholeNumbers" : { "type" : "array", "minItems" : 1, "maxItems" : 10, "uniqueItems" : true, "items" : { "type" : "integer", "minimum" : 0, "maximum" : 9 }}, 19 | "favoriteFiveLetterWord" : { "type" : "string", "minLength" : 5, "maxLength" : 5 }, 20 | "emailAddresses" : { "type" : "array", "minItems" : 1, "uniqueItems" : true, "items" : { "type" : "string", "format" : "email" }}, 21 | "ipAddresses" : { "type" : "array", "uniqueItems" : true, "items" : { "type" : "string", "format" : "ip-address" }}, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/draft4/multipleOf.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "by int", 4 | "schema": {"multipleOf": 2}, 5 | "tests": [ 6 | { 7 | "description": "int by int", 8 | "data": 10, 9 | "valid": true 10 | }, 11 | { 12 | "description": "int by int fail", 13 | "data": 7, 14 | "valid": false 15 | }, 16 | { 17 | "description": "ignores non-numbers", 18 | "data": "foo", 19 | "valid": true 20 | } 21 | ] 22 | }, 23 | { 24 | "description": "by number", 25 | "schema": {"multipleOf": 1.5}, 26 | "tests": [ 27 | { 28 | "description": "zero is multiple of anything", 29 | "data": 0, 30 | "valid": true 31 | }, 32 | { 33 | "description": "4.5 is multiple of 1.5", 34 | "data": 4.5, 35 | "valid": true 36 | }, 37 | { 38 | "description": "35 is not multiple of 1.5", 39 | "data": 35, 40 | "valid": false 41 | } 42 | ] 43 | }, 44 | { 45 | "description": "by small number", 46 | "schema": {"multipleOf": 0.0001}, 47 | "tests": [ 48 | { 49 | "description": "0.0075 is multiple of 0.0001", 50 | "data": 0.0075, 51 | "valid": true 52 | }, 53 | { 54 | "description": "0.00751 is not multiple of 0.0001", 55 | "data": 0.00751, 56 | "valid": false 57 | } 58 | ] 59 | } 60 | ] 61 | -------------------------------------------------------------------------------- /testdata/draft4/benchmark/valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "fullName" : "John Doe", 3 | "age" : 47, 4 | "state" : "Massachusetts", 5 | "city" : "Boston", 6 | "zip" : 16417, 7 | "married" : false, 8 | "dozen" : 12, 9 | "dozenOrBakersDozen" : 13, 10 | "favoriteEvenNumber" : 14, 11 | "topThreeFavoriteColors" : [ "red", "blue", "green" ], 12 | "favoriteSingleDigitWholeNumbers" : [ 7 ], 13 | "favoriteFiveLetterWord" : "coder", 14 | "emailAddresses" : 15 | [ 16 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ@letters-in-local.org", 17 | "01234567890@numbers-in-local.net", 18 | "&'*+-./=?^_{}~@other-valid-characters-in-local.net", 19 | "mixed-1234-in-{+^}-local@sld.net", 20 | "a@single-character-in-local.org", 21 | "\"quoted\"@sld.com", 22 | "\"\\e\\s\\c\\a\\p\\e\\d\"@sld.com", 23 | "\"quoted-at-sign@sld.org\"@sld.com", 24 | "\"escaped\\\"quote\"@sld.com", 25 | "\"back\\slash\"@sld.com", 26 | "one-character-third-level@a.example.com", 27 | "single-character-in-sld@x.org", 28 | "local@dash-in-sld.com", 29 | "letters-in-sld@123.com", 30 | "one-letter-sld@x.org", 31 | "uncommon-tld@sld.museum", 32 | "uncommon-tld@sld.travel", 33 | "uncommon-tld@sld.mobi", 34 | "country-code-tld@sld.uk", 35 | "country-code-tld@sld.rw", 36 | "local@sld.newTLD", 37 | "the-total-length@of-an-entire-address.cannot-be-longer-than-two-hundred-and-fifty-four-characters.and-this-address-is-254-characters-exactly.so-it-should-be-valid.and-im-going-to-add-some-more-words-here.to-increase-the-lenght-blah-blah-blah-blah-bla.org", 38 | "the-character-limit@for-each-part.of-the-domain.is-sixty-three-characters.this-is-exactly-sixty-three-characters-so-it-is-valid-blah-blah.com", 39 | "local@sub.domains.com" 40 | ], 41 | "ipAddresses" : [ "127.0.0.1", "24.48.64.2", "192.168.1.1", "209.68.44.3", "2.2.2.2" ] 42 | } 43 | -------------------------------------------------------------------------------- /testdata/draft4/benchmark/schema4.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "test", 3 | "type" : "object", 4 | "additionalProperties" : false, 5 | "required" : ["fullName", "age", "zip", "married", "dozen", "dozenOrBakersDozen", "favoriteEvenNumber", "topThreeFavoriteColors", "favoriteSingleDigitWholeNumbers", "favoriteFiveLetterWord", "emailAddresses", "ipAddresses"], 6 | "properties" : 7 | { 8 | "fullName" : { "type" : "string" }, 9 | "age" : { "type" : "integer", "minimum" : 0 }, 10 | "optionalItem" : { "type" : "string" }, 11 | "state" : { "type" : "string" }, 12 | "city" : { "type" : "string" }, 13 | "zip" : { "type" : "integer", "minimum" : 0, "maximum" : 99999 }, 14 | "married" : { "type" : "boolean" }, 15 | "dozen" : { "type" : "integer", "minimum" : 12, "maximum" : 12 }, 16 | "dozenOrBakersDozen" : { "type" : "integer", "minimum" : 12, "maximum" : 13 }, 17 | "favoriteEvenNumber" : { "type" : "integer", "multipleOf" : 2 }, 18 | "topThreeFavoriteColors" : { "type" : "array", "minItems" : 3, "maxItems" : 3, "uniqueItems" : true, "items" : { "type" : "string" }}, 19 | "favoriteSingleDigitWholeNumbers" : { "type" : "array", "minItems" : 1, "maxItems" : 10, "uniqueItems" : true, "items" : { "type" : "integer", "minimum" : 0, "maximum" : 9 }}, 20 | "favoriteFiveLetterWord" : { "type" : "string", "minLength" : 5, "maxLength" : 5 }, 21 | "emailAddresses" : { "type" : "array", "minItems" : 1, "uniqueItems" : true, "items" : { "type" : "string", "format" : "email" }}, 22 | "ipAddresses" : { "type" : "array", "uniqueItems" : true, "items" : { "type" : "string", "format" : "ipv4" }} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/draft4/anyOf.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "anyOf", 4 | "schema": { 5 | "anyOf": [ 6 | { 7 | "type": "integer" 8 | }, 9 | { 10 | "minimum": 2 11 | } 12 | ] 13 | }, 14 | "tests": [ 15 | { 16 | "description": "first anyOf valid", 17 | "data": 1, 18 | "valid": true 19 | }, 20 | { 21 | "description": "second anyOf valid", 22 | "data": 2.5, 23 | "valid": true 24 | }, 25 | { 26 | "description": "both anyOf valid", 27 | "data": 3, 28 | "valid": true 29 | }, 30 | { 31 | "description": "neither anyOf valid", 32 | "data": 1.5, 33 | "valid": false 34 | } 35 | ] 36 | }, 37 | { 38 | "description": "anyOf with base schema", 39 | "schema": { 40 | "type": "string", 41 | "anyOf" : [ 42 | { 43 | "maxLength": 2 44 | }, 45 | { 46 | "minLength": 4 47 | } 48 | ] 49 | }, 50 | "tests": [ 51 | { 52 | "description": "mismatch base schema", 53 | "data": 3, 54 | "valid": false 55 | }, 56 | { 57 | "description": "one anyOf valid", 58 | "data": "foobar", 59 | "valid": true 60 | }, 61 | { 62 | "description": "both anyOf invalid", 63 | "data": "foo", 64 | "valid": false 65 | } 66 | ] 67 | } 68 | ] 69 | -------------------------------------------------------------------------------- /testdata/draft4/oneOf.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "oneOf", 4 | "schema": { 5 | "oneOf": [ 6 | { 7 | "type": "integer" 8 | }, 9 | { 10 | "minimum": 2 11 | } 12 | ] 13 | }, 14 | "tests": [ 15 | { 16 | "description": "first oneOf valid", 17 | "data": 1, 18 | "valid": true 19 | }, 20 | { 21 | "description": "second oneOf valid", 22 | "data": 2.5, 23 | "valid": true 24 | }, 25 | { 26 | "description": "both oneOf valid", 27 | "data": 3, 28 | "valid": false 29 | }, 30 | { 31 | "description": "neither oneOf valid", 32 | "data": 1.5, 33 | "valid": false 34 | } 35 | ] 36 | }, 37 | { 38 | "description": "oneOf with base schema", 39 | "schema": { 40 | "type": "string", 41 | "oneOf" : [ 42 | { 43 | "minLength": 2 44 | }, 45 | { 46 | "maxLength": 4 47 | } 48 | ] 49 | }, 50 | "tests": [ 51 | { 52 | "description": "mismatch base schema", 53 | "data": 3, 54 | "valid": false 55 | }, 56 | { 57 | "description": "one oneOf valid", 58 | "data": "foobar", 59 | "valid": true 60 | }, 61 | { 62 | "description": "both oneOf valid", 63 | "data": "foo", 64 | "valid": false 65 | } 66 | ] 67 | } 68 | ] 69 | -------------------------------------------------------------------------------- /testdata/draft4/benchmark/schema3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "test", 3 | "type" : "object", 4 | "additionalProperties" : false, 5 | "properties" : 6 | { 7 | "fullName" : { "type" : "string", "required" : true }, 8 | "age" : { "type" : "integer", "required" : true, "minimum" : 0 }, 9 | "optionalItem" : { "type" : "string" }, 10 | "state" : { "type" : "string" }, 11 | "city" : { "type" : "string" }, 12 | "zip" : { "type" : "integer", "required" : true, "minimum" : 0, "maximum" : 99999 }, 13 | "married" : { "type" : "boolean", "required" : true }, 14 | "dozen" : { "type" : "integer", "required" : true, "minimum" : 12, "maximum" : 12 }, 15 | "dozenOrBakersDozen" : { "type" : "integer", "required" : true, "minimum" : 12, "maximum" : 13 }, 16 | "favoriteEvenNumber" : { "type" : "integer", "required" : true, "divisibleBy" : 2 }, 17 | "topThreeFavoriteColors" : { "type" : "array", "required" : true, "minItems" : 3, "maxItems" : 3, "uniqueItems" : true, "items" : { "type" : "string", "format" : "color" }}, 18 | "favoriteSingleDigitWholeNumbers" : { "type" : "array", "required" : true, "minItems" : 1, "maxItems" : 10, "uniqueItems" : true, "items" : { "type" : "integer", "minimum" : 0, "maximum" : 9 }}, 19 | "favoriteFiveLetterWord" : { "type" : "string", "required" : true, "minLength" : 5, "maxLength" : 5 }, 20 | "emailAddresses" : { "type" : "array", "required" : true, "minItems" : 1, "uniqueItems" : true, "items" : { "type" : "string", "format" : "email" }}, 21 | "ipAddresses" : { "type" : "array", "required" : true, "uniqueItems" : true, "items" : { "type" : "string", "format" : "ip-address" }}, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/draft4/optional/bignum.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "integer", 4 | "schema": {"type": "integer"}, 5 | "tests": [ 6 | { 7 | "description": "a bignum is an integer", 8 | "data": 12345678910111213141516171819202122232425262728293031, 9 | "valid": true 10 | } 11 | ] 12 | }, 13 | { 14 | "description": "number", 15 | "schema": {"type": "number"}, 16 | "tests": [ 17 | { 18 | "description": "a bignum is a number", 19 | "data": 98249283749234923498293171823948729348710298301928331, 20 | "valid": true 21 | } 22 | ] 23 | }, 24 | { 25 | "description": "string", 26 | "schema": {"type": "string"}, 27 | "tests": [ 28 | { 29 | "description": "a bignum is not a string", 30 | "data": 98249283749234923498293171823948729348710298301928331, 31 | "valid": false 32 | } 33 | ] 34 | }, 35 | { 36 | "description": "integer comparison", 37 | "schema": {"maximum": 18446744073709551615}, 38 | "tests": [ 39 | { 40 | "description": "comparison works for high numbers", 41 | "data": 18446744073709551600, 42 | "valid": true 43 | } 44 | ] 45 | }, 46 | { 47 | "description": "float comparison with high precision", 48 | "schema": { 49 | "maximum": 972783798187987123879878123.18878137, 50 | "exclusiveMaximum": true 51 | }, 52 | "tests": [ 53 | { 54 | "description": "comparison works for high numbers", 55 | "data": 972783798187987123879878123.188781371, 56 | "valid": false 57 | } 58 | ] 59 | } 60 | ] 61 | -------------------------------------------------------------------------------- /keyword_dependencies.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type dependenciesValidator struct { 8 | dependencies map[string]interface{} 9 | } 10 | 11 | func (v *dependenciesValidator) Setup(builder Builder) error { 12 | if x, found := builder.GetKeyword("dependencies"); found { 13 | y, ok := x.(map[string]interface{}) 14 | if !ok || y == nil { 15 | return fmt.Errorf("invalid 'dependencies' definition: %#v", x) 16 | } 17 | 18 | dependencies := make(map[string]interface{}, len(y)) 19 | for dependant, a := range y { 20 | switch b := a.(type) { 21 | case []interface{}: 22 | deps := make([]string, len(b)) 23 | for i, d := range b { 24 | if e, ok := d.(string); ok { 25 | deps[i] = e 26 | } else { 27 | return fmt.Errorf("invalid 'dependencies' definition: %#v", x) 28 | } 29 | } 30 | dependencies[dependant] = deps 31 | 32 | case map[string]interface{}: 33 | schema, err := builder.Build("/dependencies/"+escapeJSONPointer(dependant), b) 34 | if err != nil { 35 | return err 36 | } 37 | dependencies[dependant] = schema 38 | 39 | default: 40 | return fmt.Errorf("invalid 'dependencies' definition: %#v", x) 41 | } 42 | } 43 | 44 | v.dependencies = dependencies 45 | } 46 | return nil 47 | } 48 | 49 | func (v *dependenciesValidator) Validate(x interface{}, ctx *Context) { 50 | y, ok := x.(map[string]interface{}) 51 | if !ok || y == nil { 52 | return 53 | } 54 | 55 | for k, a := range v.dependencies { 56 | if _, found := y[k]; !found { 57 | continue 58 | } 59 | 60 | switch d := a.(type) { 61 | case []string: 62 | for _, dep := range d { 63 | if _, found := y[dep]; !found { 64 | ctx.Report(&ErrInvalidDependency{Property: k, Dependency: dep}) 65 | } 66 | } 67 | 68 | case *Schema: 69 | _, err := ctx.ValidateValueWith(x, d) 70 | if err != nil { 71 | ctx.Report(&ErrInvalidDependency{Property: k, Schema: d, Err: err}) 72 | } 73 | 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /testdata/draft4/enum.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "simple enum validation", 4 | "schema": {"enum": [1, 2, 3]}, 5 | "tests": [ 6 | { 7 | "description": "one of the enum is valid", 8 | "data": 1, 9 | "valid": true 10 | }, 11 | { 12 | "description": "something else is invalid", 13 | "data": 4, 14 | "valid": false 15 | } 16 | ] 17 | }, 18 | { 19 | "description": "heterogeneous enum validation", 20 | "schema": {"enum": [6, "foo", [], true, {"foo": 12}]}, 21 | "tests": [ 22 | { 23 | "description": "one of the enum is valid", 24 | "data": [], 25 | "valid": true 26 | }, 27 | { 28 | "description": "something else is invalid", 29 | "data": null, 30 | "valid": false 31 | }, 32 | { 33 | "description": "objects are deep compared", 34 | "data": {"foo": false}, 35 | "valid": false 36 | } 37 | ] 38 | }, 39 | { 40 | "description": "enums in properties", 41 | "schema": { 42 | "type":"object", 43 | "properties": { 44 | "foo": {"enum":["foo"]}, 45 | "bar": {"enum":["bar"]} 46 | }, 47 | "required": ["bar"] 48 | }, 49 | "tests": [ 50 | { 51 | "description": "both properties are valid", 52 | "data": {"foo":"foo", "bar":"bar"}, 53 | "valid": true 54 | }, 55 | { 56 | "description": "missing optional property is valid", 57 | "data": {"bar":"bar"}, 58 | "valid": true 59 | }, 60 | { 61 | "description": "missing required property is invalid", 62 | "data": {"foo":"foo"}, 63 | "valid": false 64 | }, 65 | { 66 | "description": "missing all properties is invalid", 67 | "data": {}, 68 | "valid": false 69 | } 70 | ] 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /testdata/draft4/refRemote.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "remote ref", 4 | "schema": {"$ref": "http://localhost:1234/integer.json"}, 5 | "tests": [ 6 | { 7 | "description": "remote ref valid", 8 | "data": 1, 9 | "valid": true 10 | }, 11 | { 12 | "description": "remote ref invalid", 13 | "data": "a", 14 | "valid": false 15 | } 16 | ] 17 | }, 18 | { 19 | "description": "fragment within remote ref", 20 | "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, 21 | "tests": [ 22 | { 23 | "description": "remote fragment valid", 24 | "data": 1, 25 | "valid": true 26 | }, 27 | { 28 | "description": "remote fragment invalid", 29 | "data": "a", 30 | "valid": false 31 | } 32 | ] 33 | }, 34 | { 35 | "description": "ref within remote ref", 36 | "schema": { 37 | "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" 38 | }, 39 | "tests": [ 40 | { 41 | "description": "ref within ref valid", 42 | "data": 1, 43 | "valid": true 44 | }, 45 | { 46 | "description": "ref within ref invalid", 47 | "data": "a", 48 | "valid": false 49 | } 50 | ] 51 | }, 52 | { 53 | "description": "change resolution scope", 54 | "schema": { 55 | "id": "http://localhost:1234/", 56 | "items": { 57 | "id": "folder/", 58 | "items": {"$ref": "folderInteger.json"} 59 | } 60 | }, 61 | "tests": [ 62 | { 63 | "description": "changed scope ref valid", 64 | "data": [[1]], 65 | "valid": true 66 | }, 67 | { 68 | "description": "changed scope ref invalid", 69 | "data": [["a"]], 70 | "valid": false 71 | } 72 | ] 73 | } 74 | ] 75 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "encoding/json" 5 | "math" 6 | "net/url" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | func isEqual(a, b interface{}) (bool, error) { 12 | 13 | // handle numbers mathematically 14 | if f, ok, err := toFloat(a); ok { 15 | if err != nil { 16 | return false, err 17 | } 18 | if g, ok, err := toFloat(b); ok { 19 | if err != nil { 20 | return false, err 21 | } 22 | 23 | d := math.Abs(f - g) 24 | return d < 0.000000001, nil 25 | } 26 | } 27 | 28 | // other values 29 | return reflect.DeepEqual(a, b), nil 30 | 31 | } 32 | 33 | func toFloat(x interface{}) (float64, bool, error) { 34 | switch y := x.(type) { 35 | 36 | case json.Number: 37 | f, err := y.Float64() 38 | if err != nil { 39 | return 0, true, err 40 | } 41 | return f, true, nil 42 | 43 | case int64: 44 | return float64(y), true, nil 45 | 46 | case float64: 47 | return y, true, nil 48 | 49 | default: 50 | return 0, false, nil 51 | 52 | } 53 | } 54 | 55 | func isRef(x interface{}) (string, bool) { 56 | m, ok := x.(map[string]interface{}) 57 | if !ok { 58 | return "", false 59 | } 60 | 61 | refi, found := m["$ref"] 62 | if !found { 63 | return "", false 64 | } 65 | 66 | ref, ok := refi.(string) 67 | if !ok { 68 | return "", false 69 | } 70 | 71 | if strings.IndexByte(ref, '#') < 0 { 72 | ref += "#" 73 | } 74 | 75 | return ref, true 76 | } 77 | 78 | func escapeJSONPointer(s string) string { 79 | s = strings.Replace(s, "~", "~0", -1) 80 | s = strings.Replace(s, "/", "~1", -1) 81 | return s 82 | } 83 | 84 | func normalizeRef(r string) string { 85 | if strings.IndexByte(r, '#') < 0 { 86 | r += "#" 87 | } 88 | return r 89 | } 90 | 91 | func resolveRef(base, ref *url.URL) *url.URL { 92 | dst := base.ResolveReference(ref) 93 | dst.Fragment = ref.Fragment 94 | return dst 95 | } 96 | 97 | func rootRef(ref string) string { 98 | ref = normalizeRef(ref) 99 | idx := strings.IndexByte(ref, '#') 100 | return ref[:idx+1] 101 | } 102 | 103 | func refURL(ref string) string { 104 | ref = normalizeRef(ref) 105 | idx := strings.IndexByte(ref, '#') 106 | return ref[:idx] 107 | } 108 | 109 | func refFragment(ref string) string { 110 | ref = normalizeRef(ref) 111 | idx := strings.IndexByte(ref, '#') 112 | return ref[idx+1:] 113 | } 114 | -------------------------------------------------------------------------------- /testdata/draft4/additionalProperties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": 4 | "additionalProperties being false does not allow other properties", 5 | "schema": { 6 | "properties": {"foo": {}, "bar": {}}, 7 | "patternProperties": { "^v": {} }, 8 | "additionalProperties": false 9 | }, 10 | "tests": [ 11 | { 12 | "description": "no additional properties is valid", 13 | "data": {"foo": 1}, 14 | "valid": true 15 | }, 16 | { 17 | "description": "an additional property is invalid", 18 | "data": {"foo" : 1, "bar" : 2, "quux" : "boom"}, 19 | "valid": false 20 | }, 21 | { 22 | "description": "ignores non-objects", 23 | "data": [1, 2, 3], 24 | "valid": true 25 | }, 26 | { 27 | "description": "patternProperties are not additional properties", 28 | "data": {"foo":1, "vroom": 2}, 29 | "valid": true 30 | } 31 | ] 32 | }, 33 | { 34 | "description": 35 | "additionalProperties allows a schema which should validate", 36 | "schema": { 37 | "properties": {"foo": {}, "bar": {}}, 38 | "additionalProperties": {"type": "boolean"} 39 | }, 40 | "tests": [ 41 | { 42 | "description": "no additional properties is valid", 43 | "data": {"foo": 1}, 44 | "valid": true 45 | }, 46 | { 47 | "description": "an additional valid property is valid", 48 | "data": {"foo" : 1, "bar" : 2, "quux" : true}, 49 | "valid": true 50 | }, 51 | { 52 | "description": "an additional invalid property is invalid", 53 | "data": {"foo" : 1, "bar" : 2, "quux" : 12}, 54 | "valid": false 55 | } 56 | ] 57 | }, 58 | { 59 | "description": "additionalProperties are allowed by default", 60 | "schema": {"properties": {"foo": {}, "bar": {}}}, 61 | "tests": [ 62 | { 63 | "description": "additional properties are allowed", 64 | "data": {"foo": 1, "bar": 2, "quux": true}, 65 | "valid": true 66 | } 67 | ] 68 | } 69 | ] 70 | -------------------------------------------------------------------------------- /keyword_type.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type typeValidator struct { 9 | expects []PrimitiveType 10 | } 11 | 12 | func (v *typeValidator) Setup(builder Builder) error { 13 | if x, found := builder.GetKeyword("type"); found { 14 | switch y := x.(type) { 15 | case string: 16 | v.expects = []PrimitiveType{PrimitiveType(y)} 17 | 18 | case []string: 19 | var z = make([]PrimitiveType, len(y)) 20 | for i, a := range y { 21 | z[i] = PrimitiveType(a) 22 | } 23 | v.expects = z 24 | 25 | case []interface{}: 26 | var z = make([]PrimitiveType, len(y)) 27 | for i, a := range y { 28 | if b, ok := a.(string); ok { 29 | z[i] = PrimitiveType(b) 30 | } else { 31 | return fmt.Errorf("invalid type expectation: %#v", x) 32 | } 33 | } 34 | v.expects = z 35 | 36 | default: 37 | return fmt.Errorf("invalid type expectation: %#v", x) 38 | } 39 | 40 | for _, t := range v.expects { 41 | if !t.Valid() { 42 | return fmt.Errorf("invalid type expectation: %#v", t) 43 | } 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | func (v *typeValidator) Validate(x interface{}, ctx *Context) { 50 | for _, t := range v.expects { 51 | switch t { 52 | case ArrayType: 53 | if _, ok := x.([]interface{}); ok && x != nil { 54 | return 55 | } 56 | 57 | case BooleanType: 58 | if _, ok := x.(bool); ok { 59 | return 60 | } 61 | 62 | case IntegerType: 63 | if y, ok := x.(json.Number); ok { 64 | i, err := y.Int64() 65 | if err == nil { 66 | ctx.UpdateValue(i) 67 | return 68 | } 69 | } 70 | if _, ok := x.(int64); ok { 71 | return 72 | } 73 | 74 | case NullType: 75 | if x == nil { 76 | return 77 | } 78 | 79 | case NumberType: 80 | if y, ok := x.(json.Number); ok { 81 | f, err := y.Float64() 82 | if err == nil { 83 | ctx.UpdateValue(f) 84 | return 85 | } 86 | } 87 | if _, ok := x.(float64); ok { 88 | return 89 | } 90 | if y, ok := x.(int64); ok { 91 | ctx.UpdateValue(float64(y)) 92 | return 93 | } 94 | 95 | case ObjectType: 96 | if _, ok := x.(map[string]interface{}); ok && x != nil { 97 | return 98 | } 99 | 100 | case StringType: 101 | if _, ok := x.(string); ok { 102 | return 103 | } 104 | 105 | default: 106 | panic("invalid type: " + t) 107 | } 108 | } 109 | 110 | ctx.Report(&ErrInvalidType{expected: v.expects, was: x}) 111 | } 112 | -------------------------------------------------------------------------------- /testdata/draft4/additionalItems.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "additionalItems as schema", 4 | "schema": { 5 | "items": [{}], 6 | "additionalItems": {"type": "integer"} 7 | }, 8 | "tests": [ 9 | { 10 | "description": "additional items match schema", 11 | "data": [ null, 2, 3, 4 ], 12 | "valid": true 13 | }, 14 | { 15 | "description": "additional items do not match schema", 16 | "data": [ null, 2, 3, "foo" ], 17 | "valid": false 18 | } 19 | ] 20 | }, 21 | { 22 | "description": "items is schema, no additionalItems", 23 | "schema": { 24 | "items": {}, 25 | "additionalItems": false 26 | }, 27 | "tests": [ 28 | { 29 | "description": "all items match schema", 30 | "data": [ 1, 2, 3, 4, 5 ], 31 | "valid": true 32 | } 33 | ] 34 | }, 35 | { 36 | "description": "array of items with no additionalItems", 37 | "schema": { 38 | "items": [{}, {}, {}], 39 | "additionalItems": false 40 | }, 41 | "tests": [ 42 | { 43 | "description": "no additional items present", 44 | "data": [ 1, 2, 3 ], 45 | "valid": true 46 | }, 47 | { 48 | "description": "additional items are not permitted", 49 | "data": [ 1, 2, 3, 4 ], 50 | "valid": false 51 | } 52 | ] 53 | }, 54 | { 55 | "description": "additionalItems as false without items", 56 | "schema": {"additionalItems": false}, 57 | "tests": [ 58 | { 59 | "description": 60 | "items defaults to empty schema so everything is valid", 61 | "data": [ 1, 2, 3, 4, 5 ], 62 | "valid": true 63 | }, 64 | { 65 | "description": "ignores non-arrays", 66 | "data": {"foo" : "bar"}, 67 | "valid": true 68 | } 69 | ] 70 | }, 71 | { 72 | "description": "additionalItems are allowed by default", 73 | "schema": {"items": [{"type": "integer"}]}, 74 | "tests": [ 75 | { 76 | "description": "only the first item is validated", 77 | "data": [1, "foo", false], 78 | "valid": true 79 | } 80 | ] 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /testdata/draft4/not.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "not", 4 | "schema": { 5 | "not": {"type": "integer"} 6 | }, 7 | "tests": [ 8 | { 9 | "description": "allowed", 10 | "data": "foo", 11 | "valid": true 12 | }, 13 | { 14 | "description": "disallowed", 15 | "data": 1, 16 | "valid": false 17 | } 18 | ] 19 | }, 20 | { 21 | "description": "not multiple types", 22 | "schema": { 23 | "not": {"type": ["integer", "boolean"]} 24 | }, 25 | "tests": [ 26 | { 27 | "description": "valid", 28 | "data": "foo", 29 | "valid": true 30 | }, 31 | { 32 | "description": "mismatch", 33 | "data": 1, 34 | "valid": false 35 | }, 36 | { 37 | "description": "other mismatch", 38 | "data": true, 39 | "valid": false 40 | } 41 | ] 42 | }, 43 | { 44 | "description": "not more complex schema", 45 | "schema": { 46 | "not": { 47 | "type": "object", 48 | "properties": { 49 | "foo": { 50 | "type": "string" 51 | } 52 | } 53 | } 54 | }, 55 | "tests": [ 56 | { 57 | "description": "match", 58 | "data": 1, 59 | "valid": true 60 | }, 61 | { 62 | "description": "other match", 63 | "data": {"foo": 1}, 64 | "valid": true 65 | }, 66 | { 67 | "description": "mismatch", 68 | "data": {"foo": "bar"}, 69 | "valid": false 70 | } 71 | ] 72 | }, 73 | { 74 | "description": "forbidden property", 75 | "schema": { 76 | "properties": { 77 | "foo": { 78 | "not": {} 79 | } 80 | } 81 | }, 82 | "tests": [ 83 | { 84 | "description": "property present", 85 | "data": {"foo": 1, "bar": 2}, 86 | "valid": false 87 | }, 88 | { 89 | "description": "property absent", 90 | "data": {"bar": 1, "baz": 2}, 91 | "valid": true 92 | } 93 | ] 94 | } 95 | 96 | ] 97 | -------------------------------------------------------------------------------- /testdata/draft4/uniqueItems.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "uniqueItems validation", 4 | "schema": {"uniqueItems": true}, 5 | "tests": [ 6 | { 7 | "description": "unique array of integers is valid", 8 | "data": [1, 2], 9 | "valid": true 10 | }, 11 | { 12 | "description": "non-unique array of integers is invalid", 13 | "data": [1, 1], 14 | "valid": false 15 | }, 16 | { 17 | "description": "numbers are unique if mathematically unequal", 18 | "data": [1.0, 1.00, 1], 19 | "valid": false 20 | }, 21 | { 22 | "description": "unique array of objects is valid", 23 | "data": [{"foo": "bar"}, {"foo": "baz"}], 24 | "valid": true 25 | }, 26 | { 27 | "description": "non-unique array of objects is invalid", 28 | "data": [{"foo": "bar"}, {"foo": "bar"}], 29 | "valid": false 30 | }, 31 | { 32 | "description": "unique array of nested objects is valid", 33 | "data": [ 34 | {"foo": {"bar" : {"baz" : true}}}, 35 | {"foo": {"bar" : {"baz" : false}}} 36 | ], 37 | "valid": true 38 | }, 39 | { 40 | "description": "non-unique array of nested objects is invalid", 41 | "data": [ 42 | {"foo": {"bar" : {"baz" : true}}}, 43 | {"foo": {"bar" : {"baz" : true}}} 44 | ], 45 | "valid": false 46 | }, 47 | { 48 | "description": "unique array of arrays is valid", 49 | "data": [["foo"], ["bar"]], 50 | "valid": true 51 | }, 52 | { 53 | "description": "non-unique array of arrays is invalid", 54 | "data": [["foo"], ["foo"]], 55 | "valid": false 56 | }, 57 | { 58 | "description": "1 and true are unique", 59 | "data": [1, true], 60 | "valid": true 61 | }, 62 | { 63 | "description": "0 and false are unique", 64 | "data": [0, false], 65 | "valid": true 66 | }, 67 | { 68 | "description": "unique heterogeneous types are valid", 69 | "data": [{}, [1], true, null, 1], 70 | "valid": true 71 | }, 72 | { 73 | "description": "non-unique heterogeneous types are invalid", 74 | "data": [{}, [1], true, null, {}, 1], 75 | "valid": false 76 | } 77 | ] 78 | } 79 | ] 80 | -------------------------------------------------------------------------------- /keyword_items.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var additionalItemsDenied = &Schema{} 8 | 9 | type itemsValidator struct { 10 | item *Schema 11 | items []*Schema 12 | additionalItem *Schema 13 | } 14 | 15 | func (v *itemsValidator) Setup(builder Builder) error { 16 | if x, found := builder.GetKeyword("items"); found { 17 | switch y := x.(type) { 18 | 19 | case map[string]interface{}: 20 | s, err := builder.Build("/items", y) 21 | if err != nil { 22 | return err 23 | } 24 | v.item = s 25 | 26 | case []interface{}: 27 | l := make([]*Schema, len(y)) 28 | for i, a := range y { 29 | b, ok := a.(map[string]interface{}) 30 | if !ok { 31 | return fmt.Errorf("invalid 'items' definition: %#v", y) 32 | } 33 | s, err := builder.Build(fmt.Sprintf("/items/%d", i), b) 34 | if err != nil { 35 | return err 36 | } 37 | l[i] = s 38 | } 39 | v.items = l 40 | 41 | default: 42 | return fmt.Errorf("invalid 'items' definition: %#v", y) 43 | 44 | } 45 | } 46 | 47 | if x, ok := builder.GetKeyword("additionalItems"); ok { 48 | switch y := x.(type) { 49 | 50 | case map[string]interface{}: 51 | s, err := builder.Build("/additionalItems", y) 52 | if err != nil { 53 | return err 54 | } 55 | v.additionalItem = s 56 | 57 | case bool: 58 | if !y { 59 | v.additionalItem = additionalItemsDenied 60 | } 61 | 62 | default: 63 | return fmt.Errorf("invalid 'additionalItems' definition: %#v", y) 64 | 65 | } 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (v *itemsValidator) Validate(x interface{}, ctx *Context) { 72 | y, ok := x.([]interface{}) 73 | if !ok || y == nil { 74 | return 75 | } 76 | 77 | if v.item != nil { 78 | for i, l := 0, len(y); i < l; i++ { 79 | newValue, err := ctx.ValidateValueWith(y[i], v.item) 80 | if err != nil { 81 | ctx.Report(&ErrInvalidItem{i, err}) 82 | } else { 83 | y[i] = newValue 84 | } 85 | } 86 | 87 | // no additionalItems 88 | 89 | return 90 | } 91 | 92 | if len(v.items) > 0 { 93 | var ( 94 | i = 0 95 | la = len(y) 96 | lb = len(v.items) 97 | ) 98 | 99 | for ; i < la && i < lb; i++ { 100 | newValue, err := ctx.ValidateValueWith(y[i], v.items[i]) 101 | if err != nil { 102 | ctx.Report(&ErrInvalidItem{i, err}) 103 | } else { 104 | y[i] = newValue 105 | } 106 | } 107 | 108 | // additionalItems 109 | if v.additionalItem == additionalItemsDenied { 110 | for ; i < la; i++ { 111 | ctx.Report(&ErrInvalidItem{i, fmt.Errorf("additional item is not allowed")}) 112 | } 113 | } else if v.additionalItem != nil { 114 | for ; i < la; i++ { 115 | newValue, err := ctx.ValidateValueWith(y[i], v.additionalItem) 116 | if err != nil { 117 | ctx.Report(&ErrInvalidItem{i, err}) 118 | } else { 119 | y[i] = newValue 120 | } 121 | } 122 | } 123 | 124 | return 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /testdata/draft4/properties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "object properties validation", 4 | "schema": { 5 | "properties": { 6 | "foo": {"type": "integer"}, 7 | "bar": {"type": "string"} 8 | } 9 | }, 10 | "tests": [ 11 | { 12 | "description": "both properties present and valid is valid", 13 | "data": {"foo": 1, "bar": "baz"}, 14 | "valid": true 15 | }, 16 | { 17 | "description": "one property invalid is invalid", 18 | "data": {"foo": 1, "bar": {}}, 19 | "valid": false 20 | }, 21 | { 22 | "description": "both properties invalid is invalid", 23 | "data": {"foo": [], "bar": {}}, 24 | "valid": false 25 | }, 26 | { 27 | "description": "doesn't invalidate other properties", 28 | "data": {"quux": []}, 29 | "valid": true 30 | }, 31 | { 32 | "description": "ignores non-objects", 33 | "data": [], 34 | "valid": true 35 | } 36 | ] 37 | }, 38 | { 39 | "description": 40 | "properties, patternProperties, additionalProperties interaction", 41 | "schema": { 42 | "properties": { 43 | "foo": {"type": "array", "maxItems": 3}, 44 | "bar": {"type": "array"} 45 | }, 46 | "patternProperties": {"f.o": {"minItems": 2}}, 47 | "additionalProperties": {"type": "integer"} 48 | }, 49 | "tests": [ 50 | { 51 | "description": "property validates property", 52 | "data": {"foo": [1, 2]}, 53 | "valid": true 54 | }, 55 | { 56 | "description": "property invalidates property", 57 | "data": {"foo": [1, 2, 3, 4]}, 58 | "valid": false 59 | }, 60 | { 61 | "description": "patternProperty invalidates property", 62 | "data": {"foo": []}, 63 | "valid": false 64 | }, 65 | { 66 | "description": "patternProperty validates nonproperty", 67 | "data": {"fxo": [1, 2]}, 68 | "valid": true 69 | }, 70 | { 71 | "description": "patternProperty invalidates nonproperty", 72 | "data": {"fxo": []}, 73 | "valid": false 74 | }, 75 | { 76 | "description": "additionalProperty ignores property", 77 | "data": {"bar": []}, 78 | "valid": true 79 | }, 80 | { 81 | "description": "additionalProperty validates others", 82 | "data": {"quux": 3}, 83 | "valid": true 84 | }, 85 | { 86 | "description": "additionalProperty invalidates others", 87 | "data": {"quux": "foo"}, 88 | "valid": false 89 | } 90 | ] 91 | } 92 | ] 93 | -------------------------------------------------------------------------------- /testdata/draft4/allOf.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "allOf", 4 | "schema": { 5 | "allOf": [ 6 | { 7 | "properties": { 8 | "bar": {"type": "integer"} 9 | }, 10 | "required": ["bar"] 11 | }, 12 | { 13 | "properties": { 14 | "foo": {"type": "string"} 15 | }, 16 | "required": ["foo"] 17 | } 18 | ] 19 | }, 20 | "tests": [ 21 | { 22 | "description": "allOf", 23 | "data": {"foo": "baz", "bar": 2}, 24 | "valid": true 25 | }, 26 | { 27 | "description": "mismatch second", 28 | "data": {"foo": "baz"}, 29 | "valid": false 30 | }, 31 | { 32 | "description": "mismatch first", 33 | "data": {"bar": 2}, 34 | "valid": false 35 | }, 36 | { 37 | "description": "wrong type", 38 | "data": {"foo": "baz", "bar": "quux"}, 39 | "valid": false 40 | } 41 | ] 42 | }, 43 | { 44 | "description": "allOf with base schema", 45 | "schema": { 46 | "properties": {"bar": {"type": "integer"}}, 47 | "required": ["bar"], 48 | "allOf" : [ 49 | { 50 | "properties": { 51 | "foo": {"type": "string"} 52 | }, 53 | "required": ["foo"] 54 | }, 55 | { 56 | "properties": { 57 | "baz": {"type": "null"} 58 | }, 59 | "required": ["baz"] 60 | } 61 | ] 62 | }, 63 | "tests": [ 64 | { 65 | "description": "valid", 66 | "data": {"foo": "quux", "bar": 2, "baz": null}, 67 | "valid": true 68 | }, 69 | { 70 | "description": "mismatch base schema", 71 | "data": {"foo": "quux", "baz": null}, 72 | "valid": false 73 | }, 74 | { 75 | "description": "mismatch first allOf", 76 | "data": {"bar": 2, "baz": null}, 77 | "valid": false 78 | }, 79 | { 80 | "description": "mismatch second allOf", 81 | "data": {"foo": "quux", "bar": 2}, 82 | "valid": false 83 | }, 84 | { 85 | "description": "mismatch both", 86 | "data": {"bar": 2}, 87 | "valid": false 88 | } 89 | ] 90 | }, 91 | { 92 | "description": "allOf simple types", 93 | "schema": { 94 | "allOf": [ 95 | {"maximum": 30}, 96 | {"minimum": 20} 97 | ] 98 | }, 99 | "tests": [ 100 | { 101 | "description": "valid", 102 | "data": 25, 103 | "valid": true 104 | }, 105 | { 106 | "description": "mismatch one", 107 | "data": 35, 108 | "valid": false 109 | } 110 | ] 111 | } 112 | ] 113 | -------------------------------------------------------------------------------- /testdata/draft4/dependencies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "dependencies", 4 | "schema": { 5 | "dependencies": {"bar": ["foo"]} 6 | }, 7 | "tests": [ 8 | { 9 | "description": "neither", 10 | "data": {}, 11 | "valid": true 12 | }, 13 | { 14 | "description": "nondependant", 15 | "data": {"foo": 1}, 16 | "valid": true 17 | }, 18 | { 19 | "description": "with dependency", 20 | "data": {"foo": 1, "bar": 2}, 21 | "valid": true 22 | }, 23 | { 24 | "description": "missing dependency", 25 | "data": {"bar": 2}, 26 | "valid": false 27 | }, 28 | { 29 | "description": "ignores non-objects", 30 | "data": "foo", 31 | "valid": true 32 | } 33 | ] 34 | }, 35 | { 36 | "description": "multiple dependencies", 37 | "schema": { 38 | "dependencies": {"quux": ["foo", "bar"]} 39 | }, 40 | "tests": [ 41 | { 42 | "description": "neither", 43 | "data": {}, 44 | "valid": true 45 | }, 46 | { 47 | "description": "nondependants", 48 | "data": {"foo": 1, "bar": 2}, 49 | "valid": true 50 | }, 51 | { 52 | "description": "with dependencies", 53 | "data": {"foo": 1, "bar": 2, "quux": 3}, 54 | "valid": true 55 | }, 56 | { 57 | "description": "missing dependency", 58 | "data": {"foo": 1, "quux": 2}, 59 | "valid": false 60 | }, 61 | { 62 | "description": "missing other dependency", 63 | "data": {"bar": 1, "quux": 2}, 64 | "valid": false 65 | }, 66 | { 67 | "description": "missing both dependencies", 68 | "data": {"quux": 1}, 69 | "valid": false 70 | } 71 | ] 72 | }, 73 | { 74 | "description": "multiple dependencies subschema", 75 | "schema": { 76 | "dependencies": { 77 | "bar": { 78 | "properties": { 79 | "foo": {"type": "integer"}, 80 | "bar": {"type": "integer"} 81 | } 82 | } 83 | } 84 | }, 85 | "tests": [ 86 | { 87 | "description": "valid", 88 | "data": {"foo": 1, "bar": 2}, 89 | "valid": true 90 | }, 91 | { 92 | "description": "no dependency", 93 | "data": {"foo": "quux"}, 94 | "valid": true 95 | }, 96 | { 97 | "description": "wrong type", 98 | "data": {"foo": "quux", "bar": 2}, 99 | "valid": false 100 | }, 101 | { 102 | "description": "wrong type other", 103 | "data": {"foo": 2, "bar": "quux"}, 104 | "valid": false 105 | }, 106 | { 107 | "description": "wrong type both", 108 | "data": {"foo": "quux", "bar": "quux"}, 109 | "valid": false 110 | } 111 | ] 112 | } 113 | ] 114 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Context struct { 8 | stack []contextStackFrame 9 | } 10 | 11 | type contextStackFrame struct { 12 | valueId int 13 | value interface{} 14 | errors []error 15 | schema *Schema 16 | } 17 | 18 | func newContext() *Context { 19 | return &Context{ 20 | stack: make([]contextStackFrame, 0, 8), 21 | } 22 | } 23 | 24 | func (c *Context) Report(err error) { 25 | l := len(c.stack) 26 | frame := &c.stack[l-1] 27 | frame.errors = append(frame.errors, err) 28 | } 29 | 30 | func (c *Context) UpdateValue(x interface{}) { 31 | l := len(c.stack) 32 | frame := &c.stack[l-1] 33 | frame.value = x 34 | } 35 | 36 | func (c *Context) CurrentSchema() *Schema { 37 | l := len(c.stack) 38 | frame := &c.stack[l-1] 39 | return frame.schema 40 | } 41 | 42 | func (c *Context) ValidateValueWith(x interface{}, schema *Schema) (interface{}, error) { 43 | l := len(c.stack) 44 | 45 | if l == cap(c.stack) { 46 | tmp := make([]contextStackFrame, l, l*2) 47 | copy(tmp, c.stack) 48 | c.stack = tmp 49 | } 50 | 51 | if schema.RefSchema != nil { 52 | return c.ValidateValueWith(x, schema.RefSchema) 53 | } 54 | 55 | var ( 56 | err error 57 | parentFrame *contextStackFrame 58 | valueId = 0 59 | ) 60 | 61 | if l > 0 { 62 | parentFrame = &c.stack[l-1] 63 | valueId = parentFrame.valueId + 1 64 | } 65 | 66 | // push stack frame 67 | c.stack = append(c.stack, contextStackFrame{ 68 | valueId: valueId, 69 | value: x, 70 | schema: schema, 71 | }) 72 | 73 | for _, validator := range schema.Validators { 74 | validator.Validate(c.stack[l].value, c) 75 | } 76 | 77 | frame := &c.stack[l] 78 | if len(frame.errors) > 0 { 79 | err = &ErrInvalidInstance{schema, frame.errors} 80 | } 81 | 82 | // pop stack frame 83 | c.stack = c.stack[:len(c.stack)-1] 84 | return frame.value, err 85 | } 86 | 87 | func (c *Context) ValidateSelfWith(schema *Schema) (interface{}, error) { 88 | l := len(c.stack) 89 | 90 | if l == cap(c.stack) { 91 | tmp := make([]contextStackFrame, l, l*2) 92 | copy(tmp, c.stack) 93 | c.stack = tmp 94 | } 95 | 96 | if l == 0 { 97 | return nil, fmt.Errorf("ValidateWith() cannot be a root frame") 98 | } 99 | 100 | if schema.RefSchema != nil { 101 | return c.ValidateSelfWith(schema.RefSchema) 102 | } 103 | 104 | var ( 105 | err error 106 | parentFrame = &c.stack[l-1] 107 | ) 108 | 109 | for i := l - 1; i >= 0; i-- { 110 | frame := &c.stack[i] 111 | if frame.valueId != parentFrame.valueId { 112 | break 113 | } 114 | if schema == frame.schema { 115 | return nil, fmt.Errorf("schema validation loops are invalid") 116 | } 117 | } 118 | 119 | // push stack frame 120 | c.stack = append(c.stack, contextStackFrame{ 121 | valueId: parentFrame.valueId, 122 | value: parentFrame.value, 123 | schema: schema, 124 | }) 125 | 126 | for _, validator := range schema.Validators { 127 | validator.Validate(c.stack[l].value, c) 128 | } 129 | 130 | frame := &c.stack[l] 131 | if len(frame.errors) > 0 { 132 | err = &ErrInvalidInstance{schema, frame.errors} 133 | } 134 | 135 | // pop stack frame 136 | c.stack = c.stack[:len(c.stack)-1] 137 | return frame.value, err 138 | } 139 | 140 | type PrimitiveType string 141 | 142 | const ( 143 | ArrayType = PrimitiveType("array") 144 | BooleanType = PrimitiveType("boolean") 145 | IntegerType = PrimitiveType("integer") 146 | NullType = PrimitiveType("null") 147 | NumberType = PrimitiveType("number") 148 | ObjectType = PrimitiveType("object") 149 | StringType = PrimitiveType("string") 150 | ) 151 | 152 | func (p PrimitiveType) Valid() bool { 153 | return p == ArrayType || 154 | p == BooleanType || 155 | p == IntegerType || 156 | p == NullType || 157 | p == NumberType || 158 | p == ObjectType || 159 | p == StringType 160 | } 161 | -------------------------------------------------------------------------------- /testdata/draft4/patternProperties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": 4 | "patternProperties validates properties matching a regex", 5 | "schema": { 6 | "patternProperties": { 7 | "f.*o": {"type": "integer"} 8 | } 9 | }, 10 | "tests": [ 11 | { 12 | "description": "a single valid match is valid", 13 | "data": {"foo": 1}, 14 | "valid": true 15 | }, 16 | { 17 | "description": "multiple valid matches is valid", 18 | "data": {"foo": 1, "foooooo" : 2}, 19 | "valid": true 20 | }, 21 | { 22 | "description": "a single invalid match is invalid", 23 | "data": {"foo": "bar", "fooooo": 2}, 24 | "valid": false 25 | }, 26 | { 27 | "description": "multiple invalid matches is invalid", 28 | "data": {"foo": "bar", "foooooo" : "baz"}, 29 | "valid": false 30 | }, 31 | { 32 | "description": "ignores non-objects", 33 | "data": 12, 34 | "valid": true 35 | } 36 | ] 37 | }, 38 | { 39 | "description": "multiple simultaneous patternProperties are validated", 40 | "schema": { 41 | "patternProperties": { 42 | "a*": {"type": "integer"}, 43 | "aaa*": {"maximum": 20} 44 | } 45 | }, 46 | "tests": [ 47 | { 48 | "description": "a single valid match is valid", 49 | "data": {"a": 21}, 50 | "valid": true 51 | }, 52 | { 53 | "description": "a simultaneous match is valid", 54 | "data": {"aaaa": 18}, 55 | "valid": true 56 | }, 57 | { 58 | "description": "multiple matches is valid", 59 | "data": {"a": 21, "aaaa": 18}, 60 | "valid": true 61 | }, 62 | { 63 | "description": "an invalid due to one is invalid", 64 | "data": {"a": "bar"}, 65 | "valid": false 66 | }, 67 | { 68 | "description": "an invalid due to the other is invalid", 69 | "data": {"aaaa": 31}, 70 | "valid": false 71 | }, 72 | { 73 | "description": "an invalid due to both is invalid", 74 | "data": {"aaa": "foo", "aaaa": 31}, 75 | "valid": false 76 | } 77 | ] 78 | }, 79 | { 80 | "description": "regexes are not anchored by default and are case sensitive", 81 | "schema": { 82 | "patternProperties": { 83 | "[0-9]{2,}": { "type": "boolean" }, 84 | "X_": { "type": "string" } 85 | } 86 | }, 87 | "tests": [ 88 | { 89 | "description": "non recognized members are ignored", 90 | "data": { "answer 1": "42" }, 91 | "valid": true 92 | }, 93 | { 94 | "description": "recognized members are accounted for", 95 | "data": { "a31b": null }, 96 | "valid": false 97 | }, 98 | { 99 | "description": "regexes are case sensitive", 100 | "data": { "a_x_3": 3 }, 101 | "valid": true 102 | }, 103 | { 104 | "description": "regexes are case sensitive, 2", 105 | "data": { "a_X_3": 3 }, 106 | "valid": false 107 | } 108 | ] 109 | } 110 | ] 111 | -------------------------------------------------------------------------------- /env.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "sort" 9 | ) 10 | 11 | type Env struct { 12 | Transport Transport 13 | 14 | schemas map[string]*Schema 15 | validators map[string]*validator 16 | formats map[string]FormatValidator 17 | } 18 | 19 | type Transport interface { 20 | Get(url string) ([]byte, error) 21 | } 22 | 23 | type validator struct { 24 | keywords []string 25 | priority int 26 | prototype reflect.Type 27 | } 28 | 29 | func NewEnv() *Env { 30 | return &Env{ 31 | schemas: map[string]*Schema{}, 32 | validators: map[string]*validator{}, 33 | formats: map[string]FormatValidator{}, 34 | } 35 | } 36 | 37 | func (e *Env) Clone() *Env { 38 | 39 | schemas := make(map[string]*Schema, len(e.schemas)) 40 | for k, v := range e.schemas { 41 | schemas[k] = v 42 | } 43 | 44 | validators := make(map[string]*validator, len(e.validators)) 45 | for k, v := range e.validators { 46 | validators[k] = v 47 | } 48 | 49 | formats := make(map[string]FormatValidator, len(e.formats)) 50 | for k, v := range e.formats { 51 | formats[k] = v 52 | } 53 | 54 | return &Env{ 55 | e.Transport, 56 | schemas, 57 | validators, 58 | formats, 59 | } 60 | } 61 | 62 | func (e *Env) RegisterKeyword(v Validator, priority int, key string, additionalKeys ...string) { 63 | keys := append(additionalKeys, key) 64 | sort.Strings(keys) 65 | 66 | for _, key := range keys { 67 | if _, found := e.validators[key]; found { 68 | panic("keyword is already registered") 69 | } 70 | } 71 | 72 | rt := reflect.TypeOf(v) 73 | if rt.Kind() != reflect.Ptr { 74 | panic("Validator must be a pointer") 75 | } 76 | 77 | validator := &validator{keys, priority, rt.Elem()} 78 | for _, key := range keys { 79 | e.validators[key] = validator 80 | } 81 | } 82 | 83 | func (e *Env) RegisterFormat(key string, v FormatValidator) { 84 | if _, found := e.formats[key]; found { 85 | panic("format is already registered") 86 | } 87 | e.formats[key] = v 88 | } 89 | 90 | func (e *Env) RegisterSchema(id string, data []byte) (*Schema, error) { 91 | schema, err := e.BuildSchema(id, data) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | e.schemas[normalizeRef(schema.Id.String())] = schema 97 | return schema, nil 98 | } 99 | 100 | func (e *Env) BuildSchema(id string, data []byte) (*Schema, error) { 101 | var ( 102 | obj map[string]interface{} 103 | superschema string 104 | ) 105 | 106 | dec := json.NewDecoder(bytes.NewReader(data)) 107 | dec.UseNumber() 108 | err := dec.Decode(&obj) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | if v, ok := obj["$schema"].(string); ok { 114 | superschema = normalizeRef(v) 115 | } 116 | 117 | if superschema == "" { 118 | superschema = "http://json-schema.org/draft-04/schema#" 119 | } 120 | 121 | if r, found := e.schemas[rootRef(superschema)]; found { 122 | if s, found := r.Subschemas[refFragment(superschema)]; found { 123 | err := s.Validate(obj) 124 | if err != nil { 125 | return nil, err 126 | } 127 | } 128 | } 129 | 130 | builder := newBuilder(e) 131 | schema, err := builder.Build(id, obj) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | err = builder.resolve() 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | if id != "" && normalizeRef(schema.Id.String()) != id { 142 | return nil, fmt.Errorf("schema id dit not match url (%q != %q)", id, schema.Id) 143 | } 144 | 145 | return schema, nil 146 | } 147 | 148 | func (e *Env) loadRemoteSchema(url string) (*Schema, error) { 149 | if e.Transport == nil { 150 | return nil, fmt.Errorf("remote schema loading is not enabled (missing transport)") 151 | } 152 | 153 | data, err := e.Transport.Get(url) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | return e.RegisterSchema("", data) 159 | } 160 | -------------------------------------------------------------------------------- /keyword_properties.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | type propertiesValidator struct { 9 | properties map[string]*Schema 10 | patterns []*patternProperty 11 | additionalProperties *Schema 12 | } 13 | 14 | type patternProperty struct { 15 | pattern string 16 | regexp *regexp.Regexp 17 | schema *Schema 18 | } 19 | 20 | var additionalPropertiesDenied = &Schema{} 21 | 22 | func (v *propertiesValidator) Setup(builder Builder) error { 23 | if x, found := builder.GetKeyword("properties"); found { 24 | defs, ok := x.(map[string]interface{}) 25 | if !ok { 26 | return fmt.Errorf("invalid 'properties' definition: %#v", x) 27 | } 28 | 29 | properties := make(map[string]*Schema, len(defs)) 30 | for k, y := range defs { 31 | mdef, ok := y.(map[string]interface{}) 32 | if !ok { 33 | return fmt.Errorf("invalid 'properties' definition: %#v", x) 34 | } 35 | 36 | schema, err := builder.Build("/properties/"+escapeJSONPointer(k), mdef) 37 | if err != nil { 38 | return err 39 | } 40 | properties[k] = schema 41 | } 42 | 43 | v.properties = properties 44 | } 45 | 46 | if x, found := builder.GetKeyword("patternProperties"); found { 47 | defs, ok := x.(map[string]interface{}) 48 | if !ok { 49 | return fmt.Errorf("invalid 'patternProperties' definition: %#v", x) 50 | } 51 | 52 | patterns := make([]*patternProperty, 0, len(defs)) 53 | for k, y := range defs { 54 | mdef, ok := y.(map[string]interface{}) 55 | if !ok { 56 | return fmt.Errorf("invalid 'patternProperties' definition: %#v", x) 57 | } 58 | 59 | reg, err := regexp.Compile(k) 60 | if err != nil { 61 | return fmt.Errorf("invalid 'patternProperties' definition: %#v (%s)", x, err) 62 | } 63 | 64 | schema, err := builder.Build("/patternProperties/"+escapeJSONPointer(k), mdef) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | patterns = append(patterns, &patternProperty{k, reg, schema}) 70 | } 71 | 72 | v.patterns = patterns 73 | } 74 | 75 | if x, ok := builder.GetKeyword("additionalProperties"); ok { 76 | switch y := x.(type) { 77 | 78 | case map[string]interface{}: 79 | s, err := builder.Build("/additionalProperties", y) 80 | if err != nil { 81 | return err 82 | } 83 | v.additionalProperties = s 84 | 85 | case bool: 86 | if !y { 87 | v.additionalProperties = additionalPropertiesDenied 88 | } 89 | 90 | default: 91 | return fmt.Errorf("invalid 'additionalProperties' definition: %#v", y) 92 | 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (v *propertiesValidator) Validate(x interface{}, ctx *Context) { 100 | y, ok := x.(map[string]interface{}) 101 | if !ok || y == nil { 102 | return 103 | } 104 | 105 | for k, m := range y { 106 | additional := true 107 | 108 | if schema, found := v.properties[k]; found { 109 | additional = false 110 | newValue, err := ctx.ValidateValueWith(m, schema) 111 | if err != nil { 112 | ctx.Report(&ErrInvalidProperty{k, err}) 113 | } else { 114 | m = newValue 115 | y[k] = newValue 116 | } 117 | } 118 | 119 | for _, pattern := range v.patterns { 120 | if pattern.regexp.MatchString(k) { 121 | additional = false 122 | newValue, err := ctx.ValidateValueWith(m, pattern.schema) 123 | if err != nil { 124 | ctx.Report(&ErrInvalidProperty{k, err}) 125 | } else { 126 | m = newValue 127 | y[k] = newValue 128 | } 129 | } 130 | } 131 | 132 | if additional { 133 | if v.additionalProperties == additionalPropertiesDenied { 134 | ctx.Report(&ErrInvalidProperty{k, fmt.Errorf("additional property is not allowed")}) 135 | } else if v.additionalProperties != nil { 136 | newValue, err := ctx.ValidateValueWith(m, v.additionalProperties) 137 | if err != nil { 138 | ctx.Report(&ErrInvalidProperty{k, err}) 139 | } else { 140 | m = newValue 141 | y[k] = newValue 142 | } 143 | } 144 | } 145 | 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /testdata/core.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://json-schema.org/draft-04/schema#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "description": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "positiveInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "positiveIntegerDefault0": { 16 | "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] 17 | }, 18 | "simpleTypes": { 19 | "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] 20 | }, 21 | "stringArray": { 22 | "type": "array", 23 | "items": { "type": "string" }, 24 | "minItems": 1, 25 | "uniqueItems": true 26 | } 27 | }, 28 | "type": "object", 29 | "properties": { 30 | "id": { 31 | "type": "string", 32 | "format": "uri" 33 | }, 34 | "$schema": { 35 | "type": "string", 36 | "format": "uri" 37 | }, 38 | "title": { 39 | "type": "string" 40 | }, 41 | "description": { 42 | "type": "string" 43 | }, 44 | "default": {}, 45 | "multipleOf": { 46 | "type": "number", 47 | "minimum": 0, 48 | "exclusiveMinimum": true 49 | }, 50 | "maximum": { 51 | "type": "number" 52 | }, 53 | "exclusiveMaximum": { 54 | "type": "boolean", 55 | "default": false 56 | }, 57 | "minimum": { 58 | "type": "number" 59 | }, 60 | "exclusiveMinimum": { 61 | "type": "boolean", 62 | "default": false 63 | }, 64 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 65 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 66 | "pattern": { 67 | "type": "string", 68 | "format": "regex" 69 | }, 70 | "additionalItems": { 71 | "anyOf": [ 72 | { "type": "boolean" }, 73 | { "$ref": "#" } 74 | ], 75 | "default": {} 76 | }, 77 | "items": { 78 | "anyOf": [ 79 | { "$ref": "#" }, 80 | { "$ref": "#/definitions/schemaArray" } 81 | ], 82 | "default": {} 83 | }, 84 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 85 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 86 | "uniqueItems": { 87 | "type": "boolean", 88 | "default": false 89 | }, 90 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 91 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 92 | "required": { "$ref": "#/definitions/stringArray" }, 93 | "additionalProperties": { 94 | "anyOf": [ 95 | { "type": "boolean" }, 96 | { "$ref": "#" } 97 | ], 98 | "default": {} 99 | }, 100 | "definitions": { 101 | "type": "object", 102 | "additionalProperties": { "$ref": "#" }, 103 | "default": {} 104 | }, 105 | "properties": { 106 | "type": "object", 107 | "additionalProperties": { "$ref": "#" }, 108 | "default": {} 109 | }, 110 | "patternProperties": { 111 | "type": "object", 112 | "additionalProperties": { "$ref": "#" }, 113 | "default": {} 114 | }, 115 | "dependencies": { 116 | "type": "object", 117 | "additionalProperties": { 118 | "anyOf": [ 119 | { "$ref": "#" }, 120 | { "$ref": "#/definitions/stringArray" } 121 | ] 122 | } 123 | }, 124 | "enum": { 125 | "type": "array", 126 | "minItems": 1, 127 | "uniqueItems": true 128 | }, 129 | "type": { 130 | "anyOf": [ 131 | { "$ref": "#/definitions/simpleTypes" }, 132 | { 133 | "type": "array", 134 | "items": { "$ref": "#/definitions/simpleTypes" }, 135 | "minItems": 1, 136 | "uniqueItems": true 137 | } 138 | ] 139 | }, 140 | "allOf": { "$ref": "#/definitions/schemaArray" }, 141 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 142 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 143 | "not": { "$ref": "#" } 144 | }, 145 | "dependencies": { 146 | "exclusiveMaximum": [ "maximum" ], 147 | "exclusiveMinimum": [ "minimum" ] 148 | }, 149 | "default": {} 150 | } 151 | -------------------------------------------------------------------------------- /testdata/draft4/ref.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "root pointer ref", 4 | "schema": { 5 | "properties": { 6 | "foo": {"$ref": "#"} 7 | }, 8 | "additionalProperties": false 9 | }, 10 | "tests": [ 11 | { 12 | "description": "match", 13 | "data": {"foo": false}, 14 | "valid": true 15 | }, 16 | { 17 | "description": "recursive match", 18 | "data": {"foo": {"foo": false}}, 19 | "valid": true 20 | }, 21 | { 22 | "description": "mismatch", 23 | "data": {"bar": false}, 24 | "valid": false 25 | }, 26 | { 27 | "description": "recursive mismatch", 28 | "data": {"foo": {"bar": false}}, 29 | "valid": false 30 | } 31 | ] 32 | }, 33 | { 34 | "description": "relative pointer ref to object", 35 | "schema": { 36 | "properties": { 37 | "foo": {"type": "integer"}, 38 | "bar": {"$ref": "#/properties/foo"} 39 | } 40 | }, 41 | "tests": [ 42 | { 43 | "description": "match", 44 | "data": {"bar": 3}, 45 | "valid": true 46 | }, 47 | { 48 | "description": "mismatch", 49 | "data": {"bar": true}, 50 | "valid": false 51 | } 52 | ] 53 | }, 54 | { 55 | "description": "relative pointer ref to array", 56 | "schema": { 57 | "items": [ 58 | {"type": "integer"}, 59 | {"$ref": "#/items/0"} 60 | ] 61 | }, 62 | "tests": [ 63 | { 64 | "description": "match array", 65 | "data": [1, 2], 66 | "valid": true 67 | }, 68 | { 69 | "description": "mismatch array", 70 | "data": [1, "foo"], 71 | "valid": false 72 | } 73 | ] 74 | }, 75 | { 76 | "description": "escaped pointer ref", 77 | "schema": { 78 | "tilda~field": {"type": "integer"}, 79 | "slash/field": {"type": "integer"}, 80 | "percent%field": {"type": "integer"}, 81 | "properties": { 82 | "tilda": {"$ref": "#/tilda~0field"}, 83 | "slash": {"$ref": "#/slash~1field"}, 84 | "percent": {"$ref": "#/percent%25field"} 85 | } 86 | }, 87 | "tests": [ 88 | { 89 | "description": "slash", 90 | "data": {"slash": "aoeu"}, 91 | "valid": false 92 | }, 93 | { 94 | "description": "tilda", 95 | "data": {"tilda": "aoeu"}, 96 | "valid": false 97 | }, 98 | { 99 | "description": "percent", 100 | "data": {"percent": "aoeu"}, 101 | "valid": false 102 | } 103 | ] 104 | }, 105 | { 106 | "description": "nested refs", 107 | "schema": { 108 | "definitions": { 109 | "a": {"type": "integer"}, 110 | "b": {"$ref": "#/definitions/a"}, 111 | "c": {"$ref": "#/definitions/b"} 112 | }, 113 | "$ref": "#/definitions/c" 114 | }, 115 | "tests": [ 116 | { 117 | "description": "nested ref valid", 118 | "data": 5, 119 | "valid": true 120 | }, 121 | { 122 | "description": "nested ref invalid", 123 | "data": "a", 124 | "valid": false 125 | } 126 | ] 127 | }, 128 | { 129 | "description": "remote ref, containing refs itself", 130 | "schema": {"$ref": "http://json-schema.org/draft-04/schema#"}, 131 | "tests": [ 132 | { 133 | "description": "remote ref valid", 134 | "data": {"minLength": 1}, 135 | "valid": true 136 | }, 137 | { 138 | "description": "remote ref invalid", 139 | "data": {"minLength": -1}, 140 | "valid": false 141 | } 142 | ] 143 | } 144 | ] 145 | -------------------------------------------------------------------------------- /schema_test.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/url" 8 | "path" 9 | "testing" 10 | ) 11 | 12 | func TestCoreIdenitySchema(t *testing.T) { 13 | var def map[string]interface{} 14 | load_test_json("core.json", &def) 15 | 16 | schema, err := RootEnv.BuildSchema("", load_test_data("core.json")) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | err = schema.Validate(def) 22 | if err != nil { 23 | t.Fatalf("error: %s", err) 24 | } 25 | } 26 | 27 | func TestDraft4(t *testing.T) { 28 | run_test_suite(t, "draft4/additionalItems.json") 29 | run_test_suite(t, "draft4/additionalProperties.json") 30 | run_test_suite(t, "draft4/allOf.json") 31 | run_test_suite(t, "draft4/anyOf.json") 32 | run_test_suite(t, "draft4/definitions.json") 33 | run_test_suite(t, "draft4/dependencies.json") 34 | run_test_suite(t, "draft4/enum.json") 35 | run_test_suite(t, "draft4/items.json") 36 | run_test_suite(t, "draft4/maxItems.json") 37 | run_test_suite(t, "draft4/maxLength.json") 38 | run_test_suite(t, "draft4/maxProperties.json") 39 | run_test_suite(t, "draft4/maximum.json") 40 | run_test_suite(t, "draft4/minItems.json") 41 | run_test_suite(t, "draft4/minLength.json") 42 | run_test_suite(t, "draft4/minProperties.json") 43 | run_test_suite(t, "draft4/minimum.json") 44 | run_test_suite(t, "draft4/multipleOf.json") 45 | run_test_suite(t, "draft4/not.json") 46 | run_test_suite(t, "draft4/oneOf.json") 47 | run_test_suite(t, "draft4/pattern.json") 48 | run_test_suite(t, "draft4/patternProperties.json") 49 | run_test_suite(t, "draft4/properties.json") 50 | run_test_suite(t, "draft4/ref.json") 51 | run_test_suite(t, "draft4/refRemote.json") 52 | run_test_suite(t, "draft4/required.json") 53 | run_test_suite(t, "draft4/type.json") 54 | run_test_suite(t, "draft4/uniqueItems.json") 55 | } 56 | 57 | func TestDraft4Optional(t *testing.T) { 58 | // run_test_suite(t, "draft4/optional/bignum.json") 59 | run_test_suite(t, "draft4/optional/format.json") 60 | run_test_suite(t, "draft4/optional/zeroTerminatedFloats.json") 61 | } 62 | 63 | func load_test_data(path string) []byte { 64 | data, err := ioutil.ReadFile("testdata/" + path) 65 | if err != nil { 66 | panic(err) 67 | } 68 | return data 69 | } 70 | 71 | func load_test_json(path string, v interface{}) { 72 | dec := json.NewDecoder(bytes.NewReader(load_test_data(path))) 73 | dec.UseNumber() 74 | err := dec.Decode(v) 75 | if err != nil { 76 | panic(err) 77 | } 78 | } 79 | 80 | func run_test_suite(t *testing.T, path string) { 81 | t.Logf("- %s", path) 82 | 83 | var suite []struct { 84 | Description string `json:"description"` 85 | SchemaDef json.RawMessage `json:"schema"` 86 | Tests []struct { 87 | Description string `json:"description"` 88 | Data interface{} `json:"data"` 89 | Valid bool `json:"valid"` 90 | } 91 | } 92 | 93 | load_test_json(path, &suite) 94 | 95 | var ( 96 | passed int 97 | failed int 98 | ) 99 | 100 | env := RootEnv.Clone() 101 | env.Transport = &testTransport{} 102 | 103 | for _, group := range suite { 104 | t.Logf(" - %s:", group.Description) 105 | 106 | schema, err := env.BuildSchema("", group.SchemaDef) 107 | if err != nil { 108 | failed++ 109 | t.Errorf(" error: %s", err) 110 | continue 111 | } 112 | 113 | for _, test := range group.Tests { 114 | err := schema.Validate(test.Data) 115 | if test.Valid && err == nil { 116 | passed++ 117 | t.Logf(" \x1B[32m✓\x1B[0m %s", test.Description) 118 | } else if !test.Valid && err != nil { 119 | passed++ 120 | t.Logf(" \x1B[32m✓\x1B[0m %s", test.Description) 121 | } else if test.Valid && err != nil { 122 | failed++ 123 | t.Logf(" \x1B[31m✗\x1B[0m %s", test.Description) 124 | t.Errorf(" error: %s", err) 125 | } else if !test.Valid && err == nil { 126 | failed++ 127 | t.Logf(" \x1B[31m✗\x1B[0m %s", test.Description) 128 | t.Errorf(" error: %s", "expected an error but non were generated") 129 | } 130 | } 131 | } 132 | 133 | if failed > 0 { 134 | t.Logf("(\x1B[32mpassed: %d\x1B[0m \x1B[31mfaild: %d\x1B[0m)", passed, failed) 135 | } else { 136 | t.Logf("(\x1B[32mpassed: %d\x1B[0m)", passed) 137 | } 138 | } 139 | 140 | type testTransport struct{} 141 | 142 | func (t *testTransport) Get(rawurl string) ([]byte, error) { 143 | u, err := url.Parse(rawurl) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | data, err := ioutil.ReadFile(path.Join("testdata/draft4/remotes", u.Path)) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | return data, nil 154 | } 155 | -------------------------------------------------------------------------------- /testdata/draft4/optional/format.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "validation of date-time strings", 4 | "schema": {"format": "date-time"}, 5 | "tests": [ 6 | { 7 | "description": "a valid date-time string", 8 | "data": "1963-06-19T08:30:06.283185Z", 9 | "valid": true 10 | }, 11 | { 12 | "description": "an invalid date-time string", 13 | "data": "06/19/1963 08:30:06 PST", 14 | "valid": false 15 | }, 16 | { 17 | "description": "only RFC3339 not all of ISO 8601 are valid", 18 | "data": "2013-350T01:01:01", 19 | "valid": false 20 | } 21 | ] 22 | }, 23 | { 24 | "description": "validation of URIs", 25 | "schema": {"format": "uri"}, 26 | "tests": [ 27 | { 28 | "description": "a valid URI", 29 | "data": "http://foo.bar/?baz=qux#quux", 30 | "valid": true 31 | }, 32 | { 33 | "description": "an invalid URI", 34 | "data": "\\\\WINDOWS\\fileshare", 35 | "valid": false 36 | }, 37 | { 38 | "description": "an invalid URI though valid URI reference", 39 | "data": "abc", 40 | "valid": false 41 | }, 42 | { 43 | "description": "a valid URI with percentage encode path components", 44 | "data": "http://example.com/foo%20bar", 45 | "valid": true 46 | } 47 | ] 48 | }, 49 | { 50 | "description": "validation of e-mail addresses", 51 | "schema": {"format": "email"}, 52 | "tests": [ 53 | { 54 | "description": "a valid e-mail address", 55 | "data": "joe.bloggs@example.com", 56 | "valid": true 57 | }, 58 | { 59 | "description": "an invalid e-mail address", 60 | "data": "2962", 61 | "valid": false 62 | } 63 | ] 64 | }, 65 | { 66 | "description": "validation of IP addresses", 67 | "schema": {"format": "ipv4"}, 68 | "tests": [ 69 | { 70 | "description": "a valid IP address", 71 | "data": "192.168.0.1", 72 | "valid": true 73 | }, 74 | { 75 | "description": "an IP address with too many components", 76 | "data": "127.0.0.0.1", 77 | "valid": false 78 | }, 79 | { 80 | "description": "an IP address with out-of-range values", 81 | "data": "256.256.256.256", 82 | "valid": false 83 | }, 84 | { 85 | "description": "an IP address without 4 components", 86 | "data": "127.0", 87 | "valid": false 88 | }, 89 | { 90 | "description": "an IP address as an integer", 91 | "data": "0x7f000001", 92 | "valid": false 93 | } 94 | ] 95 | }, 96 | { 97 | "description": "validation of IPv6 addresses", 98 | "schema": {"format": "ipv6"}, 99 | "tests": [ 100 | { 101 | "description": "a valid IPv6 address", 102 | "data": "::1", 103 | "valid": true 104 | }, 105 | { 106 | "description": "an IPv6 address with out-of-range values", 107 | "data": "12345::", 108 | "valid": false 109 | }, 110 | { 111 | "description": "an IPv6 address with too many components", 112 | "data": "1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1", 113 | "valid": false 114 | }, 115 | { 116 | "description": "an IPv6 address containing illegal characters", 117 | "data": "::laptop", 118 | "valid": false 119 | } 120 | ] 121 | }, 122 | { 123 | "description": "validation of host names", 124 | "schema": {"format": "hostname"}, 125 | "tests": [ 126 | { 127 | "description": "a valid host name", 128 | "data": "www.example.com", 129 | "valid": true 130 | }, 131 | { 132 | "description": "a host name starting with an illegal character", 133 | "data": "-a-host-name-that-starts-with--", 134 | "valid": false 135 | }, 136 | { 137 | "description": "a host name containing illegal characters", 138 | "data": "not_a_valid_host_name", 139 | "valid": false 140 | }, 141 | { 142 | "description": "a host name with a component too long", 143 | "data": "a-vvvvvvvvvvvvvvvveeeeeeeeeeeeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyyyyy-long-host-name-component", 144 | "valid": false 145 | } 146 | ] 147 | } 148 | ] 149 | -------------------------------------------------------------------------------- /env_draft4.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | var RootEnv = NewEnv() 4 | 5 | func init() { 6 | // any 7 | RootEnv.RegisterKeyword(&typeValidator{}, 100, "type") 8 | RootEnv.RegisterKeyword(&enumValidator{}, 101, "enum") 9 | RootEnv.RegisterKeyword(&anyOfValidator{}, 102, "anyOf") 10 | RootEnv.RegisterKeyword(&allOfValidator{}, 103, "allOf") 11 | RootEnv.RegisterKeyword(&oneOfValidator{}, 104, "oneOf") 12 | RootEnv.RegisterKeyword(¬Validator{}, 105, "not") 13 | RootEnv.RegisterKeyword(&definitionsValidator{}, 106, "definitions") 14 | RootEnv.RegisterKeyword(&formatValidator{}, 107, "format") 15 | 16 | // numbers 17 | RootEnv.RegisterKeyword(&multipleOfValidator{}, 200, "multipleOf") 18 | RootEnv.RegisterKeyword(&maximumValidator{}, 201, "maximum", "exclusiveMaximum") 19 | RootEnv.RegisterKeyword(&minimumValidator{}, 202, "minimum", "exclusiveMinimum") 20 | 21 | // strings 22 | RootEnv.RegisterKeyword(&maxLengthValidator{}, 300, "maxLength") 23 | RootEnv.RegisterKeyword(&minLengthValidator{}, 301, "minLength") 24 | RootEnv.RegisterKeyword(&patternValidator{}, 302, "pattern") 25 | 26 | // arrays 27 | RootEnv.RegisterKeyword(&itemsValidator{}, 400, "items", "additionalItems") 28 | RootEnv.RegisterKeyword(&maxItemsValidator{}, 401, "maxItems") 29 | RootEnv.RegisterKeyword(&minItemsValidator{}, 402, "minItems") 30 | RootEnv.RegisterKeyword(&uniqueItemsValidator{}, 403, "uniqueItems") 31 | 32 | // objects 33 | RootEnv.RegisterKeyword(&maxPropertiesValidator{}, 500, "maxProperties") 34 | RootEnv.RegisterKeyword(&minPropertiesValidator{}, 501, "minProperties") 35 | RootEnv.RegisterKeyword(&requiredValidator{}, 502, "required") 36 | RootEnv.RegisterKeyword(&propertiesValidator{}, 503, "properties", "patternProperties", "additionalProperties") 37 | RootEnv.RegisterKeyword(&dependenciesValidator{}, 504, "dependencies") 38 | 39 | RootEnv.RegisterFormat("date-time", &datetimeFormat{}) 40 | RootEnv.RegisterFormat("email", &emailFormat{}) 41 | RootEnv.RegisterFormat("hostname", &hostnameFormat{}) 42 | RootEnv.RegisterFormat("ipv4", &ipv4Format{}) 43 | RootEnv.RegisterFormat("ipv6", &ipv6Format{}) 44 | RootEnv.RegisterFormat("regex", ®exFormat{}) 45 | RootEnv.RegisterFormat("uri", &uriFormat{}) 46 | RootEnv.RegisterFormat("uri-reference", &uriReferenceFormat{}) 47 | 48 | // Set the root Schema 49 | schema, err := RootEnv.RegisterSchema("", draft4) 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | err = schema.ValidateData(draft4) 55 | if err != nil { 56 | panic(err) 57 | } 58 | } 59 | 60 | var draft4 = []byte(` 61 | { 62 | "id": "http://json-schema.org/draft-04/schema#", 63 | "$schema": "http://json-schema.org/draft-04/schema#", 64 | "description": "Core schema meta-schema", 65 | "definitions": { 66 | "schemaArray": { 67 | "type": "array", 68 | "minItems": 1, 69 | "items": { "$ref": "#" } 70 | }, 71 | "positiveInteger": { 72 | "type": "integer", 73 | "minimum": 0 74 | }, 75 | "positiveIntegerDefault0": { 76 | "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] 77 | }, 78 | "simpleTypes": { 79 | "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] 80 | }, 81 | "stringArray": { 82 | "type": "array", 83 | "items": { "type": "string" }, 84 | "minItems": 1, 85 | "uniqueItems": true 86 | } 87 | }, 88 | "type": "object", 89 | "properties": { 90 | "id": { 91 | "type": "string", 92 | "format": "uri-reference" 93 | }, 94 | "$schema": { 95 | "type": "string", 96 | "format": "uri" 97 | }, 98 | "title": { 99 | "type": "string" 100 | }, 101 | "description": { 102 | "type": "string" 103 | }, 104 | "default": {}, 105 | "multipleOf": { 106 | "type": "number", 107 | "minimum": 0, 108 | "exclusiveMinimum": true 109 | }, 110 | "maximum": { 111 | "type": "number" 112 | }, 113 | "exclusiveMaximum": { 114 | "type": "boolean", 115 | "default": false 116 | }, 117 | "minimum": { 118 | "type": "number" 119 | }, 120 | "exclusiveMinimum": { 121 | "type": "boolean", 122 | "default": false 123 | }, 124 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 125 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 126 | "pattern": { 127 | "type": "string", 128 | "format": "regex" 129 | }, 130 | "additionalItems": { 131 | "anyOf": [ 132 | { "type": "boolean" }, 133 | { "$ref": "#" } 134 | ], 135 | "default": {} 136 | }, 137 | "items": { 138 | "anyOf": [ 139 | { "$ref": "#" }, 140 | { "$ref": "#/definitions/schemaArray" } 141 | ], 142 | "default": {} 143 | }, 144 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 145 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 146 | "uniqueItems": { 147 | "type": "boolean", 148 | "default": false 149 | }, 150 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 151 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 152 | "required": { "$ref": "#/definitions/stringArray" }, 153 | "additionalProperties": { 154 | "anyOf": [ 155 | { "type": "boolean" }, 156 | { "$ref": "#" } 157 | ], 158 | "default": {} 159 | }, 160 | "definitions": { 161 | "type": "object", 162 | "additionalProperties": { "$ref": "#" }, 163 | "default": {} 164 | }, 165 | "properties": { 166 | "type": "object", 167 | "additionalProperties": { "$ref": "#" }, 168 | "default": {} 169 | }, 170 | "patternProperties": { 171 | "type": "object", 172 | "additionalProperties": { "$ref": "#" }, 173 | "default": {} 174 | }, 175 | "dependencies": { 176 | "type": "object", 177 | "additionalProperties": { 178 | "anyOf": [ 179 | { "$ref": "#" }, 180 | { "$ref": "#/definitions/stringArray" } 181 | ] 182 | } 183 | }, 184 | "enum": { 185 | "type": "array", 186 | "minItems": 1, 187 | "uniqueItems": true 188 | }, 189 | "type": { 190 | "anyOf": [ 191 | { "$ref": "#/definitions/simpleTypes" }, 192 | { 193 | "type": "array", 194 | "items": { "$ref": "#/definitions/simpleTypes" }, 195 | "minItems": 1, 196 | "uniqueItems": true 197 | } 198 | ] 199 | }, 200 | "allOf": { "$ref": "#/definitions/schemaArray" }, 201 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 202 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 203 | "not": { "$ref": "#" } 204 | }, 205 | "dependencies": { 206 | "exclusiveMaximum": [ "maximum" ], 207 | "exclusiveMinimum": [ "minimum" ] 208 | }, 209 | "default": {} 210 | } 211 | `) 212 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | func init() { 10 | var err Error 11 | // err = &ErrInvalidDependency{} 12 | // err = &ErrInvalidEnum{} 13 | // err = &ErrInvalidFormat{} 14 | // err = &ErrInvalidInstance{} 15 | // err = &ErrInvalidItem{} 16 | // err = &ErrInvalidPattern{} 17 | // err = &ErrInvalidProperty{} 18 | // err = &ErrInvalidType{} 19 | err = &ErrNotAllOf{} 20 | // err = &ErrNotAnyOf{} 21 | // err = &ErrNotMultipleOf{} 22 | // err = &ErrNotNot{} 23 | // err = &ErrNotOneOf{} 24 | // err = &ErrNotUnique{} 25 | // err = &ErrRequiredProperty{} 26 | // err = &ErrTooLarge{} 27 | // err = &ErrTooLong{} 28 | // err = &ErrTooShort{} 29 | // err = &ErrTooSmall{} 30 | _ = err 31 | } 32 | 33 | type Error interface { 34 | Error() string 35 | Value() interface{} 36 | Keyword() string 37 | Schema() *Schema 38 | } 39 | 40 | // ErrNotAllOf is returned when a `allOf` keyword failed. 41 | type ErrNotAllOf struct { 42 | value interface{} 43 | schema *Schema 44 | subschemas []*Schema 45 | errors []error 46 | } 47 | 48 | func (e *ErrNotAllOf) Schema() *Schema { return e.schema } 49 | func (e *ErrNotAllOf) Value() interface{} { return e.value } 50 | func (e *ErrNotAllOf) Keyword() string { return "allOf" } 51 | 52 | func (e *ErrNotAllOf) Error() string { 53 | var buf bytes.Buffer 54 | 55 | fmt.Fprintf(&buf, "value must be all of:") 56 | 57 | for i, schema := range e.subschemas { 58 | var ( 59 | err = e.errors[i] 60 | errstr = "" 61 | ) 62 | 63 | if err != nil { 64 | errstr = strings.Replace(err.Error(), "\n", "\n ", -1) 65 | } 66 | 67 | fmt.Fprintf(&buf, "\n- schema: %v\n error:\n %v", schema.Id.String(), errstr) 68 | } 69 | 70 | return buf.String() 71 | } 72 | 73 | // ErrNotAnyOf is returned when a `anyOf` keyword failed. 74 | type ErrNotAnyOf struct { 75 | Value interface{} 76 | Schemas []*Schema 77 | Errors []error 78 | } 79 | 80 | func (e *ErrNotAnyOf) Error() string { 81 | var buf bytes.Buffer 82 | 83 | fmt.Fprintf(&buf, "value must be any of:") 84 | 85 | for i, schema := range e.Schemas { 86 | var ( 87 | err = e.Errors[i] 88 | errstr = "" 89 | ) 90 | 91 | if err != nil { 92 | errstr = strings.Replace(err.Error(), "\n", "\n ", -1) 93 | } 94 | 95 | fmt.Fprintf(&buf, "\n- schema: %v\n error:\n %v", schema, errstr) 96 | } 97 | 98 | return buf.String() 99 | } 100 | 101 | // ErrInvalidDependency is returned when a `dependency` keyword failed. 102 | type ErrInvalidDependency struct { 103 | Property string 104 | Dependency string 105 | Schema *Schema 106 | Err error 107 | } 108 | 109 | func (e *ErrInvalidDependency) Error() string { 110 | if e.Schema != nil { 111 | return fmt.Sprintf("Invalid property %q: faile to validate dependecy: %s", e.Property, e.Err) 112 | } else { 113 | return fmt.Sprintf("Invalid property %q: missing property: %q", e.Property, e.Dependency) 114 | } 115 | } 116 | 117 | // ErrInvalidEnum is returned when a `enum` keyword failed. 118 | type ErrInvalidEnum struct { 119 | Expected []interface{} 120 | Value interface{} 121 | } 122 | 123 | func (e *ErrInvalidEnum) Error() string { 124 | return fmt.Sprintf("%v must be in %v", e.Value, e.Expected) 125 | } 126 | 127 | // ErrInvalidFormat is returned when a `format` keyword failed. 128 | type ErrInvalidFormat struct { 129 | Value interface{} 130 | Format string 131 | } 132 | 133 | func (e *ErrInvalidFormat) Error() string { 134 | return fmt.Sprintf("%#v did not match format '%s'", e.Value, e.Format) 135 | } 136 | 137 | // ErrInvalidItem is returned when a `item` keyword failed. 138 | type ErrInvalidItem struct { 139 | Index int 140 | Err error 141 | } 142 | 143 | func (e *ErrInvalidItem) Error() string { 144 | return fmt.Sprintf("Invalid item at %v: %s", e.Index, e.Err) 145 | } 146 | 147 | // ErrTooLarge is returned when a `maximum` keyword failed. 148 | type ErrTooLarge struct { 149 | max float64 150 | exclusive bool 151 | was interface{} 152 | } 153 | 154 | func (e *ErrTooLarge) Error() string { 155 | if e.exclusive { 156 | return fmt.Sprintf("expected %#v to be smaller than %v", e.was, e.max) 157 | } else { 158 | return fmt.Sprintf("expected %#v to be smaller than or equal to %v", e.was, e.max) 159 | } 160 | } 161 | 162 | // ErrTooLong is returned when a `maxLength`, `maxItems` or a `maxProperties` keyword failed. 163 | type ErrTooLong struct { 164 | max int 165 | was interface{} 166 | } 167 | 168 | func (e *ErrTooLong) Error() string { 169 | return fmt.Sprintf("expected len(%#v) to be smaller than %v", e.was, e.max) 170 | } 171 | 172 | // ErrTooSmall is returned when a `minimum` keyword failed. 173 | type ErrTooSmall struct { 174 | min float64 175 | exclusive bool 176 | was interface{} 177 | } 178 | 179 | func (e *ErrTooSmall) Error() string { 180 | if e.exclusive { 181 | return fmt.Sprintf("expected %#v to be larger than %v", e.was, e.min) 182 | } else { 183 | return fmt.Sprintf("expected %#v to be larger than or equal to %v", e.was, e.min) 184 | } 185 | } 186 | 187 | // ErrTooShort is returned when a `minLength`, `minItems` or a `minProperties` keyword failed. 188 | type ErrTooShort struct { 189 | min int 190 | was interface{} 191 | } 192 | 193 | func (e *ErrTooShort) Error() string { 194 | return fmt.Sprintf("expected len(%#v) to be larger than %v", e.was, e.min) 195 | } 196 | 197 | // ErrNotMultipleOf is returned when a `multipleOf` keyword failed. 198 | type ErrNotMultipleOf struct { 199 | factor float64 200 | was interface{} 201 | } 202 | 203 | func (e *ErrNotMultipleOf) Error() string { 204 | return fmt.Sprintf("expected %#v to be a multiple of %v", e.was, e.factor) 205 | } 206 | 207 | // ErrNotNot is returned when a `not` keyword failed. 208 | type ErrNotNot struct { 209 | Value interface{} 210 | Schema *Schema 211 | } 212 | 213 | func (e *ErrNotNot) Error() string { 214 | return fmt.Sprintf("value must not be valid for: %v", e.Schema) 215 | } 216 | 217 | // ErrNotOneOf is returned when a `oneOf` keyword failed. 218 | type ErrNotOneOf struct { 219 | Value interface{} 220 | Schemas []*Schema 221 | Errors []error 222 | } 223 | 224 | func (e *ErrNotOneOf) Error() string { 225 | var buf bytes.Buffer 226 | 227 | fmt.Fprintf(&buf, "value must be one of:") 228 | 229 | for i, schema := range e.Schemas { 230 | var ( 231 | err = e.Errors[i] 232 | errstr = "" 233 | ) 234 | 235 | if err != nil { 236 | errstr = strings.Replace(err.Error(), "\n", "\n ", -1) 237 | } 238 | 239 | fmt.Fprintf(&buf, "\n- schema: %v\n error:\n %v", schema, errstr) 240 | } 241 | 242 | return buf.String() 243 | } 244 | 245 | // ErrInvalidPattern is returned when a `pattern` keyword failed. 246 | type ErrInvalidPattern struct { 247 | pattern string 248 | was string 249 | } 250 | 251 | func (e *ErrInvalidPattern) Error() string { 252 | return fmt.Sprintf("expected %#v to be maych %q", e.was, e.pattern) 253 | } 254 | 255 | // ErrInvalidProperty is returned when a `property` keyword failed. 256 | type ErrInvalidProperty struct { 257 | Property string 258 | Err error 259 | } 260 | 261 | func (e *ErrInvalidProperty) Error() string { 262 | return fmt.Sprintf("Invalid property %q: %s", e.Property, e.Err) 263 | } 264 | 265 | // ErrRequiredProperty is returned when a `required` keyword failed. 266 | type ErrRequiredProperty struct { 267 | expected string 268 | } 269 | 270 | func (e *ErrRequiredProperty) Error() string { 271 | return fmt.Sprintf("missing required property: %q", e.expected) 272 | } 273 | 274 | // ErrInvalidType is returned when a `type` keyword failed. 275 | type ErrInvalidType struct { 276 | expected []PrimitiveType 277 | was interface{} 278 | } 279 | 280 | func (e *ErrInvalidType) Error() string { 281 | return fmt.Sprintf("expected type to be in %#v but was %#v", e.expected, e.was) 282 | } 283 | 284 | // ErrNotUnique is returned when a `uniqueItems` keyword failed. 285 | type ErrNotUnique struct { 286 | IndexA int 287 | IndexB int 288 | Value interface{} 289 | } 290 | 291 | func (e *ErrNotUnique) Error() string { 292 | return fmt.Sprintf("value at %d (%v) is not unique (repeated at %d)", e.IndexA, e.Value, e.IndexB) 293 | } 294 | 295 | // ErrInvalidInstance is returned when the instance is invalid. 296 | type ErrInvalidInstance struct { 297 | Schema *Schema 298 | Errors []error 299 | } 300 | 301 | func (e *ErrInvalidInstance) Error() string { 302 | var buf bytes.Buffer 303 | fmt.Fprintf(&buf, "Schema errors (%s):", normalizeRef(e.Schema.Id.String())) 304 | for _, err := range e.Errors { 305 | s := strings.Replace(err.Error(), "\n", "\n ", -1) 306 | fmt.Fprintf(&buf, "\n- %s", s) 307 | } 308 | return buf.String() 309 | } 310 | -------------------------------------------------------------------------------- /testdata/draft4/type.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "integer type matches integers", 4 | "schema": {"type": "integer"}, 5 | "tests": [ 6 | { 7 | "description": "an integer is an integer", 8 | "data": 1, 9 | "valid": true 10 | }, 11 | { 12 | "description": "a float is not an integer", 13 | "data": 1.1, 14 | "valid": false 15 | }, 16 | { 17 | "description": "a string is not an integer", 18 | "data": "foo", 19 | "valid": false 20 | }, 21 | { 22 | "description": "an object is not an integer", 23 | "data": {}, 24 | "valid": false 25 | }, 26 | { 27 | "description": "an array is not an integer", 28 | "data": [], 29 | "valid": false 30 | }, 31 | { 32 | "description": "a boolean is not an integer", 33 | "data": true, 34 | "valid": false 35 | }, 36 | { 37 | "description": "null is not an integer", 38 | "data": null, 39 | "valid": false 40 | } 41 | ] 42 | }, 43 | { 44 | "description": "number type matches numbers", 45 | "schema": {"type": "number"}, 46 | "tests": [ 47 | { 48 | "description": "an integer is a number", 49 | "data": 1, 50 | "valid": true 51 | }, 52 | { 53 | "description": "a float is a number", 54 | "data": 1.1, 55 | "valid": true 56 | }, 57 | { 58 | "description": "a string is not a number", 59 | "data": "foo", 60 | "valid": false 61 | }, 62 | { 63 | "description": "an object is not a number", 64 | "data": {}, 65 | "valid": false 66 | }, 67 | { 68 | "description": "an array is not a number", 69 | "data": [], 70 | "valid": false 71 | }, 72 | { 73 | "description": "a boolean is not a number", 74 | "data": true, 75 | "valid": false 76 | }, 77 | { 78 | "description": "null is not a number", 79 | "data": null, 80 | "valid": false 81 | } 82 | ] 83 | }, 84 | { 85 | "description": "string type matches strings", 86 | "schema": {"type": "string"}, 87 | "tests": [ 88 | { 89 | "description": "1 is not a string", 90 | "data": 1, 91 | "valid": false 92 | }, 93 | { 94 | "description": "a float is not a string", 95 | "data": 1.1, 96 | "valid": false 97 | }, 98 | { 99 | "description": "a string is a string", 100 | "data": "foo", 101 | "valid": true 102 | }, 103 | { 104 | "description": "an object is not a string", 105 | "data": {}, 106 | "valid": false 107 | }, 108 | { 109 | "description": "an array is not a string", 110 | "data": [], 111 | "valid": false 112 | }, 113 | { 114 | "description": "a boolean is not a string", 115 | "data": true, 116 | "valid": false 117 | }, 118 | { 119 | "description": "null is not a string", 120 | "data": null, 121 | "valid": false 122 | } 123 | ] 124 | }, 125 | { 126 | "description": "object type matches objects", 127 | "schema": {"type": "object"}, 128 | "tests": [ 129 | { 130 | "description": "an integer is not an object", 131 | "data": 1, 132 | "valid": false 133 | }, 134 | { 135 | "description": "a float is not an object", 136 | "data": 1.1, 137 | "valid": false 138 | }, 139 | { 140 | "description": "a string is not an object", 141 | "data": "foo", 142 | "valid": false 143 | }, 144 | { 145 | "description": "an object is an object", 146 | "data": {}, 147 | "valid": true 148 | }, 149 | { 150 | "description": "an array is not an object", 151 | "data": [], 152 | "valid": false 153 | }, 154 | { 155 | "description": "a boolean is not an object", 156 | "data": true, 157 | "valid": false 158 | }, 159 | { 160 | "description": "null is not an object", 161 | "data": null, 162 | "valid": false 163 | } 164 | ] 165 | }, 166 | { 167 | "description": "array type matches arrays", 168 | "schema": {"type": "array"}, 169 | "tests": [ 170 | { 171 | "description": "an integer is not an array", 172 | "data": 1, 173 | "valid": false 174 | }, 175 | { 176 | "description": "a float is not an array", 177 | "data": 1.1, 178 | "valid": false 179 | }, 180 | { 181 | "description": "a string is not an array", 182 | "data": "foo", 183 | "valid": false 184 | }, 185 | { 186 | "description": "an object is not an array", 187 | "data": {}, 188 | "valid": false 189 | }, 190 | { 191 | "description": "an array is not an array", 192 | "data": [], 193 | "valid": true 194 | }, 195 | { 196 | "description": "a boolean is not an array", 197 | "data": true, 198 | "valid": false 199 | }, 200 | { 201 | "description": "null is not an array", 202 | "data": null, 203 | "valid": false 204 | } 205 | ] 206 | }, 207 | { 208 | "description": "boolean type matches booleans", 209 | "schema": {"type": "boolean"}, 210 | "tests": [ 211 | { 212 | "description": "an integer is not a boolean", 213 | "data": 1, 214 | "valid": false 215 | }, 216 | { 217 | "description": "a float is not a boolean", 218 | "data": 1.1, 219 | "valid": false 220 | }, 221 | { 222 | "description": "a string is not a boolean", 223 | "data": "foo", 224 | "valid": false 225 | }, 226 | { 227 | "description": "an object is not a boolean", 228 | "data": {}, 229 | "valid": false 230 | }, 231 | { 232 | "description": "an array is not a boolean", 233 | "data": [], 234 | "valid": false 235 | }, 236 | { 237 | "description": "a boolean is not a boolean", 238 | "data": true, 239 | "valid": true 240 | }, 241 | { 242 | "description": "null is not a boolean", 243 | "data": null, 244 | "valid": false 245 | } 246 | ] 247 | }, 248 | { 249 | "description": "null type matches only the null object", 250 | "schema": {"type": "null"}, 251 | "tests": [ 252 | { 253 | "description": "an integer is not null", 254 | "data": 1, 255 | "valid": false 256 | }, 257 | { 258 | "description": "a float is not null", 259 | "data": 1.1, 260 | "valid": false 261 | }, 262 | { 263 | "description": "a string is not null", 264 | "data": "foo", 265 | "valid": false 266 | }, 267 | { 268 | "description": "an object is not null", 269 | "data": {}, 270 | "valid": false 271 | }, 272 | { 273 | "description": "an array is not null", 274 | "data": [], 275 | "valid": false 276 | }, 277 | { 278 | "description": "a boolean is not null", 279 | "data": true, 280 | "valid": false 281 | }, 282 | { 283 | "description": "null is null", 284 | "data": null, 285 | "valid": true 286 | } 287 | ] 288 | }, 289 | { 290 | "description": "multiple types can be specified in an array", 291 | "schema": {"type": ["integer", "string"]}, 292 | "tests": [ 293 | { 294 | "description": "an integer is valid", 295 | "data": 1, 296 | "valid": true 297 | }, 298 | { 299 | "description": "a string is valid", 300 | "data": "foo", 301 | "valid": true 302 | }, 303 | { 304 | "description": "a float is invalid", 305 | "data": 1.1, 306 | "valid": false 307 | }, 308 | { 309 | "description": "an object is invalid", 310 | "data": {}, 311 | "valid": false 312 | }, 313 | { 314 | "description": "an array is invalid", 315 | "data": [], 316 | "valid": false 317 | }, 318 | { 319 | "description": "a boolean is invalid", 320 | "data": true, 321 | "valid": false 322 | }, 323 | { 324 | "description": "null is invalid", 325 | "data": null, 326 | "valid": false 327 | } 328 | ] 329 | } 330 | ] 331 | --------------------------------------------------------------------------------