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