reflect.go

   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	"bytes"
  11	"encoding/json"
  12	"net"
  13	"net/url"
  14	"reflect"
  15	"strconv"
  16	"strings"
  17	"time"
  18)
  19
  20// customSchemaImpl is used to detect if the type provides it's own
  21// custom Schema Type definition to use instead. Very useful for situations
  22// where there are custom JSON Marshal and Unmarshal methods.
  23type customSchemaImpl interface {
  24	JSONSchema() *Schema
  25}
  26
  27// Function to be run after the schema has been generated.
  28// this will let you modify a schema afterwards
  29type extendSchemaImpl interface {
  30	JSONSchemaExtend(*Schema)
  31}
  32
  33// If the object to be reflected defines a `JSONSchemaAlias` method, its type will
  34// be used instead of the original type.
  35type aliasSchemaImpl interface {
  36	JSONSchemaAlias() any
  37}
  38
  39// If an object to be reflected defines a `JSONSchemaPropertyAlias` method,
  40// it will be called for each property to determine if another object
  41// should be used for the contents.
  42type propertyAliasSchemaImpl interface {
  43	JSONSchemaProperty(prop string) any
  44}
  45
  46var customAliasSchema = reflect.TypeOf((*aliasSchemaImpl)(nil)).Elem()
  47var customPropertyAliasSchema = reflect.TypeOf((*propertyAliasSchemaImpl)(nil)).Elem()
  48
  49var customType = reflect.TypeOf((*customSchemaImpl)(nil)).Elem()
  50var extendType = reflect.TypeOf((*extendSchemaImpl)(nil)).Elem()
  51
  52// customSchemaGetFieldDocString
  53type customSchemaGetFieldDocString interface {
  54	GetFieldDocString(fieldName string) string
  55}
  56
  57type customGetFieldDocString func(fieldName string) string
  58
  59var customStructGetFieldDocString = reflect.TypeOf((*customSchemaGetFieldDocString)(nil)).Elem()
  60
  61// Reflect reflects to Schema from a value using the default Reflector
  62func Reflect(v any) *Schema {
  63	return ReflectFromType(reflect.TypeOf(v))
  64}
  65
  66// ReflectFromType generates root schema using the default Reflector
  67func ReflectFromType(t reflect.Type) *Schema {
  68	r := &Reflector{}
  69	return r.ReflectFromType(t)
  70}
  71
  72// A Reflector reflects values into a Schema.
  73type Reflector struct {
  74	// BaseSchemaID defines the URI that will be used as a base to determine Schema
  75	// IDs for models. For example, a base Schema ID of `https://invopop.com/schemas`
  76	// when defined with a struct called `User{}`, will result in a schema with an
  77	// ID set to `https://invopop.com/schemas/user`.
  78	//
  79	// If no `BaseSchemaID` is provided, we'll take the type's complete package path
  80	// and use that as a base instead. Set `Anonymous` to try if you do not want to
  81	// include a schema ID.
  82	BaseSchemaID ID
  83
  84	// Anonymous when true will hide the auto-generated Schema ID and provide what is
  85	// known as an "anonymous schema". As a rule, this is not recommended.
  86	Anonymous bool
  87
  88	// AssignAnchor when true will use the original struct's name as an anchor inside
  89	// every definition, including the root schema. These can be useful for having a
  90	// reference to the original struct's name in CamelCase instead of the snake-case used
  91	// by default for URI compatibility.
  92	//
  93	// Anchors do not appear to be widely used out in the wild, so at this time the
  94	// anchors themselves will not be used inside generated schema.
  95	AssignAnchor bool
  96
  97	// AllowAdditionalProperties will cause the Reflector to generate a schema
  98	// without additionalProperties set to 'false' for all struct types. This means
  99	// the presence of additional keys in JSON objects will not cause validation
 100	// to fail. Note said additional keys will simply be dropped when the
 101	// validated JSON is unmarshaled.
 102	AllowAdditionalProperties bool
 103
 104	// RequiredFromJSONSchemaTags will cause the Reflector to generate a schema
 105	// that requires any key tagged with `jsonschema:required`, overriding the
 106	// default of requiring any key *not* tagged with `json:,omitempty`.
 107	RequiredFromJSONSchemaTags bool
 108
 109	// Do not reference definitions. This will remove the top-level $defs map and
 110	// instead cause the entire structure of types to be output in one tree. The
 111	// list of type definitions (`$defs`) will not be included.
 112	DoNotReference bool
 113
 114	// ExpandedStruct when true will include the reflected type's definition in the
 115	// root as opposed to a definition with a reference.
 116	ExpandedStruct bool
 117
 118	// FieldNameTag will change the tag used to get field names. json tags are used by default.
 119	FieldNameTag string
 120
 121	// IgnoredTypes defines a slice of types that should be ignored in the schema,
 122	// switching to just allowing additional properties instead.
 123	IgnoredTypes []any
 124
 125	// Lookup allows a function to be defined that will provide a custom mapping of
 126	// types to Schema IDs. This allows existing schema documents to be referenced
 127	// by their ID instead of being embedded into the current schema definitions.
 128	// Reflected types will never be pointers, only underlying elements.
 129	Lookup func(reflect.Type) ID
 130
 131	// Mapper is a function that can be used to map custom Go types to jsonschema schemas.
 132	Mapper func(reflect.Type) *Schema
 133
 134	// Namer allows customizing of type names. The default is to use the type's name
 135	// provided by the reflect package.
 136	Namer func(reflect.Type) string
 137
 138	// KeyNamer allows customizing of key names.
 139	// The default is to use the key's name as is, or the json tag if present.
 140	// If a json tag is present, KeyNamer will receive the tag's name as an argument, not the original key name.
 141	KeyNamer func(string) string
 142
 143	// AdditionalFields allows adding structfields for a given type
 144	AdditionalFields func(reflect.Type) []reflect.StructField
 145
 146	// LookupComment allows customizing comment lookup. Given a reflect.Type and optionally
 147	// a field name, it should return the comment string associated with this type or field.
 148	//
 149	// If the field name is empty, it should return the type's comment; otherwise, the field's
 150	// comment should be returned. If no comment is found, an empty string should be returned.
 151	//
 152	// When set, this function is called before the below CommentMap lookup mechanism. However,
 153	// if it returns an empty string, the CommentMap is still consulted.
 154	LookupComment func(reflect.Type, string) string
 155
 156	// CommentMap is a dictionary of fully qualified go types and fields to comment
 157	// strings that will be used if a description has not already been provided in
 158	// the tags. Types and fields are added to the package path using "." as a
 159	// separator.
 160	//
 161	// Type descriptions should be defined like:
 162	//
 163	//   map[string]string{"github.com/invopop/jsonschema.Reflector": "A Reflector reflects values into a Schema."}
 164	//
 165	// And Fields defined as:
 166	//
 167	//   map[string]string{"github.com/invopop/jsonschema.Reflector.DoNotReference": "Do not reference definitions."}
 168	//
 169	// See also: AddGoComments, LookupComment
 170	CommentMap map[string]string
 171}
 172
 173// Reflect reflects to Schema from a value.
 174func (r *Reflector) Reflect(v any) *Schema {
 175	return r.ReflectFromType(reflect.TypeOf(v))
 176}
 177
 178// ReflectFromType generates root schema
 179func (r *Reflector) ReflectFromType(t reflect.Type) *Schema {
 180	if t.Kind() == reflect.Ptr {
 181		t = t.Elem() // re-assign from pointer
 182	}
 183
 184	name := r.typeName(t)
 185
 186	s := new(Schema)
 187	definitions := Definitions{}
 188	s.Definitions = definitions
 189	bs := r.reflectTypeToSchemaWithID(definitions, t)
 190	if r.ExpandedStruct {
 191		*s = *definitions[name]
 192		delete(definitions, name)
 193	} else {
 194		*s = *bs
 195	}
 196
 197	// Attempt to set the schema ID
 198	if !r.Anonymous && s.ID == EmptyID {
 199		baseSchemaID := r.BaseSchemaID
 200		if baseSchemaID == EmptyID {
 201			id := ID("https://" + t.PkgPath())
 202			if err := id.Validate(); err == nil {
 203				// it's okay to silently ignore URL errors
 204				baseSchemaID = id
 205			}
 206		}
 207		if baseSchemaID != EmptyID {
 208			s.ID = baseSchemaID.Add(ToSnakeCase(name))
 209		}
 210	}
 211
 212	s.Version = Version
 213	if !r.DoNotReference {
 214		s.Definitions = definitions
 215	}
 216
 217	return s
 218}
 219
 220// Available Go defined types for JSON Schema Validation.
 221// RFC draft-wright-json-schema-validation-00, section 7.3
 222var (
 223	timeType = reflect.TypeOf(time.Time{}) // date-time RFC section 7.3.1
 224	ipType   = reflect.TypeOf(net.IP{})    // ipv4 and ipv6 RFC section 7.3.4, 7.3.5
 225	uriType  = reflect.TypeOf(url.URL{})   // uri RFC section 7.3.6
 226)
 227
 228// Byte slices will be encoded as base64
 229var byteSliceType = reflect.TypeOf([]byte(nil))
 230
 231// Except for json.RawMessage
 232var rawMessageType = reflect.TypeOf(json.RawMessage{})
 233
 234// Go code generated from protobuf enum types should fulfil this interface.
 235type protoEnum interface {
 236	EnumDescriptor() ([]byte, []int)
 237}
 238
 239var protoEnumType = reflect.TypeOf((*protoEnum)(nil)).Elem()
 240
 241// SetBaseSchemaID is a helper use to be able to set the reflectors base
 242// schema ID from a string as opposed to then ID instance.
 243func (r *Reflector) SetBaseSchemaID(id string) {
 244	r.BaseSchemaID = ID(id)
 245}
 246
 247func (r *Reflector) refOrReflectTypeToSchema(definitions Definitions, t reflect.Type) *Schema {
 248	id := r.lookupID(t)
 249	if id != EmptyID {
 250		return &Schema{
 251			Ref: id.String(),
 252		}
 253	}
 254
 255	// Already added to definitions?
 256	if def := r.refDefinition(definitions, t); def != nil {
 257		return def
 258	}
 259
 260	return r.reflectTypeToSchemaWithID(definitions, t)
 261}
 262
 263func (r *Reflector) reflectTypeToSchemaWithID(defs Definitions, t reflect.Type) *Schema {
 264	s := r.reflectTypeToSchema(defs, t)
 265	if s != nil {
 266		if r.Lookup != nil {
 267			id := r.Lookup(t)
 268			if id != EmptyID {
 269				s.ID = id
 270			}
 271		}
 272	}
 273	return s
 274}
 275
 276func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) *Schema {
 277	// only try to reflect non-pointers
 278	if t.Kind() == reflect.Ptr {
 279		return r.refOrReflectTypeToSchema(definitions, t.Elem())
 280	}
 281
 282	// Check if the there is an alias method that provides an object
 283	// that we should use instead of this one.
 284	if t.Implements(customAliasSchema) {
 285		v := reflect.New(t)
 286		o := v.Interface().(aliasSchemaImpl)
 287		t = reflect.TypeOf(o.JSONSchemaAlias())
 288		return r.refOrReflectTypeToSchema(definitions, t)
 289	}
 290
 291	// Do any pre-definitions exist?
 292	if r.Mapper != nil {
 293		if t := r.Mapper(t); t != nil {
 294			return t
 295		}
 296	}
 297	if rt := r.reflectCustomSchema(definitions, t); rt != nil {
 298		return rt
 299	}
 300
 301	// Prepare a base to which details can be added
 302	st := new(Schema)
 303
 304	// jsonpb will marshal protobuf enum options as either strings or integers.
 305	// It will unmarshal either.
 306	if t.Implements(protoEnumType) {
 307		st.OneOf = []*Schema{
 308			{Type: "string"},
 309			{Type: "integer"},
 310		}
 311		return st
 312	}
 313
 314	// Defined format types for JSON Schema Validation
 315	// RFC draft-wright-json-schema-validation-00, section 7.3
 316	// TODO email RFC section 7.3.2, hostname RFC section 7.3.3, uriref RFC section 7.3.7
 317	if t == ipType {
 318		// TODO differentiate ipv4 and ipv6 RFC section 7.3.4, 7.3.5
 319		st.Type = "string"
 320		st.Format = "ipv4"
 321		return st
 322	}
 323
 324	switch t.Kind() {
 325	case reflect.Struct:
 326		r.reflectStruct(definitions, t, st)
 327
 328	case reflect.Slice, reflect.Array:
 329		r.reflectSliceOrArray(definitions, t, st)
 330
 331	case reflect.Map:
 332		r.reflectMap(definitions, t, st)
 333
 334	case reflect.Interface:
 335		// empty
 336
 337	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
 338		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 339		st.Type = "integer"
 340
 341	case reflect.Float32, reflect.Float64:
 342		st.Type = "number"
 343
 344	case reflect.Bool:
 345		st.Type = "boolean"
 346
 347	case reflect.String:
 348		st.Type = "string"
 349
 350	default:
 351		panic("unsupported type " + t.String())
 352	}
 353
 354	r.reflectSchemaExtend(definitions, t, st)
 355
 356	// Always try to reference the definition which may have just been created
 357	if def := r.refDefinition(definitions, t); def != nil {
 358		return def
 359	}
 360
 361	return st
 362}
 363
 364func (r *Reflector) reflectCustomSchema(definitions Definitions, t reflect.Type) *Schema {
 365	if t.Kind() == reflect.Ptr {
 366		return r.reflectCustomSchema(definitions, t.Elem())
 367	}
 368
 369	if t.Implements(customType) {
 370		v := reflect.New(t)
 371		o := v.Interface().(customSchemaImpl)
 372		st := o.JSONSchema()
 373		r.addDefinition(definitions, t, st)
 374		if ref := r.refDefinition(definitions, t); ref != nil {
 375			return ref
 376		}
 377		return st
 378	}
 379
 380	return nil
 381}
 382
 383func (r *Reflector) reflectSchemaExtend(definitions Definitions, t reflect.Type, s *Schema) *Schema {
 384	if t.Implements(extendType) {
 385		v := reflect.New(t)
 386		o := v.Interface().(extendSchemaImpl)
 387		o.JSONSchemaExtend(s)
 388		if ref := r.refDefinition(definitions, t); ref != nil {
 389			return ref
 390		}
 391	}
 392
 393	return s
 394}
 395
 396func (r *Reflector) reflectSliceOrArray(definitions Definitions, t reflect.Type, st *Schema) {
 397	if t == rawMessageType {
 398		return
 399	}
 400
 401	r.addDefinition(definitions, t, st)
 402
 403	if st.Description == "" {
 404		st.Description = r.lookupComment(t, "")
 405	}
 406
 407	if t.Kind() == reflect.Array {
 408		l := uint64(t.Len())
 409		st.MinItems = &l
 410		st.MaxItems = &l
 411	}
 412	if t.Kind() == reflect.Slice && t.Elem() == byteSliceType.Elem() {
 413		st.Type = "string"
 414		// NOTE: ContentMediaType is not set here
 415		st.ContentEncoding = "base64"
 416	} else {
 417		st.Type = "array"
 418		st.Items = r.refOrReflectTypeToSchema(definitions, t.Elem())
 419	}
 420}
 421
 422func (r *Reflector) reflectMap(definitions Definitions, t reflect.Type, st *Schema) {
 423	r.addDefinition(definitions, t, st)
 424
 425	st.Type = "object"
 426	if st.Description == "" {
 427		st.Description = r.lookupComment(t, "")
 428	}
 429
 430	switch t.Key().Kind() {
 431	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 432		st.PatternProperties = map[string]*Schema{
 433			"^[0-9]+$": r.refOrReflectTypeToSchema(definitions, t.Elem()),
 434		}
 435		st.AdditionalProperties = FalseSchema
 436		return
 437	}
 438	if t.Elem().Kind() != reflect.Interface {
 439		st.AdditionalProperties = r.refOrReflectTypeToSchema(definitions, t.Elem())
 440	}
 441}
 442
 443// Reflects a struct to a JSON Schema type.
 444func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Schema) {
 445	// Handle special types
 446	switch t {
 447	case timeType: // date-time RFC section 7.3.1
 448		s.Type = "string"
 449		s.Format = "date-time"
 450		return
 451	case uriType: // uri RFC section 7.3.6
 452		s.Type = "string"
 453		s.Format = "uri"
 454		return
 455	}
 456
 457	r.addDefinition(definitions, t, s)
 458	s.Type = "object"
 459	s.Properties = NewProperties()
 460	s.Description = r.lookupComment(t, "")
 461	if r.AssignAnchor {
 462		s.Anchor = t.Name()
 463	}
 464	if !r.AllowAdditionalProperties && s.AdditionalProperties == nil {
 465		s.AdditionalProperties = FalseSchema
 466	}
 467
 468	ignored := false
 469	for _, it := range r.IgnoredTypes {
 470		if reflect.TypeOf(it) == t {
 471			ignored = true
 472			break
 473		}
 474	}
 475	if !ignored {
 476		r.reflectStructFields(s, definitions, t)
 477	}
 478}
 479
 480func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t reflect.Type) {
 481	if t.Kind() == reflect.Ptr {
 482		t = t.Elem()
 483	}
 484	if t.Kind() != reflect.Struct {
 485		return
 486	}
 487
 488	var getFieldDocString customGetFieldDocString
 489	if t.Implements(customStructGetFieldDocString) {
 490		v := reflect.New(t)
 491		o := v.Interface().(customSchemaGetFieldDocString)
 492		getFieldDocString = o.GetFieldDocString
 493	}
 494
 495	customPropertyMethod := func(string) any {
 496		return nil
 497	}
 498	if t.Implements(customPropertyAliasSchema) {
 499		v := reflect.New(t)
 500		o := v.Interface().(propertyAliasSchemaImpl)
 501		customPropertyMethod = o.JSONSchemaProperty
 502	}
 503
 504	handleField := func(f reflect.StructField) {
 505		name, shouldEmbed, required, nullable := r.reflectFieldName(f)
 506		// if anonymous and exported type should be processed recursively
 507		// current type should inherit properties of anonymous one
 508		if name == "" {
 509			if shouldEmbed {
 510				r.reflectStructFields(st, definitions, f.Type)
 511			}
 512			return
 513		}
 514
 515		// If a JSONSchemaAlias(prop string) method is defined, attempt to use
 516		// the provided object's type instead of the field's type.
 517		var property *Schema
 518		if alias := customPropertyMethod(name); alias != nil {
 519			property = r.refOrReflectTypeToSchema(definitions, reflect.TypeOf(alias))
 520		} else {
 521			property = r.refOrReflectTypeToSchema(definitions, f.Type)
 522		}
 523
 524		property.structKeywordsFromTags(f, st, name)
 525		if property.Description == "" {
 526			property.Description = r.lookupComment(t, f.Name)
 527		}
 528		if getFieldDocString != nil {
 529			property.Description = getFieldDocString(f.Name)
 530		}
 531
 532		if nullable {
 533			property = &Schema{
 534				OneOf: []*Schema{
 535					property,
 536					{
 537						Type: "null",
 538					},
 539				},
 540			}
 541		}
 542
 543		st.Properties.Set(name, property)
 544		if required {
 545			st.Required = appendUniqueString(st.Required, name)
 546		}
 547	}
 548
 549	for i := 0; i < t.NumField(); i++ {
 550		f := t.Field(i)
 551		handleField(f)
 552	}
 553	if r.AdditionalFields != nil {
 554		if af := r.AdditionalFields(t); af != nil {
 555			for _, sf := range af {
 556				handleField(sf)
 557			}
 558		}
 559	}
 560}
 561
 562func appendUniqueString(base []string, value string) []string {
 563	for _, v := range base {
 564		if v == value {
 565			return base
 566		}
 567	}
 568	return append(base, value)
 569}
 570
 571// addDefinition will append the provided schema. If needed, an ID and anchor will also be added.
 572func (r *Reflector) addDefinition(definitions Definitions, t reflect.Type, s *Schema) {
 573	name := r.typeName(t)
 574	if name == "" {
 575		return
 576	}
 577	definitions[name] = s
 578}
 579
 580// refDefinition will provide a schema with a reference to an existing definition.
 581func (r *Reflector) refDefinition(definitions Definitions, t reflect.Type) *Schema {
 582	if r.DoNotReference {
 583		return nil
 584	}
 585	name := r.typeName(t)
 586	if name == "" {
 587		return nil
 588	}
 589	if _, ok := definitions[name]; !ok {
 590		return nil
 591	}
 592	return &Schema{
 593		Ref: "#/$defs/" + name,
 594	}
 595}
 596
 597func (r *Reflector) lookupID(t reflect.Type) ID {
 598	if r.Lookup != nil {
 599		if t.Kind() == reflect.Ptr {
 600			t = t.Elem()
 601		}
 602		return r.Lookup(t)
 603
 604	}
 605	return EmptyID
 606}
 607
 608func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, propertyName string) {
 609	t.Description = f.Tag.Get("jsonschema_description")
 610
 611	tags := splitOnUnescapedCommas(f.Tag.Get("jsonschema"))
 612	tags = t.genericKeywords(tags, parent, propertyName)
 613
 614	switch t.Type {
 615	case "string":
 616		t.stringKeywords(tags)
 617	case "number":
 618		t.numericalKeywords(tags)
 619	case "integer":
 620		t.numericalKeywords(tags)
 621	case "array":
 622		t.arrayKeywords(tags)
 623	case "boolean":
 624		t.booleanKeywords(tags)
 625	}
 626	extras := strings.Split(f.Tag.Get("jsonschema_extras"), ",")
 627	t.extraKeywords(extras)
 628}
 629
 630// read struct tags for generic keywords
 631func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName string) []string { //nolint:gocyclo
 632	unprocessed := make([]string, 0, len(tags))
 633	for _, tag := range tags {
 634		nameValue := strings.SplitN(tag, "=", 2)
 635		if len(nameValue) == 2 {
 636			name, val := nameValue[0], nameValue[1]
 637			switch name {
 638			case "title":
 639				t.Title = val
 640			case "description":
 641				t.Description = val
 642			case "type":
 643				t.Type = val
 644			case "anchor":
 645				t.Anchor = val
 646			case "oneof_required":
 647				var typeFound *Schema
 648				for i := range parent.OneOf {
 649					if parent.OneOf[i].Title == nameValue[1] {
 650						typeFound = parent.OneOf[i]
 651					}
 652				}
 653				if typeFound == nil {
 654					typeFound = &Schema{
 655						Title:    nameValue[1],
 656						Required: []string{},
 657					}
 658					parent.OneOf = append(parent.OneOf, typeFound)
 659				}
 660				typeFound.Required = append(typeFound.Required, propertyName)
 661			case "anyof_required":
 662				var typeFound *Schema
 663				for i := range parent.AnyOf {
 664					if parent.AnyOf[i].Title == nameValue[1] {
 665						typeFound = parent.AnyOf[i]
 666					}
 667				}
 668				if typeFound == nil {
 669					typeFound = &Schema{
 670						Title:    nameValue[1],
 671						Required: []string{},
 672					}
 673					parent.AnyOf = append(parent.AnyOf, typeFound)
 674				}
 675				typeFound.Required = append(typeFound.Required, propertyName)
 676			case "oneof_ref":
 677				subSchema := t
 678				if t.Items != nil {
 679					subSchema = t.Items
 680				}
 681				if subSchema.OneOf == nil {
 682					subSchema.OneOf = make([]*Schema, 0, 1)
 683				}
 684				subSchema.Ref = ""
 685				refs := strings.Split(nameValue[1], ";")
 686				for _, r := range refs {
 687					subSchema.OneOf = append(subSchema.OneOf, &Schema{
 688						Ref: r,
 689					})
 690				}
 691			case "oneof_type":
 692				if t.OneOf == nil {
 693					t.OneOf = make([]*Schema, 0, 1)
 694				}
 695				t.Type = ""
 696				types := strings.Split(nameValue[1], ";")
 697				for _, ty := range types {
 698					t.OneOf = append(t.OneOf, &Schema{
 699						Type: ty,
 700					})
 701				}
 702			case "anyof_ref":
 703				subSchema := t
 704				if t.Items != nil {
 705					subSchema = t.Items
 706				}
 707				if subSchema.AnyOf == nil {
 708					subSchema.AnyOf = make([]*Schema, 0, 1)
 709				}
 710				subSchema.Ref = ""
 711				refs := strings.Split(nameValue[1], ";")
 712				for _, r := range refs {
 713					subSchema.AnyOf = append(subSchema.AnyOf, &Schema{
 714						Ref: r,
 715					})
 716				}
 717			case "anyof_type":
 718				if t.AnyOf == nil {
 719					t.AnyOf = make([]*Schema, 0, 1)
 720				}
 721				t.Type = ""
 722				types := strings.Split(nameValue[1], ";")
 723				for _, ty := range types {
 724					t.AnyOf = append(t.AnyOf, &Schema{
 725						Type: ty,
 726					})
 727				}
 728			default:
 729				unprocessed = append(unprocessed, tag)
 730			}
 731		}
 732	}
 733	return unprocessed
 734}
 735
 736// read struct tags for boolean type keywords
 737func (t *Schema) booleanKeywords(tags []string) {
 738	for _, tag := range tags {
 739		nameValue := strings.Split(tag, "=")
 740		if len(nameValue) != 2 {
 741			continue
 742		}
 743		name, val := nameValue[0], nameValue[1]
 744		if name == "default" {
 745			if val == "true" {
 746				t.Default = true
 747			} else if val == "false" {
 748				t.Default = false
 749			}
 750		}
 751	}
 752}
 753
 754// read struct tags for string type keywords
 755func (t *Schema) stringKeywords(tags []string) {
 756	for _, tag := range tags {
 757		nameValue := strings.SplitN(tag, "=", 2)
 758		if len(nameValue) == 2 {
 759			name, val := nameValue[0], nameValue[1]
 760			switch name {
 761			case "minLength":
 762				t.MinLength = parseUint(val)
 763			case "maxLength":
 764				t.MaxLength = parseUint(val)
 765			case "pattern":
 766				t.Pattern = val
 767			case "format":
 768				t.Format = val
 769			case "readOnly":
 770				i, _ := strconv.ParseBool(val)
 771				t.ReadOnly = i
 772			case "writeOnly":
 773				i, _ := strconv.ParseBool(val)
 774				t.WriteOnly = i
 775			case "default":
 776				t.Default = val
 777			case "example":
 778				t.Examples = append(t.Examples, val)
 779			case "enum":
 780				t.Enum = append(t.Enum, val)
 781			}
 782		}
 783	}
 784}
 785
 786// read struct tags for numerical type keywords
 787func (t *Schema) numericalKeywords(tags []string) {
 788	for _, tag := range tags {
 789		nameValue := strings.Split(tag, "=")
 790		if len(nameValue) == 2 {
 791			name, val := nameValue[0], nameValue[1]
 792			switch name {
 793			case "multipleOf":
 794				t.MultipleOf, _ = toJSONNumber(val)
 795			case "minimum":
 796				t.Minimum, _ = toJSONNumber(val)
 797			case "maximum":
 798				t.Maximum, _ = toJSONNumber(val)
 799			case "exclusiveMaximum":
 800				t.ExclusiveMaximum, _ = toJSONNumber(val)
 801			case "exclusiveMinimum":
 802				t.ExclusiveMinimum, _ = toJSONNumber(val)
 803			case "default":
 804				if num, ok := toJSONNumber(val); ok {
 805					t.Default = num
 806				}
 807			case "example":
 808				if num, ok := toJSONNumber(val); ok {
 809					t.Examples = append(t.Examples, num)
 810				}
 811			case "enum":
 812				if num, ok := toJSONNumber(val); ok {
 813					t.Enum = append(t.Enum, num)
 814				}
 815			}
 816		}
 817	}
 818}
 819
 820// read struct tags for object type keywords
 821// func (t *Type) objectKeywords(tags []string) {
 822//     for _, tag := range tags{
 823//         nameValue := strings.Split(tag, "=")
 824//         name, val := nameValue[0], nameValue[1]
 825//         switch name{
 826//             case "dependencies":
 827//                 t.Dependencies = val
 828//                 break;
 829//             case "patternProperties":
 830//                 t.PatternProperties = val
 831//                 break;
 832//         }
 833//     }
 834// }
 835
 836// read struct tags for array type keywords
 837func (t *Schema) arrayKeywords(tags []string) {
 838	var defaultValues []any
 839
 840	unprocessed := make([]string, 0, len(tags))
 841	for _, tag := range tags {
 842		nameValue := strings.Split(tag, "=")
 843		if len(nameValue) == 2 {
 844			name, val := nameValue[0], nameValue[1]
 845			switch name {
 846			case "minItems":
 847				t.MinItems = parseUint(val)
 848			case "maxItems":
 849				t.MaxItems = parseUint(val)
 850			case "uniqueItems":
 851				t.UniqueItems = true
 852			case "default":
 853				defaultValues = append(defaultValues, val)
 854			case "format":
 855				t.Items.Format = val
 856			case "pattern":
 857				t.Items.Pattern = val
 858			default:
 859				unprocessed = append(unprocessed, tag) // left for further processing by underlying type
 860			}
 861		}
 862	}
 863	if len(defaultValues) > 0 {
 864		t.Default = defaultValues
 865	}
 866
 867	if len(unprocessed) == 0 {
 868		// we don't have anything else to process
 869		return
 870	}
 871
 872	switch t.Items.Type {
 873	case "string":
 874		t.Items.stringKeywords(unprocessed)
 875	case "number":
 876		t.Items.numericalKeywords(unprocessed)
 877	case "integer":
 878		t.Items.numericalKeywords(unprocessed)
 879	case "array":
 880		// explicitly don't support traversal for the [][]..., as it's unclear where the array tags belong
 881	case "boolean":
 882		t.Items.booleanKeywords(unprocessed)
 883	}
 884}
 885
 886func (t *Schema) extraKeywords(tags []string) {
 887	for _, tag := range tags {
 888		nameValue := strings.SplitN(tag, "=", 2)
 889		if len(nameValue) == 2 {
 890			t.setExtra(nameValue[0], nameValue[1])
 891		}
 892	}
 893}
 894
 895func (t *Schema) setExtra(key, val string) {
 896	if t.Extras == nil {
 897		t.Extras = map[string]any{}
 898	}
 899	if existingVal, ok := t.Extras[key]; ok {
 900		switch existingVal := existingVal.(type) {
 901		case string:
 902			t.Extras[key] = []string{existingVal, val}
 903		case []string:
 904			t.Extras[key] = append(existingVal, val)
 905		case int:
 906			t.Extras[key], _ = strconv.Atoi(val)
 907		case bool:
 908			t.Extras[key] = (val == "true" || val == "t")
 909		}
 910	} else {
 911		switch key {
 912		case "minimum":
 913			t.Extras[key], _ = strconv.Atoi(val)
 914		default:
 915			var x any
 916			if val == "true" {
 917				x = true
 918			} else if val == "false" {
 919				x = false
 920			} else {
 921				x = val
 922			}
 923			t.Extras[key] = x
 924		}
 925	}
 926}
 927
 928func requiredFromJSONTags(tags []string, val *bool) {
 929	if ignoredByJSONTags(tags) {
 930		return
 931	}
 932
 933	for _, tag := range tags[1:] {
 934		if tag == "omitempty" {
 935			*val = false
 936			return
 937		}
 938	}
 939	*val = true
 940}
 941
 942func requiredFromJSONSchemaTags(tags []string, val *bool) {
 943	if ignoredByJSONSchemaTags(tags) {
 944		return
 945	}
 946	for _, tag := range tags {
 947		if tag == "required" {
 948			*val = true
 949		}
 950	}
 951}
 952
 953func nullableFromJSONSchemaTags(tags []string) bool {
 954	if ignoredByJSONSchemaTags(tags) {
 955		return false
 956	}
 957	for _, tag := range tags {
 958		if tag == "nullable" {
 959			return true
 960		}
 961	}
 962	return false
 963}
 964
 965func ignoredByJSONTags(tags []string) bool {
 966	return tags[0] == "-"
 967}
 968
 969func ignoredByJSONSchemaTags(tags []string) bool {
 970	return tags[0] == "-"
 971}
 972
 973func inlinedByJSONTags(tags []string) bool {
 974	for _, tag := range tags[1:] {
 975		if tag == "inline" {
 976			return true
 977		}
 978	}
 979	return false
 980}
 981
 982// toJSONNumber converts string to *json.Number.
 983// It'll aso return whether the number is valid.
 984func toJSONNumber(s string) (json.Number, bool) {
 985	num := json.Number(s)
 986	if _, err := num.Int64(); err == nil {
 987		return num, true
 988	}
 989	if _, err := num.Float64(); err == nil {
 990		return num, true
 991	}
 992	return json.Number(""), false
 993}
 994
 995func parseUint(num string) *uint64 {
 996	val, err := strconv.ParseUint(num, 10, 64)
 997	if err != nil {
 998		return nil
 999	}
1000	return &val
1001}
1002
1003func (r *Reflector) fieldNameTag() string {
1004	if r.FieldNameTag != "" {
1005		return r.FieldNameTag
1006	}
1007	return "json"
1008}
1009
1010func (r *Reflector) reflectFieldName(f reflect.StructField) (string, bool, bool, bool) {
1011	jsonTagString := f.Tag.Get(r.fieldNameTag())
1012	jsonTags := strings.Split(jsonTagString, ",")
1013
1014	if ignoredByJSONTags(jsonTags) {
1015		return "", false, false, false
1016	}
1017
1018	schemaTags := strings.Split(f.Tag.Get("jsonschema"), ",")
1019	if ignoredByJSONSchemaTags(schemaTags) {
1020		return "", false, false, false
1021	}
1022
1023	var required bool
1024	if !r.RequiredFromJSONSchemaTags {
1025		requiredFromJSONTags(jsonTags, &required)
1026	}
1027	requiredFromJSONSchemaTags(schemaTags, &required)
1028
1029	nullable := nullableFromJSONSchemaTags(schemaTags)
1030
1031	if f.Anonymous && jsonTags[0] == "" {
1032		// As per JSON Marshal rules, anonymous structs are inherited
1033		if f.Type.Kind() == reflect.Struct {
1034			return "", true, false, false
1035		}
1036
1037		// As per JSON Marshal rules, anonymous pointer to structs are inherited
1038		if f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct {
1039			return "", true, false, false
1040		}
1041	}
1042
1043	// As per JSON Marshal rules, inline nested structs that have `inline` tag.
1044	if inlinedByJSONTags(jsonTags) {
1045		return "", true, false, false
1046	}
1047
1048	// Try to determine the name from the different combos
1049	name := f.Name
1050	if jsonTags[0] != "" {
1051		name = jsonTags[0]
1052	}
1053	if !f.Anonymous && f.PkgPath != "" {
1054		// field not anonymous and not export has no export name
1055		name = ""
1056	} else if r.KeyNamer != nil {
1057		name = r.KeyNamer(name)
1058	}
1059
1060	return name, false, required, nullable
1061}
1062
1063// UnmarshalJSON is used to parse a schema object or boolean.
1064func (t *Schema) UnmarshalJSON(data []byte) error {
1065	if bytes.Equal(data, []byte("true")) {
1066		*t = *TrueSchema
1067		return nil
1068	} else if bytes.Equal(data, []byte("false")) {
1069		*t = *FalseSchema
1070		return nil
1071	}
1072	type SchemaAlt Schema
1073	aux := &struct {
1074		*SchemaAlt
1075	}{
1076		SchemaAlt: (*SchemaAlt)(t),
1077	}
1078	return json.Unmarshal(data, aux)
1079}
1080
1081// MarshalJSON is used to serialize a schema object or boolean.
1082func (t *Schema) MarshalJSON() ([]byte, error) {
1083	if t.boolean != nil {
1084		if *t.boolean {
1085			return []byte("true"), nil
1086		}
1087		return []byte("false"), nil
1088	}
1089	if reflect.DeepEqual(&Schema{}, t) {
1090		// Don't bother returning empty schemas
1091		return []byte("true"), nil
1092	}
1093	type SchemaAlt Schema
1094	b, err := json.Marshal((*SchemaAlt)(t))
1095	if err != nil {
1096		return nil, err
1097	}
1098	if len(t.Extras) == 0 {
1099		return b, nil
1100	}
1101	m, err := json.Marshal(t.Extras)
1102	if err != nil {
1103		return nil, err
1104	}
1105	if len(b) == 2 {
1106		return m, nil
1107	}
1108	b[len(b)-1] = ','
1109	return append(b, m[1:]...), nil
1110}
1111
1112func (r *Reflector) typeName(t reflect.Type) string {
1113	if r.Namer != nil {
1114		if name := r.Namer(t); name != "" {
1115			return name
1116		}
1117	}
1118	return t.Name()
1119}
1120
1121// Split on commas that are not preceded by `\`.
1122// This way, we prevent splitting regexes
1123func splitOnUnescapedCommas(tagString string) []string {
1124	ret := make([]string, 0)
1125	separated := strings.Split(tagString, ",")
1126	ret = append(ret, separated[0])
1127	i := 0
1128	for _, nextTag := range separated[1:] {
1129		if len(ret[i]) == 0 {
1130			ret = append(ret, nextTag)
1131			i++
1132			continue
1133		}
1134
1135		if ret[i][len(ret[i])-1] == '\\' {
1136			ret[i] = ret[i][:len(ret[i])-1] + "," + nextTag
1137		} else {
1138			ret = append(ret, nextTag)
1139			i++
1140		}
1141	}
1142
1143	return ret
1144}
1145
1146func fullyQualifiedTypeName(t reflect.Type) string {
1147	return t.PkgPath() + "." + t.Name()
1148}