// Package jsonschema uses reflection to generate JSON Schemas from Go types [1]. // // If json tags are present on struct fields, they will be used to infer // property names and if a property is required (omitempty is present). // // [1] http://json-schema.org/latest/json-schema-validation.html package jsonschema import ( "encoding/json" "net" "net/url" "reflect" "strconv" "strings" "time" "github.com/iancoleman/orderedmap" ) // Version is the JSON Schema version. // If extending JSON Schema with custom values use a custom URI. // RFC draft-wright-json-schema-00, section 6 var Version = "http://json-schema.org/draft-04/schema#" // Schema is the root schema. // RFC draft-wright-json-schema-00, section 4.5 type Schema struct { *Type Definitions Definitions } // customSchemaType is used to detect if the structure provides it's own // custom Schema Type definition to use instead. Very useful for situations // where there are custom JSON Marshal and Unmarshal methods. type customSchemaType interface { JSONSchemaType() *Type } var customStructType = reflect.TypeOf((*customSchemaType)(nil)).Elem() // customSchemaGetFieldDocString type customSchemaGetFieldDocString interface { GetFieldDocString(fieldName string) string } type customGetFieldDocString func(fieldName string) string var customStructGetFieldDocString = reflect.TypeOf((*customSchemaGetFieldDocString)(nil)).Elem() // Type represents a JSON Schema object type. type Type struct { // RFC draft-wright-json-schema-00 Version string `json:"$schema,omitempty"` // section 6.1 Ref string `json:"$ref,omitempty"` // section 7 // RFC draft-wright-json-schema-validation-00, section 5 MultipleOf int `json:"multipleOf,omitempty"` // section 5.1 Maximum int `json:"maximum,omitempty"` // section 5.2 ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` // section 5.3 Minimum int `json:"minimum,omitempty"` // section 5.4 ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` // section 5.5 MaxLength int `json:"maxLength,omitempty"` // section 5.6 MinLength int `json:"minLength,omitempty"` // section 5.7 Pattern string `json:"pattern,omitempty"` // section 5.8 AdditionalItems *Type `json:"additionalItems,omitempty"` // section 5.9 Items *Type `json:"items,omitempty"` // section 5.9 MaxItems int `json:"maxItems,omitempty"` // section 5.10 MinItems int `json:"minItems,omitempty"` // section 5.11 UniqueItems bool `json:"uniqueItems,omitempty"` // section 5.12 MaxProperties int `json:"maxProperties,omitempty"` // section 5.13 MinProperties int `json:"minProperties,omitempty"` // section 5.14 Required []string `json:"required,omitempty"` // section 5.15 Properties *orderedmap.OrderedMap `json:"properties,omitempty"` // section 5.16 PatternProperties map[string]*Type `json:"patternProperties,omitempty"` // section 5.17 AdditionalProperties json.RawMessage `json:"additionalProperties,omitempty"` // section 5.18 Dependencies map[string]*Type `json:"dependencies,omitempty"` // section 5.19 Enum []interface{} `json:"enum,omitempty"` // section 5.20 Type string `json:"type,omitempty"` // section 5.21 AllOf []*Type `json:"allOf,omitempty"` // section 5.22 AnyOf []*Type `json:"anyOf,omitempty"` // section 5.23 OneOf []*Type `json:"oneOf,omitempty"` // section 5.24 Not *Type `json:"not,omitempty"` // section 5.25 Definitions Definitions `json:"definitions,omitempty"` // section 5.26 // RFC draft-wright-json-schema-validation-00, section 6, 7 Title string `json:"title,omitempty"` // section 6.1 Description string `json:"description,omitempty"` // section 6.1 Default interface{} `json:"default,omitempty"` // section 6.2 Format string `json:"format,omitempty"` // section 7 Examples []interface{} `json:"examples,omitempty"` // section 7.4 // RFC draft-wright-json-schema-hyperschema-00, section 4 Media *Type `json:"media,omitempty"` // section 4.3 BinaryEncoding string `json:"binaryEncoding,omitempty"` // section 4.3 Extras map[string]interface{} `json:"-"` } // Reflect reflects to Schema from a value using the default Reflector func Reflect(v interface{}) *Schema { return ReflectFromType(reflect.TypeOf(v)) } // ReflectFromType generates root schema using the default Reflector func ReflectFromType(t reflect.Type) *Schema { r := &Reflector{} return r.ReflectFromType(t) } // A Reflector reflects values into a Schema. type Reflector struct { // AllowAdditionalProperties will cause the Reflector to generate a schema // with additionalProperties to 'true' for all struct types. This means // the presence of additional keys in JSON objects will not cause validation // to fail. Note said additional keys will simply be dropped when the // validated JSON is unmarshaled. AllowAdditionalProperties bool // RequiredFromJSONSchemaTags will cause the Reflector to generate a schema // that requires any key tagged with `jsonschema:required`, overriding the // default of requiring any key *not* tagged with `json:,omitempty`. RequiredFromJSONSchemaTags bool // YAMLEmbeddedStructs will cause the Reflector to generate a schema that does // not inline embedded structs. This should be enabled if the JSON schemas are // used with yaml.Marshal/Unmarshal. YAMLEmbeddedStructs bool // Prefer yaml: tags over json: tags to generate the schema even if json: tags // are present PreferYAMLSchema bool // ExpandedStruct will cause the toplevel definitions of the schema not // be referenced itself to a definition. ExpandedStruct bool // Do not reference definitions. // All types are still registered under the "definitions" top-level object, // but instead of $ref fields in containing types, the entire definition // of the contained type is inserted. // This will cause the entire structure of types to be output in one tree. DoNotReference bool // Use package paths as well as type names, to avoid conflicts. // Without this setting, if two packages contain a type with the same name, // and both are present in a schema, they will conflict and overwrite in // the definition map and produce bad output. This is particularly // noticeable when using DoNotReference. FullyQualifyTypeNames bool // IgnoredTypes defines a slice of types that should be ignored in the schema, // switching to just allowing additional properties instead. IgnoredTypes []interface{} // TypeMapper is a function that can be used to map custom Go types to jsconschema types. TypeMapper func(reflect.Type) *Type // TypeNamer allows customizing of type names TypeNamer func(reflect.Type) string // AdditionalFields allows adding structfields for a given type AdditionalFields func(reflect.Type) []reflect.StructField } // Reflect reflects to Schema from a value. func (r *Reflector) Reflect(v interface{}) *Schema { return r.ReflectFromType(reflect.TypeOf(v)) } // ReflectFromType generates root schema func (r *Reflector) ReflectFromType(t reflect.Type) *Schema { definitions := Definitions{} if r.ExpandedStruct { st := &Type{ Version: Version, Type: "object", Properties: orderedmap.New(), AdditionalProperties: []byte("false"), } if r.AllowAdditionalProperties { st.AdditionalProperties = []byte("true") } r.reflectStructFields(st, definitions, t) r.reflectStruct(definitions, t) delete(definitions, r.typeName(t)) return &Schema{Type: st, Definitions: definitions} } s := &Schema{ Type: r.reflectTypeToSchema(definitions, t), Definitions: definitions, } return s } // Definitions hold schema definitions. // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.26 // RFC draft-wright-json-schema-validation-00, section 5.26 type Definitions map[string]*Type // Available Go defined types for JSON Schema Validation. // RFC draft-wright-json-schema-validation-00, section 7.3 var ( timeType = reflect.TypeOf(time.Time{}) // date-time RFC section 7.3.1 ipType = reflect.TypeOf(net.IP{}) // ipv4 and ipv6 RFC section 7.3.4, 7.3.5 uriType = reflect.TypeOf(url.URL{}) // uri RFC section 7.3.6 ) // Byte slices will be encoded as base64 var byteSliceType = reflect.TypeOf([]byte(nil)) // Except for json.RawMessage var rawMessageType = reflect.TypeOf(json.RawMessage{}) // Go code generated from protobuf enum types should fulfil this interface. type protoEnum interface { EnumDescriptor() ([]byte, []int) } var protoEnumType = reflect.TypeOf((*protoEnum)(nil)).Elem() func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) *Type { // Already added to definitions? if _, ok := definitions[r.typeName(t)]; ok && !r.DoNotReference { return &Type{Ref: "#/definitions/" + r.typeName(t)} } // jsonpb will marshal protobuf enum options as either strings or integers. // It will unmarshal either. if t.Implements(protoEnumType) { return &Type{OneOf: []*Type{ {Type: "string"}, {Type: "integer"}, }} } if r.TypeMapper != nil { if t := r.TypeMapper(t); t != nil { return t } } // Defined format types for JSON Schema Validation // RFC draft-wright-json-schema-validation-00, section 7.3 // TODO email RFC section 7.3.2, hostname RFC section 7.3.3, uriref RFC section 7.3.7 if t == ipType { // TODO differentiate ipv4 and ipv6 RFC section 7.3.4, 7.3.5 return &Type{Type: "string", Format: "ipv4"} // ipv4 RFC section 7.3.4 } switch t.Kind() { case reflect.Struct: switch t { case timeType: // date-time RFC section 7.3.1 return &Type{Type: "string", Format: "date-time"} case uriType: // uri RFC section 7.3.6 return &Type{Type: "string", Format: "uri"} default: return r.reflectStruct(definitions, t) } case reflect.Map: if t.Implements(customStructType) { v := reflect.New(t) o := v.Interface().(customSchemaType) return o.JSONSchemaType() } switch t.Key().Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: rt := &Type{ Type: "object", PatternProperties: map[string]*Type{ "^[0-9]+$": r.reflectTypeToSchema(definitions, t.Elem()), }, AdditionalProperties: []byte("false"), } return rt } rt := &Type{ Type: "object", PatternProperties: map[string]*Type{ ".*": r.reflectTypeToSchema(definitions, t.Elem()), }, } delete(rt.PatternProperties, "additionalProperties") return rt case reflect.Slice, reflect.Array: returnType := &Type{} if t.Implements(customStructType) { v := reflect.New(t) o := v.Interface().(customSchemaType) return o.JSONSchemaType() } if t == rawMessageType { return &Type{ AdditionalProperties: []byte("true"), } } if t.Kind() == reflect.Array { returnType.MinItems = t.Len() returnType.MaxItems = returnType.MinItems } if t.Kind() == reflect.Slice && t.Elem() == byteSliceType.Elem() { returnType.Type = "string" returnType.Media = &Type{BinaryEncoding: "base64"} return returnType } returnType.Type = "array" returnType.Items = r.reflectTypeToSchema(definitions, t.Elem()) return returnType case reflect.Interface: return &Type{ AdditionalProperties: []byte("true"), } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return &Type{Type: "integer"} case reflect.Float32, reflect.Float64: return &Type{Type: "number"} case reflect.Bool: return &Type{Type: "boolean"} case reflect.String: return &Type{Type: "string"} case reflect.Ptr: return r.reflectTypeToSchema(definitions, t.Elem()) } panic("unsupported type " + t.String()) } // Refects a struct to a JSON Schema type. func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type) *Type { for _, ignored := range r.IgnoredTypes { if reflect.TypeOf(ignored) == t { st := &Type{ Type: "object", Properties: orderedmap.New(), AdditionalProperties: []byte("true"), } definitions[r.typeName(t)] = st if r.DoNotReference { return st } else { return &Type{ Version: Version, Ref: "#/definitions/" + r.typeName(t), } } } } var st *Type if t.Implements(customStructType) { v := reflect.New(t) o := v.Interface().(customSchemaType) st = o.JSONSchemaType() definitions[r.typeName(t)] = st } else { st = &Type{ Type: "object", Properties: orderedmap.New(), AdditionalProperties: []byte("false"), } if r.AllowAdditionalProperties { st.AdditionalProperties = []byte("true") } definitions[r.typeName(t)] = st r.reflectStructFields(st, definitions, t) } if r.DoNotReference { return st } else { return &Type{ Version: Version, Ref: "#/definitions/" + r.typeName(t), } } } func (r *Reflector) reflectStructFields(st *Type, definitions Definitions, t reflect.Type) { if t.Kind() == reflect.Ptr { t = t.Elem() } if t.Kind() != reflect.Struct { return } var getFieldDocString customGetFieldDocString if t.Implements(customStructGetFieldDocString) { v := reflect.New(t) o := v.Interface().(customSchemaGetFieldDocString) getFieldDocString = o.GetFieldDocString } handleField := func(f reflect.StructField) { name, shouldEmbed, required, nullable := r.reflectFieldName(f) // if anonymous and exported type should be processed recursively // current type should inherit properties of anonymous one if name == "" { if shouldEmbed { r.reflectStructFields(st, definitions, f.Type) } return } property := r.reflectTypeToSchema(definitions, f.Type) property.structKeywordsFromTags(f, st, name) if getFieldDocString != nil { property.Description = getFieldDocString(f.Name) } if nullable { property = &Type{ OneOf: []*Type{ property, { Type: "null", }, }, } } st.Properties.Set(name, property) if required { st.Required = append(st.Required, name) } } for i := 0; i < t.NumField(); i++ { f := t.Field(i) handleField(f) } if r.AdditionalFields != nil { if af := r.AdditionalFields(t); af != nil { for _, sf := range af { handleField(sf) } } } } func (t *Type) structKeywordsFromTags(f reflect.StructField, parentType *Type, propertyName string) { t.Description = f.Tag.Get("jsonschema_description") tags := strings.Split(f.Tag.Get("jsonschema"), ",") t.genericKeywords(tags, parentType, propertyName) switch t.Type { case "string": t.stringKeywords(tags) case "number": t.numbericKeywords(tags) case "integer": t.numbericKeywords(tags) case "array": t.arrayKeywords(tags) } extras := strings.Split(f.Tag.Get("jsonschema_extras"), ",") t.extraKeywords(extras) } // read struct tags for generic keyworks func (t *Type) genericKeywords(tags []string, parentType *Type, propertyName string) { for _, tag := range tags { nameValue := strings.Split(tag, "=") if len(nameValue) == 2 { name, val := nameValue[0], nameValue[1] switch name { case "title": t.Title = val case "description": t.Description = val case "type": t.Type = val case "oneof_required": var typeFound *Type for i := range parentType.OneOf { if parentType.OneOf[i].Title == nameValue[1] { typeFound = parentType.OneOf[i] } } if typeFound == nil { typeFound = &Type{ Title: nameValue[1], Required: []string{}, } parentType.OneOf = append(parentType.OneOf, typeFound) } typeFound.Required = append(typeFound.Required, propertyName) case "oneof_type": if t.OneOf == nil { t.OneOf = make([]*Type, 0, 1) } t.Type = "" types := strings.Split(nameValue[1], ";") for _, ty := range types { t.OneOf = append(t.OneOf, &Type{ Type: ty, }) } case "enum": switch t.Type { case "string": t.Enum = append(t.Enum, val) case "integer": i, _ := strconv.Atoi(val) t.Enum = append(t.Enum, i) case "number": f, _ := strconv.ParseFloat(val, 64) t.Enum = append(t.Enum, f) } } } } } // read struct tags for string type keyworks func (t *Type) stringKeywords(tags []string) { for _, tag := range tags { nameValue := strings.Split(tag, "=") if len(nameValue) == 2 { name, val := nameValue[0], nameValue[1] switch name { case "minLength": i, _ := strconv.Atoi(val) t.MinLength = i case "maxLength": i, _ := strconv.Atoi(val) t.MaxLength = i case "pattern": t.Pattern = val case "format": switch val { case "date-time", "email", "hostname", "ipv4", "ipv6", "uri": t.Format = val break } case "default": t.Default = val case "example": t.Examples = append(t.Examples, val) } } } } // read struct tags for numberic type keyworks func (t *Type) numbericKeywords(tags []string) { for _, tag := range tags { nameValue := strings.Split(tag, "=") if len(nameValue) == 2 { name, val := nameValue[0], nameValue[1] switch name { case "multipleOf": i, _ := strconv.Atoi(val) t.MultipleOf = i case "minimum": i, _ := strconv.Atoi(val) t.Minimum = i case "maximum": i, _ := strconv.Atoi(val) t.Maximum = i case "exclusiveMaximum": b, _ := strconv.ParseBool(val) t.ExclusiveMaximum = b case "exclusiveMinimum": b, _ := strconv.ParseBool(val) t.ExclusiveMinimum = b case "default": i, _ := strconv.Atoi(val) t.Default = i case "example": if i, err := strconv.Atoi(val); err == nil { t.Examples = append(t.Examples, i) } } } } } // read struct tags for object type keyworks // func (t *Type) objectKeywords(tags []string) { // for _, tag := range tags{ // nameValue := strings.Split(tag, "=") // name, val := nameValue[0], nameValue[1] // switch name{ // case "dependencies": // t.Dependencies = val // break; // case "patternProperties": // t.PatternProperties = val // break; // } // } // } // read struct tags for array type keyworks func (t *Type) arrayKeywords(tags []string) { var defaultValues []interface{} for _, tag := range tags { nameValue := strings.Split(tag, "=") if len(nameValue) == 2 { name, val := nameValue[0], nameValue[1] switch name { case "minItems": i, _ := strconv.Atoi(val) t.MinItems = i case "maxItems": i, _ := strconv.Atoi(val) t.MaxItems = i case "uniqueItems": t.UniqueItems = true case "default": defaultValues = append(defaultValues, val) case "enum": switch t.Items.Type { case "string": t.Items.Enum = append(t.Items.Enum, val) case "integer": i, _ := strconv.Atoi(val) t.Items.Enum = append(t.Items.Enum, i) case "number": f, _ := strconv.ParseFloat(val, 64) t.Items.Enum = append(t.Items.Enum, f) } } } } if len(defaultValues) > 0 { t.Default = defaultValues } } func (t *Type) extraKeywords(tags []string) { for _, tag := range tags { nameValue := strings.Split(tag, "=") if len(nameValue) == 2 { t.setExtra(nameValue[0], nameValue[1]) } } } func (t *Type) setExtra(key, val string) { if t.Extras == nil { t.Extras = map[string]interface{}{} } if existingVal, ok := t.Extras[key]; ok { switch existingVal := existingVal.(type) { case string: t.Extras[key] = []string{existingVal, val} case []string: t.Extras[key] = append(existingVal, val) case int: t.Extras[key], _ = strconv.Atoi(val) } } else { switch key { case "minimum": t.Extras[key], _ = strconv.Atoi(val) default: t.Extras[key] = val } } } func requiredFromJSONTags(tags []string) bool { if ignoredByJSONTags(tags) { return false } for _, tag := range tags[1:] { if tag == "omitempty" { return false } } return true } func requiredFromJSONSchemaTags(tags []string) bool { if ignoredByJSONSchemaTags(tags) { return false } for _, tag := range tags { if tag == "required" { return true } } return false } func nullableFromJSONSchemaTags(tags []string) bool { if ignoredByJSONSchemaTags(tags) { return false } for _, tag := range tags { if tag == "nullable" { return true } } return false } func inlineYAMLTags(tags []string) bool { for _, tag := range tags { if tag == "inline" { return true } } return false } func ignoredByJSONTags(tags []string) bool { return tags[0] == "-" } func ignoredByJSONSchemaTags(tags []string) bool { return tags[0] == "-" } func (r *Reflector) reflectFieldName(f reflect.StructField) (string, bool, bool, bool) { jsonTags, exist := f.Tag.Lookup("json") yamlTags, yamlExist := f.Tag.Lookup("yaml") if !exist || r.PreferYAMLSchema { jsonTags = yamlTags exist = yamlExist } jsonTagsList := strings.Split(jsonTags, ",") yamlTagsList := strings.Split(yamlTags, ",") if ignoredByJSONTags(jsonTagsList) { return "", false, false, false } jsonSchemaTags := strings.Split(f.Tag.Get("jsonschema"), ",") if ignoredByJSONSchemaTags(jsonSchemaTags) { return "", false, false, false } name := f.Name required := requiredFromJSONTags(jsonTagsList) if r.RequiredFromJSONSchemaTags { required = requiredFromJSONSchemaTags(jsonSchemaTags) } nullable := nullableFromJSONSchemaTags(jsonSchemaTags) if jsonTagsList[0] != "" { name = jsonTagsList[0] } // field not anonymous and not export has no export name if !f.Anonymous && f.PkgPath != "" { name = "" } embed := false // field anonymous but without json tag should be inherited by current type if f.Anonymous && !exist { if !r.YAMLEmbeddedStructs { name = "" embed = true } else { name = strings.ToLower(name) } } if yamlExist && inlineYAMLTags(yamlTagsList) { name = "" embed = true } return name, embed, required, nullable } func (s *Schema) MarshalJSON() ([]byte, error) { b, err := json.Marshal(s.Type) if err != nil { return nil, err } if s.Definitions == nil || len(s.Definitions) == 0 { return b, nil } d, err := json.Marshal(struct { Definitions Definitions `json:"definitions,omitempty"` }{s.Definitions}) if err != nil { return nil, err } if len(b) == 2 { return d, nil } else { b[len(b)-1] = ',' return append(b, d[1:]...), nil } } func (t *Type) MarshalJSON() ([]byte, error) { type Type_ Type b, err := json.Marshal((*Type_)(t)) if err != nil { return nil, err } if t.Extras == nil || len(t.Extras) == 0 { return b, nil } m, err := json.Marshal(t.Extras) if err != nil { return nil, err } if len(b) == 2 { return m, nil } else { b[len(b)-1] = ',' return append(b, m[1:]...), nil } } func (r *Reflector) typeName(t reflect.Type) string { if r.TypeNamer != nil { if name := r.TypeNamer(t); name != "" { return name } } if r.FullyQualifyTypeNames { return t.PkgPath() + "." + t.Name() } return t.Name() }