// 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 ( ) // customSchemaImpl is used to detect if the type 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 customSchemaImpl interface { JSONSchema() *Schema } // Function to be run after the schema has been generated. // this will let you modify a schema afterwards type extendSchemaImpl interface { JSONSchemaExtend(*Schema) } // If the object to be reflected defines a `JSONSchemaAlias` method, its type will // be used instead of the original type. type aliasSchemaImpl interface { JSONSchemaAlias() any } // If an object to be reflected defines a `JSONSchemaPropertyAlias` method, // it will be called for each property to determine if another object // should be used for the contents. type propertyAliasSchemaImpl interface { JSONSchemaProperty(prop string) any } var customAliasSchema = reflect.TypeOf((*aliasSchemaImpl)(nil)).Elem() var customPropertyAliasSchema = reflect.TypeOf((*propertyAliasSchemaImpl)(nil)).Elem() var customType = reflect.TypeOf((*customSchemaImpl)(nil)).Elem() var extendType = reflect.TypeOf((*extendSchemaImpl)(nil)).Elem() // customSchemaGetFieldDocString type customSchemaGetFieldDocString interface { GetFieldDocString(fieldName string) string } type customGetFieldDocString func(fieldName string) string var customStructGetFieldDocString = reflect.TypeOf((*customSchemaGetFieldDocString)(nil)).Elem() // Reflect reflects to Schema from a value using the default Reflector func ( any) *Schema { return ReflectFromType(reflect.TypeOf()) } // ReflectFromType generates root schema using the default Reflector func ( reflect.Type) *Schema { := &Reflector{} return .ReflectFromType() } // A Reflector reflects values into a Schema. type Reflector struct { // BaseSchemaID defines the URI that will be used as a base to determine Schema // IDs for models. For example, a base Schema ID of `https://invopop.com/schemas` // when defined with a struct called `User{}`, will result in a schema with an // ID set to `https://invopop.com/schemas/user`. // // If no `BaseSchemaID` is provided, we'll take the type's complete package path // and use that as a base instead. Set `Anonymous` to try if you do not want to // include a schema ID. BaseSchemaID ID // Anonymous when true will hide the auto-generated Schema ID and provide what is // known as an "anonymous schema". As a rule, this is not recommended. Anonymous bool // AssignAnchor when true will use the original struct's name as an anchor inside // every definition, including the root schema. These can be useful for having a // reference to the original struct's name in CamelCase instead of the snake-case used // by default for URI compatibility. // // Anchors do not appear to be widely used out in the wild, so at this time the // anchors themselves will not be used inside generated schema. AssignAnchor bool // AllowAdditionalProperties will cause the Reflector to generate a schema // without additionalProperties set to 'false' 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 // Do not reference definitions. This will remove the top-level $defs map and // instead cause the entire structure of types to be output in one tree. The // list of type definitions (`$defs`) will not be included. DoNotReference bool // ExpandedStruct when true will include the reflected type's definition in the // root as opposed to a definition with a reference. ExpandedStruct bool // FieldNameTag will change the tag used to get field names. json tags are used by default. FieldNameTag string // IgnoredTypes defines a slice of types that should be ignored in the schema, // switching to just allowing additional properties instead. IgnoredTypes []any // Lookup allows a function to be defined that will provide a custom mapping of // types to Schema IDs. This allows existing schema documents to be referenced // by their ID instead of being embedded into the current schema definitions. // Reflected types will never be pointers, only underlying elements. Lookup func(reflect.Type) ID // Mapper is a function that can be used to map custom Go types to jsonschema schemas. Mapper func(reflect.Type) *Schema // Namer allows customizing of type names. The default is to use the type's name // provided by the reflect package. Namer func(reflect.Type) string // KeyNamer allows customizing of key names. // The default is to use the key's name as is, or the json tag if present. // If a json tag is present, KeyNamer will receive the tag's name as an argument, not the original key name. KeyNamer func(string) string // AdditionalFields allows adding structfields for a given type AdditionalFields func(reflect.Type) []reflect.StructField // CommentMap is a dictionary of fully qualified go types and fields to comment // strings that will be used if a description has not already been provided in // the tags. Types and fields are added to the package path using "." as a // separator. // // Type descriptions should be defined like: // // map[string]string{"github.com/invopop/jsonschema.Reflector": "A Reflector reflects values into a Schema."} // // And Fields defined as: // // map[string]string{"github.com/invopop/jsonschema.Reflector.DoNotReference": "Do not reference definitions."} // // See also: AddGoComments CommentMap map[string]string } // Reflect reflects to Schema from a value. func ( *Reflector) ( any) *Schema { return .ReflectFromType(reflect.TypeOf()) } // ReflectFromType generates root schema func ( *Reflector) ( reflect.Type) *Schema { if .Kind() == reflect.Ptr { = .Elem() // re-assign from pointer } := .typeName() := new(Schema) := Definitions{} .Definitions = := .reflectTypeToSchemaWithID(, ) if .ExpandedStruct { * = *[] delete(, ) } else { * = * } // Attempt to set the schema ID if !.Anonymous && .ID == EmptyID { := .BaseSchemaID if == EmptyID { := ID("https://" + .PkgPath()) if := .Validate(); == nil { // it's okay to silently ignore URL errors = } } if != EmptyID { .ID = .Add(ToSnakeCase()) } } .Version = Version if !.DoNotReference { .Definitions = } return } // 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() // SetBaseSchemaID is a helper use to be able to set the reflectors base // schema ID from a string as opposed to then ID instance. func ( *Reflector) ( string) { .BaseSchemaID = ID() } func ( *Reflector) ( Definitions, reflect.Type) *Schema { := .lookupID() if != EmptyID { return &Schema{ Ref: .String(), } } // Already added to definitions? if := .refDefinition(, ); != nil { return } return .reflectTypeToSchemaWithID(, ) } func ( *Reflector) ( Definitions, reflect.Type) *Schema { := .reflectTypeToSchema(, ) if != nil { if .Lookup != nil { := .Lookup() if != EmptyID { .ID = } } } return } func ( *Reflector) ( Definitions, reflect.Type) *Schema { // only try to reflect non-pointers if .Kind() == reflect.Ptr { return .refOrReflectTypeToSchema(, .Elem()) } // Check if the there is an alias method that provides an object // that we should use instead of this one. if .Implements(customAliasSchema) { := reflect.New() := .Interface().(aliasSchemaImpl) = reflect.TypeOf(.JSONSchemaAlias()) return .refOrReflectTypeToSchema(, ) } // Do any pre-definitions exist? if .Mapper != nil { if := .Mapper(); != nil { return } } if := .reflectCustomSchema(, ); != nil { return } // Prepare a base to which details can be added := new(Schema) // jsonpb will marshal protobuf enum options as either strings or integers. // It will unmarshal either. if .Implements(protoEnumType) { .OneOf = []*Schema{ {Type: "string"}, {Type: "integer"}, } return } // 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 == ipType { // TODO differentiate ipv4 and ipv6 RFC section 7.3.4, 7.3.5 .Type = "string" .Format = "ipv4" return } switch .Kind() { case reflect.Struct: .reflectStruct(, , ) case reflect.Slice, reflect.Array: .reflectSliceOrArray(, , ) case reflect.Map: .reflectMap(, , ) case reflect.Interface: // empty case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: .Type = "integer" case reflect.Float32, reflect.Float64: .Type = "number" case reflect.Bool: .Type = "boolean" case reflect.String: .Type = "string" default: panic("unsupported type " + .String()) } .reflectSchemaExtend(, , ) // Always try to reference the definition which may have just been created if := .refDefinition(, ); != nil { return } return } func ( *Reflector) ( Definitions, reflect.Type) *Schema { if .Kind() == reflect.Ptr { return .(, .Elem()) } if .Implements(customType) { := reflect.New() := .Interface().(customSchemaImpl) := .JSONSchema() .addDefinition(, , ) if := .refDefinition(, ); != nil { return } return } return nil } func ( *Reflector) ( Definitions, reflect.Type, *Schema) *Schema { if .Implements(extendType) { := reflect.New() := .Interface().(extendSchemaImpl) .JSONSchemaExtend() if := .refDefinition(, ); != nil { return } } return } func ( *Reflector) ( Definitions, reflect.Type, *Schema) { if == rawMessageType { return } .addDefinition(, , ) if .Description == "" { .Description = .lookupComment(, "") } if .Kind() == reflect.Array { := uint64(.Len()) .MinItems = & .MaxItems = & } if .Kind() == reflect.Slice && .Elem() == byteSliceType.Elem() { .Type = "string" // NOTE: ContentMediaType is not set here .ContentEncoding = "base64" } else { .Type = "array" .Items = .refOrReflectTypeToSchema(, .Elem()) } } func ( *Reflector) ( Definitions, reflect.Type, *Schema) { .addDefinition(, , ) .Type = "object" if .Description == "" { .Description = .lookupComment(, "") } switch .Key().Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: .PatternProperties = map[string]*Schema{ "^[0-9]+$": .refOrReflectTypeToSchema(, .Elem()), } .AdditionalProperties = FalseSchema return } if .Elem().Kind() != reflect.Interface { .AdditionalProperties = .refOrReflectTypeToSchema(, .Elem()) } } // Reflects a struct to a JSON Schema type. func ( *Reflector) ( Definitions, reflect.Type, *Schema) { // Handle special types switch { case timeType: // date-time RFC section 7.3.1 .Type = "string" .Format = "date-time" return case uriType: // uri RFC section 7.3.6 .Type = "string" .Format = "uri" return } .addDefinition(, , ) .Type = "object" .Properties = NewProperties() .Description = .lookupComment(, "") if .AssignAnchor { .Anchor = .Name() } if !.AllowAdditionalProperties && .AdditionalProperties == nil { .AdditionalProperties = FalseSchema } := false for , := range .IgnoredTypes { if reflect.TypeOf() == { = true break } } if ! { .reflectStructFields(, , ) } } func ( *Reflector) ( *Schema, Definitions, reflect.Type) { if .Kind() == reflect.Ptr { = .Elem() } if .Kind() != reflect.Struct { return } var customGetFieldDocString if .Implements(customStructGetFieldDocString) { := reflect.New() := .Interface().(customSchemaGetFieldDocString) = .GetFieldDocString } := func(string) any { return nil } if .Implements(customPropertyAliasSchema) { := reflect.New() := .Interface().(propertyAliasSchemaImpl) = .JSONSchemaProperty } := func( reflect.StructField) { , , , := .reflectFieldName() // if anonymous and exported type should be processed recursively // current type should inherit properties of anonymous one if == "" { if { .(, , .Type) } return } // If a JSONSchemaAlias(prop string) method is defined, attempt to use // the provided object's type instead of the field's type. var *Schema if := (); != nil { = .refOrReflectTypeToSchema(, reflect.TypeOf()) } else { = .refOrReflectTypeToSchema(, .Type) } .structKeywordsFromTags(, , ) if .Description == "" { .Description = .lookupComment(, .Name) } if != nil { .Description = (.Name) } if { = &Schema{ OneOf: []*Schema{ , { Type: "null", }, }, } } .Properties.Set(, ) if { .Required = appendUniqueString(.Required, ) } } for := 0; < .NumField(); ++ { := .Field() () } if .AdditionalFields != nil { if := .AdditionalFields(); != nil { for , := range { () } } } } func appendUniqueString( []string, string) []string { for , := range { if == { return } } return append(, ) } func ( *Reflector) ( reflect.Type, string) string { if .CommentMap == nil { return "" } := fullyQualifiedTypeName() if != "" { = + "." + } return .CommentMap[] } // addDefinition will append the provided schema. If needed, an ID and anchor will also be added. func ( *Reflector) ( Definitions, reflect.Type, *Schema) { := .typeName() if == "" { return } [] = } // refDefinition will provide a schema with a reference to an existing definition. func ( *Reflector) ( Definitions, reflect.Type) *Schema { if .DoNotReference { return nil } := .typeName() if == "" { return nil } if , := []; ! { return nil } return &Schema{ Ref: "#/$defs/" + , } } func ( *Reflector) ( reflect.Type) ID { if .Lookup != nil { if .Kind() == reflect.Ptr { = .Elem() } return .Lookup() } return EmptyID } func ( *Schema) ( reflect.StructField, *Schema, string) { .Description = .Tag.Get("jsonschema_description") := splitOnUnescapedCommas(.Tag.Get("jsonschema")) = .genericKeywords(, , ) switch .Type { case "string": .stringKeywords() case "number": .numericalKeywords() case "integer": .numericalKeywords() case "array": .arrayKeywords() case "boolean": .booleanKeywords() } := strings.Split(.Tag.Get("jsonschema_extras"), ",") .extraKeywords() } // read struct tags for generic keywords func ( *Schema) ( []string, *Schema, string) []string { //nolint:gocyclo := make([]string, 0, len()) for , := range { := strings.SplitN(, "=", 2) if len() == 2 { , := [0], [1] switch { case "title": .Title = case "description": .Description = case "type": .Type = case "anchor": .Anchor = case "oneof_required": var *Schema for := range .OneOf { if .OneOf[].Title == [1] { = .OneOf[] } } if == nil { = &Schema{ Title: [1], Required: []string{}, } .OneOf = append(.OneOf, ) } .Required = append(.Required, ) case "anyof_required": var *Schema for := range .AnyOf { if .AnyOf[].Title == [1] { = .AnyOf[] } } if == nil { = &Schema{ Title: [1], Required: []string{}, } .AnyOf = append(.AnyOf, ) } .Required = append(.Required, ) case "oneof_ref": := if .Items != nil { = .Items } if .OneOf == nil { .OneOf = make([]*Schema, 0, 1) } .Ref = "" := strings.Split([1], ";") for , := range { .OneOf = append(.OneOf, &Schema{ Ref: , }) } case "oneof_type": if .OneOf == nil { .OneOf = make([]*Schema, 0, 1) } .Type = "" := strings.Split([1], ";") for , := range { .OneOf = append(.OneOf, &Schema{ Type: , }) } case "anyof_ref": := if .Items != nil { = .Items } if .AnyOf == nil { .AnyOf = make([]*Schema, 0, 1) } .Ref = "" := strings.Split([1], ";") for , := range { .AnyOf = append(.AnyOf, &Schema{ Ref: , }) } case "anyof_type": if .AnyOf == nil { .AnyOf = make([]*Schema, 0, 1) } .Type = "" := strings.Split([1], ";") for , := range { .AnyOf = append(.AnyOf, &Schema{ Type: , }) } default: = append(, ) } } } return } // read struct tags for boolean type keywords func ( *Schema) ( []string) { for , := range { := strings.Split(, "=") if len() != 2 { continue } , := [0], [1] if == "default" { if == "true" { .Default = true } else if == "false" { .Default = false } } } } // read struct tags for string type keywords func ( *Schema) ( []string) { for , := range { := strings.SplitN(, "=", 2) if len() == 2 { , := [0], [1] switch { case "minLength": .MinLength = parseUint() case "maxLength": .MaxLength = parseUint() case "pattern": .Pattern = case "format": switch { case "date-time", "email", "hostname", "ipv4", "ipv6", "uri", "uuid": .Format = } case "readOnly": , := strconv.ParseBool() .ReadOnly = case "writeOnly": , := strconv.ParseBool() .WriteOnly = case "default": .Default = case "example": .Examples = append(.Examples, ) case "enum": .Enum = append(.Enum, ) } } } } // read struct tags for numerical type keywords func ( *Schema) ( []string) { for , := range { := strings.Split(, "=") if len() == 2 { , := [0], [1] switch { case "multipleOf": .MultipleOf, _ = toJSONNumber() case "minimum": .Minimum, _ = toJSONNumber() case "maximum": .Maximum, _ = toJSONNumber() case "exclusiveMaximum": .ExclusiveMaximum, _ = toJSONNumber() case "exclusiveMinimum": .ExclusiveMinimum, _ = toJSONNumber() case "default": if , := toJSONNumber(); { .Default = } case "example": if , := toJSONNumber(); { .Examples = append(.Examples, ) } case "enum": if , := toJSONNumber(); { .Enum = append(.Enum, ) } } } } } // read struct tags for object type keywords // 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 keywords func ( *Schema) ( []string) { var []any := make([]string, 0, len()) for , := range { := strings.Split(, "=") if len() == 2 { , := [0], [1] switch { case "minItems": .MinItems = parseUint() case "maxItems": .MaxItems = parseUint() case "uniqueItems": .UniqueItems = true case "default": = append(, ) case "format": .Items.Format = case "pattern": .Items.Pattern = default: = append(, ) // left for further processing by underlying type } } } if len() > 0 { .Default = } if len() == 0 { // we don't have anything else to process return } switch .Items.Type { case "string": .Items.stringKeywords() case "number": .Items.numericalKeywords() case "integer": .Items.numericalKeywords() case "array": // explicitly don't support traversal for the [][]..., as it's unclear where the array tags belong case "boolean": .Items.booleanKeywords() } } func ( *Schema) ( []string) { for , := range { := strings.SplitN(, "=", 2) if len() == 2 { .setExtra([0], [1]) } } } func ( *Schema) (, string) { if .Extras == nil { .Extras = map[string]any{} } if , := .Extras[]; { switch existingVal := .(type) { case string: .Extras[] = []string{, } case []string: .Extras[] = append(, ) case int: .Extras[], _ = strconv.Atoi() case bool: .Extras[] = ( == "true" || == "t") } } else { switch { case "minimum": .Extras[], _ = strconv.Atoi() default: var any if == "true" { = true } else if == "false" { = false } else { = } .Extras[] = } } } func requiredFromJSONTags( []string, *bool) { if ignoredByJSONTags() { return } for , := range [1:] { if == "omitempty" { * = false return } } * = true } func requiredFromJSONSchemaTags( []string, *bool) { if ignoredByJSONSchemaTags() { return } for , := range { if == "required" { * = true } } } func nullableFromJSONSchemaTags( []string) bool { if ignoredByJSONSchemaTags() { return false } for , := range { if == "nullable" { return true } } return false } func ignoredByJSONTags( []string) bool { return [0] == "-" } func ignoredByJSONSchemaTags( []string) bool { return [0] == "-" } // toJSONNumber converts string to *json.Number. // It'll aso return whether the number is valid. func toJSONNumber( string) (json.Number, bool) { := json.Number() if , := .Int64(); == nil { return , true } if , := .Float64(); == nil { return , true } return json.Number(""), false } func parseUint( string) *uint64 { , := strconv.ParseUint(, 10, 64) if != nil { return nil } return & } func ( *Reflector) () string { if .FieldNameTag != "" { return .FieldNameTag } return "json" } func ( *Reflector) ( reflect.StructField) (string, bool, bool, bool) { := .Tag.Get(.fieldNameTag()) := strings.Split(, ",") if ignoredByJSONTags() { return "", false, false, false } := strings.Split(.Tag.Get("jsonschema"), ",") if ignoredByJSONSchemaTags() { return "", false, false, false } var bool if !.RequiredFromJSONSchemaTags { requiredFromJSONTags(, &) } requiredFromJSONSchemaTags(, &) := nullableFromJSONSchemaTags() if .Anonymous && [0] == "" { // As per JSON Marshal rules, anonymous structs are inherited if .Type.Kind() == reflect.Struct { return "", true, false, false } // As per JSON Marshal rules, anonymous pointer to structs are inherited if .Type.Kind() == reflect.Ptr && .Type.Elem().Kind() == reflect.Struct { return "", true, false, false } } // Try to determine the name from the different combos := .Name if [0] != "" { = [0] } if !.Anonymous && .PkgPath != "" { // field not anonymous and not export has no export name = "" } else if .KeyNamer != nil { = .KeyNamer() } return , false, , } // UnmarshalJSON is used to parse a schema object or boolean. func ( *Schema) ( []byte) error { if bytes.Equal(, []byte("true")) { * = *TrueSchema return nil } else if bytes.Equal(, []byte("false")) { * = *FalseSchema return nil } type Schema := &struct { * }{ : (*)(), } return json.Unmarshal(, ) } // MarshalJSON is used to serialize a schema object or boolean. func ( *Schema) () ([]byte, error) { if .boolean != nil { if *.boolean { return []byte("true"), nil } return []byte("false"), nil } if reflect.DeepEqual(&Schema{}, ) { // Don't bother returning empty schemas return []byte("true"), nil } type Schema , := json.Marshal((*)()) if != nil { return nil, } if .Extras == nil || len(.Extras) == 0 { return , nil } , := json.Marshal(.Extras) if != nil { return nil, } if len() == 2 { return , nil } [len()-1] = ',' return append(, [1:]...), nil } func ( *Reflector) ( reflect.Type) string { if .Namer != nil { if := .Namer(); != "" { return } } return .Name() } // Split on commas that are not preceded by `\`. // This way, we prevent splitting regexes func splitOnUnescapedCommas( string) []string { := make([]string, 0) := strings.Split(, ",") = append(, [0]) := 0 for , := range [1:] { if len([]) == 0 { = append(, ) ++ continue } if [][len([])-1] == '\\' { [] = [][:len([])-1] + "," + } else { = append(, ) ++ } } return } func fullyQualifiedTypeName( reflect.Type) string { return .PkgPath() + "." + .Name() } // AddGoComments will update the reflectors comment map with all the comments // found in the provided source directories. See the #ExtractGoComments method // for more details. func ( *Reflector) (, string) error { if .CommentMap == nil { .CommentMap = make(map[string]string) } return ExtractGoComments(, , .CommentMap) }