1// Copyright 2017 Santhosh Kumar Tekuri. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package jsonschema
6
7import (
8	"encoding/json"
9	"fmt"
10	"io"
11	"math/big"
12	"net/url"
13	"regexp"
14	"strconv"
15	"strings"
16	"unicode/utf8"
17
18	"github.com/santhosh-tekuri/jsonschema/decoders"
19	"github.com/santhosh-tekuri/jsonschema/formats"
20	"github.com/santhosh-tekuri/jsonschema/mediatypes"
21)
22
23// A Schema represents compiled version of json-schema.
24type Schema struct {
25	URL string // absolute url of the resource.
26	Ptr string // json-pointer to schema. always starts with `#`.
27
28	// type agnostic validations
29	Always    *bool         // always pass/fail. used when booleans are used as schemas in draft-07.
30	Ref       *Schema       // reference to actual schema. if not nil, all the remaining fields are ignored.
31	Types     []string      // allowed types.
32	Constant  []interface{} // first element in slice is constant value. note: slice is used to capture nil constant.
33	Enum      []interface{} // allowed values.
34	enumError string        // error message for enum fail. captured here to avoid constructing error message every time.
35	Not       *Schema
36	AllOf     []*Schema
37	AnyOf     []*Schema
38	OneOf     []*Schema
39	If        *Schema
40	Then      *Schema // nil, when If is nil.
41	Else      *Schema // nil, when If is nil.
42
43	// object validations
44	MinProperties        int      // -1 if not specified.
45	MaxProperties        int      // -1 if not specified.
46	Required             []string // list of required properties.
47	Properties           map[string]*Schema
48	PropertyNames        *Schema
49	RegexProperties      bool // property names must be valid regex. used only in draft4 as workaround in metaschema.
50	PatternProperties    map[*regexp.Regexp]*Schema
51	AdditionalProperties interface{}            // nil or false or *Schema.
52	Dependencies         map[string]interface{} // value is *Schema or []string.
53
54	// array validations
55	MinItems        int // -1 if not specified.
56	MaxItems        int // -1 if not specified.
57	UniqueItems     bool
58	Items           interface{} // nil or *Schema or []*Schema
59	AdditionalItems interface{} // nil or bool or *Schema.
60	Contains        *Schema
61
62	// string validations
63	MinLength        int // -1 if not specified.
64	MaxLength        int // -1 if not specified.
65	Pattern          *regexp.Regexp
66	Format           formats.Format
67	FormatName       string
68	ContentEncoding  string
69	Decoder          decoders.Decoder
70	ContentMediaType string
71	MediaType        mediatypes.MediaType
72
73	// number validators
74	Minimum          *big.Float
75	ExclusiveMinimum *big.Float
76	Maximum          *big.Float
77	ExclusiveMaximum *big.Float
78	MultipleOf       *big.Float
79
80	// annotations. captured only when Compiler.ExtractAnnotations is true.
81	Title       string
82	Description string
83	Default     interface{}
84	ReadOnly    bool
85	WriteOnly   bool
86	Examples    []interface{}
87}
88
89// Compile parses json-schema at given url returns, if successful,
90// a Schema object that can be used to match against json.
91//
92// The json-schema is validated with draft4 specification.
93// Returned error can be *SchemaError
94func Compile(url string) (*Schema, error) {
95	return NewCompiler().Compile(url)
96}
97
98// MustCompile is like Compile but panics if the url cannot be compiled to *Schema.
99// It simplifies safe initialization of global variables holding compiled Schemas.
100func MustCompile(url string) *Schema {
101	return NewCompiler().MustCompile(url)
102}
103
104// Validate validates the given json data, against the json-schema.
105//
106// Returned error can be *ValidationError.
107func (s *Schema) Validate(r io.Reader) error {
108	doc, err := DecodeJSON(r)
109	if err != nil {
110		return err
111	}
112	return s.ValidateInterface(doc)
113}
114
115// ValidateInterface validates given doc, against the json-schema.
116//
117// the doc must be the value decoded by json package using interface{} type.
118// we recommend to use jsonschema.DecodeJSON(io.Reader) to decode JSON.
119func (s *Schema) ValidateInterface(doc interface{}) (err error) {
120	defer func() {
121		if r := recover(); r != nil {
122			if _, ok := r.(InvalidJSONTypeError); ok {
123				err = r.(InvalidJSONTypeError)
124			} else {
125				panic(r)
126			}
127		}
128	}()
129	if err := s.validate(doc); err != nil {
130		finishSchemaContext(err, s)
131		finishInstanceContext(err)
132		return &ValidationError{
133			Message:     fmt.Sprintf("doesn't validate with %q", s.URL+s.Ptr),
134			InstancePtr: "#",
135			SchemaURL:   s.URL,
136			SchemaPtr:   s.Ptr,
137			Causes:      []*ValidationError{err.(*ValidationError)},
138		}
139	}
140	return nil
141}
142
143// validate validates given value v with this schema.
144func (s *Schema) validate(v interface{}) error {
145	if s.Always != nil {
146		if !*s.Always {
147			return validationError("", "always fail")
148		}
149		return nil
150	}
151
152	if s.Ref != nil {
153		if err := s.Ref.validate(v); err != nil {
154			finishSchemaContext(err, s.Ref)
155			var refURL string
156			if s.URL == s.Ref.URL {
157				refURL = s.Ref.Ptr
158			} else {
159				refURL = s.Ref.URL + s.Ref.Ptr
160			}
161			return validationError("$ref", "doesn't validate with %q", refURL).add(err)
162		}
163
164		// All other properties in a "$ref" object MUST be ignored
165		return nil
166	}
167
168	if len(s.Types) > 0 {
169		vType := jsonType(v)
170		matched := false
171		for _, t := range s.Types {
172			if vType == t {
173				matched = true
174				break
175			} else if t == "integer" && vType == "number" {
176				if _, ok := new(big.Int).SetString(fmt.Sprint(v), 10); ok {
177					matched = true
178					break
179				}
180			}
181		}
182		if !matched {
183			return validationError("type", "expected %s, but got %s", strings.Join(s.Types, " or "), vType)
184		}
185	}
186
187	if len(s.Constant) > 0 {
188		if !equals(v, s.Constant[0]) {
189			switch jsonType(s.Constant[0]) {
190			case "object", "array":
191				return validationError("const", "const failed")
192			default:
193				return validationError("const", "value must be %#v", s.Constant[0])
194			}
195		}
196	}
197
198	if len(s.Enum) > 0 {
199		matched := false
200		for _, item := range s.Enum {
201			if equals(v, item) {
202				matched = true
203				break
204			}
205		}
206		if !matched {
207			return validationError("enum", s.enumError)
208		}
209	}
210
211	if s.Not != nil && s.Not.validate(v) == nil {
212		return validationError("not", "not failed")
213	}
214
215	for i, sch := range s.AllOf {
216		if err := sch.validate(v); err != nil {
217			return validationError("allOf/"+strconv.Itoa(i), "allOf failed").add(err)
218		}
219	}
220
221	if len(s.AnyOf) > 0 {
222		matched := false
223		var causes []error
224		for i, sch := range s.AnyOf {
225			if err := sch.validate(v); err == nil {
226				matched = true
227				break
228			} else {
229				causes = append(causes, addContext("", strconv.Itoa(i), err))
230			}
231		}
232		if !matched {
233			return validationError("anyOf", "anyOf failed").add(causes...)
234		}
235	}
236
237	if len(s.OneOf) > 0 {
238		matched := -1
239		var causes []error
240		for i, sch := range s.OneOf {
241			if err := sch.validate(v); err == nil {
242				if matched == -1 {
243					matched = i
244				} else {
245					return validationError("oneOf", "valid against schemas at indexes %d and %d", matched, i)
246				}
247			} else {
248				causes = append(causes, addContext("", strconv.Itoa(i), err))
249			}
250		}
251		if matched == -1 {
252			return validationError("oneOf", "oneOf failed").add(causes...)
253		}
254	}
255
256	if s.If != nil {
257		if s.If.validate(v) == nil {
258			if s.Then != nil {
259				if err := s.Then.validate(v); err != nil {
260					return validationError("then", "if-then failed").add(err)
261				}
262			}
263		} else {
264			if s.Else != nil {
265				if err := s.Else.validate(v); err != nil {
266					return validationError("else", "if-else failed").add(err)
267				}
268			}
269		}
270	}
271
272	switch v := v.(type) {
273	case map[string]interface{}:
274		if s.MinProperties != -1 && len(v) < s.MinProperties {
275			return validationError("minProperties", "minimum %d properties allowed, but found %d properties", s.MinProperties, len(v))
276		}
277		if s.MaxProperties != -1 && len(v) > s.MaxProperties {
278			return validationError("maxProperties", "maximum %d properties allowed, but found %d properties", s.MaxProperties, len(v))
279		}
280		if len(s.Required) > 0 {
281			var missing []string
282			for _, pname := range s.Required {
283				if _, ok := v[pname]; !ok {
284					missing = append(missing, strconv.Quote(pname))
285				}
286			}
287			if len(missing) > 0 {
288				return validationError("required", "missing properties: %s", strings.Join(missing, ", "))
289			}
290		}
291
292		var additionalProps map[string]struct{}
293		if s.AdditionalProperties != nil {
294			additionalProps = make(map[string]struct{}, len(v))
295			for pname := range v {
296				additionalProps[pname] = struct{}{}
297			}
298		}
299
300		if len(s.Properties) > 0 {
301			for pname, pschema := range s.Properties {
302				if pvalue, ok := v[pname]; ok {
303					delete(additionalProps, pname)
304					if err := pschema.validate(pvalue); err != nil {
305						return addContext(escape(pname), "properties/"+escape(pname), err)
306					}
307				}
308			}
309		}
310
311		if s.PropertyNames != nil {
312			for pname := range v {
313				if err := s.PropertyNames.validate(pname); err != nil {
314					return addContext(escape(pname), "propertyNames", err)
315				}
316			}
317		}
318
319		if s.RegexProperties {
320			for pname := range v {
321				if !formats.IsRegex(pname) {
322					return validationError("", "patternProperty %q is not valid regex", pname)
323				}
324			}
325		}
326		for pattern, pschema := range s.PatternProperties {
327			for pname, pvalue := range v {
328				if pattern.MatchString(pname) {
329					delete(additionalProps, pname)
330					if err := pschema.validate(pvalue); err != nil {
331						return addContext(escape(pname), "patternProperties/"+escape(pattern.String()), err)
332					}
333				}
334			}
335		}
336		if s.AdditionalProperties != nil {
337			if _, ok := s.AdditionalProperties.(bool); ok {
338				if len(additionalProps) != 0 {
339					pnames := make([]string, 0, len(additionalProps))
340					for pname := range additionalProps {
341						pnames = append(pnames, strconv.Quote(pname))
342					}
343					return validationError("additionalProperties", "additionalProperties %s not allowed", strings.Join(pnames, ", "))
344				}
345			} else {
346				schema := s.AdditionalProperties.(*Schema)
347				for pname := range additionalProps {
348					if pvalue, ok := v[pname]; ok {
349						if err := schema.validate(pvalue); err != nil {
350							return addContext(escape(pname), "additionalProperties", err)
351						}
352					}
353				}
354			}
355		}
356		for dname, dvalue := range s.Dependencies {
357			if _, ok := v[dname]; ok {
358				switch dvalue := dvalue.(type) {
359				case *Schema:
360					if err := dvalue.validate(v); err != nil {
361						return addContext("", "dependencies/"+escape(dname), err)
362					}
363				case []string:
364					for i, pname := range dvalue {
365						if _, ok := v[pname]; !ok {
366							return validationError("dependencies/"+escape(dname)+"/"+strconv.Itoa(i), "property %q is required, if %q property exists", pname, dname)
367						}
368					}
369				}
370			}
371		}
372
373	case []interface{}:
374		if s.MinItems != -1 && len(v) < s.MinItems {
375			return validationError("minItems", "minimum %d items allowed, but found %d items", s.MinItems, len(v))
376		}
377		if s.MaxItems != -1 && len(v) > s.MaxItems {
378			return validationError("maxItems", "maximum %d items allowed, but found %d items", s.MaxItems, len(v))
379		}
380		if s.UniqueItems {
381			for i := 1; i < len(v); i++ {
382				for j := 0; j < i; j++ {
383					if equals(v[i], v[j]) {
384						return validationError("uniqueItems", "items at index %d and %d are equal", j, i)
385					}
386				}
387			}
388		}
389		switch items := s.Items.(type) {
390		case *Schema:
391			for i, item := range v {
392				if err := items.validate(item); err != nil {
393					return addContext(strconv.Itoa(i), "items", err)
394				}
395			}
396		case []*Schema:
397			if additionalItems, ok := s.AdditionalItems.(bool); ok {
398				if !additionalItems && len(v) > len(items) {
399					return validationError("additionalItems", "only %d items are allowed, but found %d items", len(items), len(v))
400				}
401			}
402			for i, item := range v {
403				if i < len(items) {
404					if err := items[i].validate(item); err != nil {
405						return addContext(strconv.Itoa(i), "items/"+strconv.Itoa(i), err)
406					}
407				} else if sch, ok := s.AdditionalItems.(*Schema); ok {
408					if err := sch.validate(item); err != nil {
409						return addContext(strconv.Itoa(i), "additionalItems", err)
410					}
411				} else {
412					break
413				}
414			}
415		}
416		if s.Contains != nil {
417			matched := false
418			var causes []error
419			for i, item := range v {
420				if err := s.Contains.validate(item); err != nil {
421					causes = append(causes, addContext(strconv.Itoa(i), "", err))
422				} else {
423					matched = true
424					break
425				}
426			}
427			if !matched {
428				return validationError("contains", "contains failed").add(causes...)
429			}
430		}
431
432	case string:
433		if s.MinLength != -1 || s.MaxLength != -1 {
434			length := utf8.RuneCount([]byte(v))
435			if s.MinLength != -1 && length < s.MinLength {
436				return validationError("minLength", "length must be >= %d, but got %d", s.MinLength, length)
437			}
438			if s.MaxLength != -1 && length > s.MaxLength {
439				return validationError("maxLength", "length must be <= %d, but got %d", s.MaxLength, length)
440			}
441		}
442		if s.Pattern != nil && !s.Pattern.MatchString(v) {
443			return validationError("pattern", "does not match pattern %q", s.Pattern)
444		}
445		if s.Format != nil && !s.Format(v) {
446			return validationError("format", "%q is not valid %q", v, s.FormatName)
447		}
448
449		var content []byte
450		if s.Decoder != nil {
451			b, err := s.Decoder(v)
452			if err != nil {
453				return validationError("contentEncoding", "%q is not %s encoded", v, s.ContentEncoding)
454			}
455			content = b
456		}
457		if s.MediaType != nil {
458			if s.Decoder == nil {
459				content = []byte(v)
460			}
461			if err := s.MediaType(content); err != nil {
462				return validationError("contentMediaType", "value is not of mediatype %q", s.ContentMediaType)
463			}
464		}
465
466	case json.Number, float64, int, int32, int64:
467		num, _ := new(big.Float).SetString(fmt.Sprint(v))
468		if s.Minimum != nil && num.Cmp(s.Minimum) < 0 {
469			return validationError("minimum", "must be >= %v but found %v", s.Minimum, v)
470		}
471		if s.ExclusiveMinimum != nil && num.Cmp(s.ExclusiveMinimum) <= 0 {
472			return validationError("exclusiveMinimum", "must be > %v but found %v", s.ExclusiveMinimum, v)
473		}
474		if s.Maximum != nil && num.Cmp(s.Maximum) > 0 {
475			return validationError("maximum", "must be <= %v but found %v", s.Maximum, v)
476		}
477		if s.ExclusiveMaximum != nil && num.Cmp(s.ExclusiveMaximum) >= 0 {
478			return validationError("exclusiveMaximum", "must be < %v but found %v", s.ExclusiveMaximum, v)
479		}
480		if s.MultipleOf != nil {
481			if q := new(big.Float).Quo(num, s.MultipleOf); !q.IsInt() {
482				return validationError("multipleOf", "%v not multipleOf %v", v, s.MultipleOf)
483			}
484		}
485	}
486
487	return nil
488}
489
490// jsonType returns the json type of given value v.
491//
492// It panics if the given value is not valid json value
493func jsonType(v interface{}) string {
494	switch v.(type) {
495	case nil:
496		return "null"
497	case bool:
498		return "boolean"
499	case json.Number, float64, int, int32, int64:
500		return "number"
501	case string:
502		return "string"
503	case []interface{}:
504		return "array"
505	case map[string]interface{}:
506		return "object"
507	}
508	panic(InvalidJSONTypeError(fmt.Sprintf("%T", v)))
509}
510
511// equals tells if given two json values are equal or not.
512func equals(v1, v2 interface{}) bool {
513	v1Type := jsonType(v1)
514	if v1Type != jsonType(v2) {
515		return false
516	}
517	switch v1Type {
518	case "array":
519		arr1, arr2 := v1.([]interface{}), v2.([]interface{})
520		if len(arr1) != len(arr2) {
521			return false
522		}
523		for i := range arr1 {
524			if !equals(arr1[i], arr2[i]) {
525				return false
526			}
527		}
528		return true
529	case "object":
530		obj1, obj2 := v1.(map[string]interface{}), v2.(map[string]interface{})
531		if len(obj1) != len(obj2) {
532			return false
533		}
534		for k, v1 := range obj1 {
535			if v2, ok := obj2[k]; ok {
536				if !equals(v1, v2) {
537					return false
538				}
539			} else {
540				return false
541			}
542		}
543		return true
544	case "number":
545		num1, _ := new(big.Float).SetString(string(v1.(json.Number)))
546		num2, _ := new(big.Float).SetString(string(v2.(json.Number)))
547		return num1.Cmp(num2) == 0
548	default:
549		return v1 == v2
550	}
551}
552
553// escape converts given token to valid json-pointer token
554func escape(token string) string {
555	token = strings.Replace(token, "~", "~0", -1)
556	token = strings.Replace(token, "/", "~1", -1)
557	return url.PathEscape(token)
558}
559