1// Package jsonschema uses reflection to generate JSON Schemas from Go types [1]. 2// 3// If json tags are present on struct fields, they will be used to infer 4// property names and if a property is required (omitempty is present). 5// 6// [1] http://json-schema.org/latest/json-schema-validation.html 7package jsonschema 8 9import ( 10 "encoding/json" 11 "net" 12 "net/url" 13 "reflect" 14 "strconv" 15 "strings" 16 "time" 17 18 "github.com/iancoleman/orderedmap" 19) 20 21// Version is the JSON Schema version. 22// If extending JSON Schema with custom values use a custom URI. 23// RFC draft-wright-json-schema-00, section 6 24var Version = "http://json-schema.org/draft-04/schema#" 25 26// Schema is the root schema. 27// RFC draft-wright-json-schema-00, section 4.5 28type Schema struct { 29 *Type 30 Definitions Definitions 31} 32 33// Type represents a JSON Schema object type. 34type Type struct { 35 // RFC draft-wright-json-schema-00 36 Version string `json:"$schema,omitempty"` // section 6.1 37 Ref string `json:"$ref,omitempty"` // section 7 38 // RFC draft-wright-json-schema-validation-00, section 5 39 MultipleOf int `json:"multipleOf,omitempty"` // section 5.1 40 Maximum int `json:"maximum,omitempty"` // section 5.2 41 ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` // section 5.3 42 Minimum int `json:"minimum,omitempty"` // section 5.4 43 ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` // section 5.5 44 MaxLength int `json:"maxLength,omitempty"` // section 5.6 45 MinLength int `json:"minLength,omitempty"` // section 5.7 46 Pattern string `json:"pattern,omitempty"` // section 5.8 47 AdditionalItems *Type `json:"additionalItems,omitempty"` // section 5.9 48 Items *Type `json:"items,omitempty"` // section 5.9 49 MaxItems int `json:"maxItems,omitempty"` // section 5.10 50 MinItems int `json:"minItems,omitempty"` // section 5.11 51 UniqueItems bool `json:"uniqueItems,omitempty"` // section 5.12 52 MaxProperties int `json:"maxProperties,omitempty"` // section 5.13 53 MinProperties int `json:"minProperties,omitempty"` // section 5.14 54 Required []string `json:"required,omitempty"` // section 5.15 55 Properties *orderedmap.OrderedMap `json:"properties,omitempty"` // section 5.16 56 PatternProperties map[string]*Type `json:"patternProperties,omitempty"` // section 5.17 57 AdditionalProperties json.RawMessage `json:"additionalProperties,omitempty"` // section 5.18 58 Dependencies map[string]*Type `json:"dependencies,omitempty"` // section 5.19 59 Enum []interface{} `json:"enum,omitempty"` // section 5.20 60 Type string `json:"type,omitempty"` // section 5.21 61 AllOf []*Type `json:"allOf,omitempty"` // section 5.22 62 AnyOf []*Type `json:"anyOf,omitempty"` // section 5.23 63 OneOf []*Type `json:"oneOf,omitempty"` // section 5.24 64 Not *Type `json:"not,omitempty"` // section 5.25 65 Definitions Definitions `json:"definitions,omitempty"` // section 5.26 66 // RFC draft-wright-json-schema-validation-00, section 6, 7 67 Title string `json:"title,omitempty"` // section 6.1 68 Description string `json:"description,omitempty"` // section 6.1 69 Default interface{} `json:"default,omitempty"` // section 6.2 70 Format string `json:"format,omitempty"` // section 7 71 Examples []interface{} `json:"examples,omitempty"` // section 7.4 72 // RFC draft-wright-json-schema-hyperschema-00, section 4 73 Media *Type `json:"media,omitempty"` // section 4.3 74 BinaryEncoding string `json:"binaryEncoding,omitempty"` // section 4.3 75 76 Extras map[string]interface{} `json:"-"` 77} 78 79// Reflect reflects to Schema from a value using the default Reflector 80func Reflect(v interface{}) *Schema { 81 return ReflectFromType(reflect.TypeOf(v)) 82} 83 84// ReflectFromType generates root schema using the default Reflector 85func ReflectFromType(t reflect.Type) *Schema { 86 r := &Reflector{} 87 return r.ReflectFromType(t) 88} 89 90// A Reflector reflects values into a Schema. 91type Reflector struct { 92 // AllowAdditionalProperties will cause the Reflector to generate a schema 93 // with additionalProperties to 'true' for all struct types. This means 94 // the presence of additional keys in JSON objects will not cause validation 95 // to fail. Note said additional keys will simply be dropped when the 96 // validated JSON is unmarshaled. 97 AllowAdditionalProperties bool 98 99 // RequiredFromJSONSchemaTags will cause the Reflector to generate a schema 100 // that requires any key tagged with `jsonschema:required`, overriding the 101 // default of requiring any key *not* tagged with `json:,omitempty`. 102 RequiredFromJSONSchemaTags bool 103 104 // ExpandedStruct will cause the toplevel definitions of the schema not 105 // be referenced itself to a definition. 106 ExpandedStruct bool 107 108 // Do not reference definitions. 109 // All types are still registered under the "definitions" top-level object, 110 // but instead of $ref fields in containing types, the entire definition 111 // of the contained type is inserted. 112 // This will cause the entire structure of types to be output in one tree. 113 DoNotReference bool 114 115 // Use package paths as well as type names, to avoid conflicts. 116 // Without this setting, if two packages contain a type with the same name, 117 // and both are present in a schema, they will conflict and overwrite in 118 // the definition map and produce bad output. This is particularly 119 // noticeable when using DoNotReference. 120 FullyQualifyTypeNames bool 121 122 // IgnoredTypes defines a slice of types that should be ignored in the schema, 123 // switching to just allowing additional properties instead. 124 IgnoredTypes []interface{} 125 126 // TypeMapper is a function that can be used to map custom Go types to jsconschema types. 127 TypeMapper func(reflect.Type) *Type 128} 129 130// Reflect reflects to Schema from a value. 131func (r *Reflector) Reflect(v interface{}) *Schema { 132 return r.ReflectFromType(reflect.TypeOf(v)) 133} 134 135// ReflectFromType generates root schema 136func (r *Reflector) ReflectFromType(t reflect.Type) *Schema { 137 definitions := Definitions{} 138 if r.ExpandedStruct { 139 st := &Type{ 140 Version: Version, 141 Type: "object", 142 Properties: orderedmap.New(), 143 AdditionalProperties: []byte("false"), 144 } 145 if r.AllowAdditionalProperties { 146 st.AdditionalProperties = []byte("true") 147 } 148 r.reflectStructFields(st, definitions, t) 149 r.reflectStruct(definitions, t) 150 delete(definitions, r.typeName(t)) 151 return &Schema{Type: st, Definitions: definitions} 152 } 153 154 s := &Schema{ 155 Type: r.reflectTypeToSchema(definitions, t), 156 Definitions: definitions, 157 } 158 return s 159} 160 161// Definitions hold schema definitions. 162// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.26 163// RFC draft-wright-json-schema-validation-00, section 5.26 164type Definitions map[string]*Type 165 166// Available Go defined types for JSON Schema Validation. 167// RFC draft-wright-json-schema-validation-00, section 7.3 168var ( 169 timeType = reflect.TypeOf(time.Time{}) // date-time RFC section 7.3.1 170 ipType = reflect.TypeOf(net.IP{}) // ipv4 and ipv6 RFC section 7.3.4, 7.3.5 171 uriType = reflect.TypeOf(url.URL{}) // uri RFC section 7.3.6 172) 173 174// Byte slices will be encoded as base64 175var byteSliceType = reflect.TypeOf([]byte(nil)) 176 177// Go code generated from protobuf enum types should fulfil this interface. 178type protoEnum interface { 179 EnumDescriptor() ([]byte, []int) 180} 181 182var protoEnumType = reflect.TypeOf((*protoEnum)(nil)).Elem() 183 184func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) *Type { 185 // Already added to definitions? 186 if _, ok := definitions[r.typeName(t)]; ok && !r.DoNotReference { 187 return &Type{Ref: "#/definitions/" + r.typeName(t)} 188 } 189 190 // jsonpb will marshal protobuf enum options as either strings or integers. 191 // It will unmarshal either. 192 if t.Implements(protoEnumType) { 193 return &Type{OneOf: []*Type{ 194 {Type: "string"}, 195 {Type: "integer"}, 196 }} 197 } 198 199 if r.TypeMapper != nil { 200 if t := r.TypeMapper(t); t != nil { 201 return t 202 } 203 } 204 205 // Defined format types for JSON Schema Validation 206 // RFC draft-wright-json-schema-validation-00, section 7.3 207 // TODO email RFC section 7.3.2, hostname RFC section 7.3.3, uriref RFC section 7.3.7 208 switch t { 209 case ipType: 210 // TODO differentiate ipv4 and ipv6 RFC section 7.3.4, 7.3.5 211 return &Type{Type: "string", Format: "ipv4"} // ipv4 RFC section 7.3.4 212 } 213 214 switch t.Kind() { 215 case reflect.Struct: 216 217 switch t { 218 case timeType: // date-time RFC section 7.3.1 219 return &Type{Type: "string", Format: "date-time"} 220 case uriType: // uri RFC section 7.3.6 221 return &Type{Type: "string", Format: "uri"} 222 default: 223 return r.reflectStruct(definitions, t) 224 } 225 226 case reflect.Map: 227 rt := &Type{ 228 Type: "object", 229 PatternProperties: map[string]*Type{ 230 ".*": r.reflectTypeToSchema(definitions, t.Elem()), 231 }, 232 } 233 delete(rt.PatternProperties, "additionalProperties") 234 return rt 235 236 case reflect.Slice, reflect.Array: 237 returnType := &Type{} 238 if t.Kind() == reflect.Array { 239 returnType.MinItems = t.Len() 240 returnType.MaxItems = returnType.MinItems 241 } 242 switch t { 243 case byteSliceType: 244 returnType.Type = "string" 245 returnType.Media = &Type{BinaryEncoding: "base64"} 246 return returnType 247 default: 248 returnType.Type = "array" 249 returnType.Items = r.reflectTypeToSchema(definitions, t.Elem()) 250 return returnType 251 } 252 253 case reflect.Interface: 254 return &Type{ 255 AdditionalProperties: []byte("true"), 256 } 257 258 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 259 reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 260 return &Type{Type: "integer"} 261 262 case reflect.Float32, reflect.Float64: 263 return &Type{Type: "number"} 264 265 case reflect.Bool: 266 return &Type{Type: "boolean"} 267 268 case reflect.String: 269 return &Type{Type: "string"} 270 271 case reflect.Ptr: 272 return r.reflectTypeToSchema(definitions, t.Elem()) 273 } 274 panic("unsupported type " + t.String()) 275} 276 277// Refects a struct to a JSON Schema type. 278func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type) *Type { 279 for _, ignored := range r.IgnoredTypes { 280 if reflect.TypeOf(ignored) == t { 281 st := &Type{ 282 Type: "object", 283 Properties: orderedmap.New(), 284 AdditionalProperties: []byte("true"), 285 } 286 definitions[r.typeName(t)] = st 287 288 if r.DoNotReference { 289 return st 290 } else { 291 return &Type{ 292 Version: Version, 293 Ref: "#/definitions/" + r.typeName(t), 294 } 295 } 296 297 } 298 } 299 st := &Type{ 300 Type: "object", 301 Properties: orderedmap.New(), 302 AdditionalProperties: []byte("false"), 303 } 304 if r.AllowAdditionalProperties { 305 st.AdditionalProperties = []byte("true") 306 } 307 definitions[r.typeName(t)] = st 308 r.reflectStructFields(st, definitions, t) 309 310 if r.DoNotReference { 311 return st 312 } else { 313 return &Type{ 314 Version: Version, 315 Ref: "#/definitions/" + r.typeName(t), 316 } 317 } 318} 319 320func (r *Reflector) reflectStructFields(st *Type, definitions Definitions, t reflect.Type) { 321 if t.Kind() == reflect.Ptr { 322 t = t.Elem() 323 } 324 if t.Kind() != reflect.Struct { 325 return 326 } 327 for i := 0; i < t.NumField(); i++ { 328 f := t.Field(i) 329 name, exist, required := r.reflectFieldName(f) 330 // if anonymous and exported type should be processed recursively 331 // current type should inherit properties of anonymous one 332 if name == "" { 333 if f.Anonymous && !exist { 334 r.reflectStructFields(st, definitions, f.Type) 335 } 336 continue 337 } 338 339 property := r.reflectTypeToSchema(definitions, f.Type) 340 property.structKeywordsFromTags(f, st, name) 341 342 st.Properties.Set(name, property) 343 if required { 344 st.Required = append(st.Required, name) 345 } 346 } 347} 348 349func (t *Type) structKeywordsFromTags(f reflect.StructField, parentType *Type, propertyName string) { 350 t.Description = f.Tag.Get("jsonschema_description") 351 tags := strings.Split(f.Tag.Get("jsonschema"), ",") 352 t.genericKeywords(tags, parentType, propertyName) 353 switch t.Type { 354 case "string": 355 t.stringKeywords(tags) 356 case "number": 357 t.numbericKeywords(tags) 358 case "integer": 359 t.numbericKeywords(tags) 360 case "array": 361 t.arrayKeywords(tags) 362 } 363 extras := strings.Split(f.Tag.Get("jsonschema_extras"), ",") 364 t.extraKeywords(extras) 365} 366 367// read struct tags for generic keyworks 368func (t *Type) genericKeywords(tags []string, parentType *Type, propertyName string) { 369 for _, tag := range tags { 370 nameValue := strings.Split(tag, "=") 371 if len(nameValue) == 2 { 372 name, val := nameValue[0], nameValue[1] 373 switch name { 374 case "title": 375 t.Title = val 376 case "description": 377 t.Description = val 378 case "type": 379 t.Type = val 380 case "oneof_required": 381 var typeFound *Type 382 for i := range parentType.OneOf { 383 if parentType.OneOf[i].Title == nameValue[1] { 384 typeFound = parentType.OneOf[i] 385 } 386 } 387 if typeFound == nil { 388 typeFound = &Type{ 389 Title: nameValue[1], 390 Required: []string{}, 391 } 392 parentType.OneOf = append(parentType.OneOf, typeFound) 393 } 394 typeFound.Required = append(typeFound.Required, propertyName) 395 case "oneof_type": 396 if t.OneOf == nil { 397 t.OneOf = make([]*Type, 0, 1) 398 } 399 t.Type = "" 400 types := strings.Split(nameValue[1], ";") 401 for _, ty := range types { 402 t.OneOf = append(t.OneOf, &Type{ 403 Type: ty, 404 }) 405 } 406 case "enum": 407 switch t.Type { 408 case "string": 409 t.Enum = append(t.Enum, val) 410 case "integer": 411 i, _ := strconv.Atoi(val) 412 t.Enum = append(t.Enum, i) 413 case "number": 414 f, _ := strconv.ParseFloat(val, 64) 415 t.Enum = append(t.Enum, f) 416 } 417 } 418 } 419 } 420} 421 422// read struct tags for string type keyworks 423func (t *Type) stringKeywords(tags []string) { 424 for _, tag := range tags { 425 nameValue := strings.Split(tag, "=") 426 if len(nameValue) == 2 { 427 name, val := nameValue[0], nameValue[1] 428 switch name { 429 case "minLength": 430 i, _ := strconv.Atoi(val) 431 t.MinLength = i 432 case "maxLength": 433 i, _ := strconv.Atoi(val) 434 t.MaxLength = i 435 case "pattern": 436 t.Pattern = val 437 case "format": 438 switch val { 439 case "date-time", "email", "hostname", "ipv4", "ipv6", "uri": 440 t.Format = val 441 break 442 } 443 case "default": 444 t.Default = val 445 case "example": 446 t.Examples = append(t.Examples, val) 447 } 448 } 449 } 450} 451 452// read struct tags for numberic type keyworks 453func (t *Type) numbericKeywords(tags []string) { 454 for _, tag := range tags { 455 nameValue := strings.Split(tag, "=") 456 if len(nameValue) == 2 { 457 name, val := nameValue[0], nameValue[1] 458 switch name { 459 case "multipleOf": 460 i, _ := strconv.Atoi(val) 461 t.MultipleOf = i 462 case "minimum": 463 i, _ := strconv.Atoi(val) 464 t.Minimum = i 465 case "maximum": 466 i, _ := strconv.Atoi(val) 467 t.Maximum = i 468 case "exclusiveMaximum": 469 b, _ := strconv.ParseBool(val) 470 t.ExclusiveMaximum = b 471 case "exclusiveMinimum": 472 b, _ := strconv.ParseBool(val) 473 t.ExclusiveMinimum = b 474 case "default": 475 i, _ := strconv.Atoi(val) 476 t.Default = i 477 case "example": 478 if i, err := strconv.Atoi(val); err == nil { 479 t.Examples = append(t.Examples, i) 480 } 481 } 482 } 483 } 484} 485 486// read struct tags for object type keyworks 487// func (t *Type) objectKeywords(tags []string) { 488// for _, tag := range tags{ 489// nameValue := strings.Split(tag, "=") 490// name, val := nameValue[0], nameValue[1] 491// switch name{ 492// case "dependencies": 493// t.Dependencies = val 494// break; 495// case "patternProperties": 496// t.PatternProperties = val 497// break; 498// } 499// } 500// } 501 502// read struct tags for array type keyworks 503func (t *Type) arrayKeywords(tags []string) { 504 var defaultValues []interface{} 505 for _, tag := range tags { 506 nameValue := strings.Split(tag, "=") 507 if len(nameValue) == 2 { 508 name, val := nameValue[0], nameValue[1] 509 switch name { 510 case "minItems": 511 i, _ := strconv.Atoi(val) 512 t.MinItems = i 513 case "maxItems": 514 i, _ := strconv.Atoi(val) 515 t.MaxItems = i 516 case "uniqueItems": 517 t.UniqueItems = true 518 case "default": 519 defaultValues = append(defaultValues, val) 520 } 521 } 522 } 523 if len(defaultValues) > 0 { 524 t.Default = defaultValues 525 } 526} 527 528func (t *Type) extraKeywords(tags []string) { 529 for _, tag := range tags { 530 nameValue := strings.Split(tag, "=") 531 if len(nameValue) == 2 { 532 t.setExtra(nameValue[0], nameValue[1]) 533 } 534 } 535} 536 537func (t *Type) setExtra(key, val string) { 538 if t.Extras == nil { 539 t.Extras = map[string]interface{}{} 540 } 541 if existingVal, ok := t.Extras[key]; ok { 542 switch existingVal.(type) { 543 case string: 544 t.Extras[key] = []string{existingVal.(string), val} 545 case []string: 546 t.Extras[key] = append(existingVal.([]string), val) 547 } 548 } else { 549 t.Extras[key] = val 550 } 551} 552 553func requiredFromJSONTags(tags []string) bool { 554 if ignoredByJSONTags(tags) { 555 return false 556 } 557 558 for _, tag := range tags[1:] { 559 if tag == "omitempty" { 560 return false 561 } 562 } 563 return true 564} 565 566func requiredFromJSONSchemaTags(tags []string) bool { 567 if ignoredByJSONSchemaTags(tags) { 568 return false 569 } 570 for _, tag := range tags { 571 if tag == "required" { 572 return true 573 } 574 } 575 return false 576} 577 578func ignoredByJSONTags(tags []string) bool { 579 return tags[0] == "-" 580} 581 582func ignoredByJSONSchemaTags(tags []string) bool { 583 return tags[0] == "-" 584} 585 586func (r *Reflector) reflectFieldName(f reflect.StructField) (string, bool, bool) { 587 jsonTags, exist := f.Tag.Lookup("json") 588 if !exist { 589 jsonTags = f.Tag.Get("yaml") 590 } 591 592 jsonTagsList := strings.Split(jsonTags, ",") 593 594 if ignoredByJSONTags(jsonTagsList) { 595 return "", exist, false 596 } 597 598 jsonSchemaTags := strings.Split(f.Tag.Get("jsonschema"), ",") 599 if ignoredByJSONSchemaTags(jsonSchemaTags) { 600 return "", exist, false 601 } 602 603 name := f.Name 604 required := requiredFromJSONTags(jsonTagsList) 605 606 if r.RequiredFromJSONSchemaTags { 607 required = requiredFromJSONSchemaTags(jsonSchemaTags) 608 } 609 610 if jsonTagsList[0] != "" { 611 name = jsonTagsList[0] 612 } 613 614 // field not anonymous and not export has no export name 615 if !f.Anonymous && f.PkgPath != "" { 616 name = "" 617 } 618 619 // field anonymous but without json tag should be inherited by current type 620 if f.Anonymous && !exist { 621 name = "" 622 } 623 624 return name, exist, required 625} 626 627func (s *Schema) MarshalJSON() ([]byte, error) { 628 b, err := json.Marshal(s.Type) 629 if err != nil { 630 return nil, err 631 } 632 if s.Definitions == nil || len(s.Definitions) == 0 { 633 return b, nil 634 } 635 d, err := json.Marshal(struct { 636 Definitions Definitions `json:"definitions,omitempty"` 637 }{s.Definitions}) 638 if err != nil { 639 return nil, err 640 } 641 if len(b) == 2 { 642 return d, nil 643 } else { 644 b[len(b)-1] = ',' 645 return append(b, d[1:]...), nil 646 } 647} 648 649func (t *Type) MarshalJSON() ([]byte, error) { 650 type Type_ Type 651 b, err := json.Marshal((*Type_)(t)) 652 if err != nil { 653 return nil, err 654 } 655 if t.Extras == nil || len(t.Extras) == 0 { 656 return b, nil 657 } 658 m, err := json.Marshal(t.Extras) 659 if err != nil { 660 return nil, err 661 } 662 if len(b) == 2 { 663 return m, nil 664 } else { 665 b[len(b)-1] = ',' 666 return append(b, m[1:]...), nil 667 } 668} 669 670func (r *Reflector) typeName(t reflect.Type) string { 671 if r.FullyQualifyTypeNames { 672 return t.PkgPath() + "." + t.Name() 673 } 674 return t.Name() 675} 676