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}