838 lines
24 KiB
Go
838 lines
24 KiB
Go
|
// 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 jsonschema 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()
|
||
|
}
|