// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.// Use of this source code is governed by an MIT-style// license that can be found in the LICENSE file.// This file contains functions that infer a schema from a Go type.package jsonschemaimport ()const debugEnv = "JSONSCHEMAGODEBUG"// ForOptions are options for the [For] and [ForType] functions.typeForOptionsstruct {// If IgnoreInvalidTypes is true, fields that can't be represented as a JSON // Schema are ignored instead of causing an error. // This allows callers to adjust the resulting schema using custom knowledge. // For example, an interface type where all the possible implementations are // known can be described with "oneof". IgnoreInvalidTypes bool// TypeSchemas maps types to their schemas. // If [For] encounters a type that is a key in this map, the // corresponding value is used as the resulting schema (after cloning to // ensure uniqueness). // Types in this map override the default translations, as described // in [For]'s documentation. // PropertyOrder defined in these schemas will not be used in [For] or [ForType]. TypeSchemas map[reflect.Type]*Schema}// For constructs a JSON schema object for the given type argument.// If non-nil, the provided options configure certain aspects of this contruction,// described below.// It translates Go types into compatible JSON schema types, as follows.// These defaults can be overridden by [ForOptions.TypeSchemas].//// - Strings have schema type "string".// - Bools have schema type "boolean".// - Signed and unsigned integer types have schema type "integer".// - Floating point types have schema type "number".// - Slices and arrays have schema type "array", and a corresponding schema// for items.// - Maps with string key have schema type "object", and corresponding// schema for additionalProperties.// - Structs have schema type "object", and disallow additionalProperties.// Their properties are derived from exported struct fields, using the// struct field JSON name. Fields that are marked "omitempty" or "omitzero" are// considered optional; all other fields become required properties.// For structs, the PropertyOrder will be set to the field order.// - Some types in the standard library that implement json.Marshaler// translate to schemas that match the values to which they marshal.// For example, [time.Time] translates to the schema for strings.//// For will return an error if there is a cycle in the types.//// By default, For returns an error if t contains (possibly recursively) any of the// following Go types, as they are incompatible with the JSON schema spec.// If [ForOptions.IgnoreInvalidTypes] is true, then these types are ignored instead.// - maps with key other than 'string'// - function types// - channel types// - complex numbers// - unsafe pointers//// This function recognizes struct field tags named "jsonschema".// A jsonschema tag on a field is used as the description for the corresponding property.// For future compatibility, descriptions must not start with "WORD=", where WORD is a// sequence of non-whitespace characters.func [ any]( *ForOptions) (*Schema, error) {if == nil { = &ForOptions{} } := maps.Clone(initialSchemaMap)// Add types from the options. They override the default ones.maps.Copy(, .TypeSchemas) , := forType(reflect.TypeFor[](), map[reflect.Type]bool{}, .IgnoreInvalidTypes, )if != nil {varreturnnil, fmt.Errorf("For[%T](): %w", , ) }return , nil}// ForType is like [For], but takes a [reflect.Type]func ( reflect.Type, *ForOptions) (*Schema, error) {if == nil { = &ForOptions{} } := maps.Clone(initialSchemaMap)// Add types from the options. They override the default ones.maps.Copy(, .TypeSchemas) , := forType(, map[reflect.Type]bool{}, .IgnoreInvalidTypes, )if != nil {returnnil, fmt.Errorf("ForType(%s): %w", , ) }return , nil}// Helper to create a *float64 pointer from a valuefunc f64Ptr( float64) *float64 {return &}func forType( reflect.Type, map[reflect.Type]bool, bool, map[reflect.Type]*Schema) (*Schema, error) {// Follow pointers: the schema for *T is almost the same as for T, except that // an explicit JSON "null" is allowed for the pointer. := falsefor .Kind() == reflect.Pointer { = true = .Elem() }// Check for cycles // User defined types have a name, so we can skip those that are natively definedif .Name() != "" {if [] {returnnil, fmt.Errorf("cycle detected for type %v", ) } [] = truedeferdelete(, ) }if := []; != nil { := .CloneSchemas()ifos.Getenv(debugEnv) != "typeschemasnull=1" && {if .Type != "" { .Types = []string{"null", .Type} .Type = "" } elseif !slices.Contains(.Types, "null") { .Types = append([]string{"null"}, .Types...) } }return , nil }var ( = new(Schema)error )switch .Kind() {casereflect.Bool: .Type = "boolean"casereflect.Int, reflect.Int64: .Type = "integer"casereflect.Uint, reflect.Uint64, reflect.Uintptr: .Type = "integer" .Minimum = f64Ptr(0)casereflect.Int8: .Type = "integer" .Minimum = f64Ptr(math.MinInt8) .Maximum = f64Ptr(math.MaxInt8)casereflect.Uint8: .Type = "integer" .Minimum = f64Ptr(0) .Maximum = f64Ptr(math.MaxUint8)casereflect.Int16: .Type = "integer" .Minimum = f64Ptr(math.MinInt16) .Maximum = f64Ptr(math.MaxInt16)casereflect.Uint16: .Type = "integer" .Minimum = f64Ptr(0) .Maximum = f64Ptr(math.MaxUint16)casereflect.Int32: .Type = "integer" .Minimum = f64Ptr(math.MinInt32) .Maximum = f64Ptr(math.MaxInt32)casereflect.Uint32: .Type = "integer" .Minimum = f64Ptr(0) .Maximum = f64Ptr(math.MaxUint32)casereflect.Float32, reflect.Float64: .Type = "number"casereflect.Interface:// Unrestrictedcasereflect.Map:if .Key().Kind() != reflect.String {if {returnnil, nil// ignore }returnnil, fmt.Errorf("unsupported map key type %v", .Key().Kind()) }if .Key().Kind() != reflect.String { } .Type = "object" .AdditionalProperties, = (.Elem(), , , )if != nil {returnnil, fmt.Errorf("computing map value schema: %v", ) }if && .AdditionalProperties == nil {// Ignore if the element type is invalid.returnnil, nil }casereflect.Slice, reflect.Array:ifos.Getenv(debugEnv) != "typeschemasnull=1" && .Kind() == reflect.Slice { .Types = []string{"null", "array"} } else { .Type = "array" } , := (.Elem(), , , )if != nil {returnnil, fmt.Errorf("computing element schema: %v", ) }if == nil {returnnil, nil } .Items = if && .Items == nil {// Ignore if the element type is invalid.returnnil, nil }if .Kind() == reflect.Array { .MinItems = Ptr(.Len()) .MaxItems = Ptr(.Len()) }casereflect.String: .Type = "string"casereflect.Struct: .Type = "object"// no additional properties are allowed .AdditionalProperties = falseSchema()// If skipPath is non-nil, it is path to an anonymous field whose // schema has been replaced by a known schema.var []intfor , := rangereflect.VisibleFields() {if .Properties == nil { .Properties = make(map[string]*Schema) }if .Anonymous { := [.Type]if != nil {// Type must be object, and only properties can be set.if .Type != "object" {returnnil, fmt.Errorf(`custom schema for embedded struct must have type "object", got %q`, .Type) }// Check that all keywords relevant for objects are absent, except properties. := reflect.ValueOf().Elem()for , := rangeschemaFieldInfos {if .sf.Name == "Type" || .sf.Name == "Properties" {continue } := .FieldByIndex(.sf.Index)if !.IsZero() {returnnil, fmt.Errorf(`overrides for embedded fields can have only "Type" and "Properties"; this has %q`, .sf.Name) } } = .Index := make([]string, 0, len(.Properties))for := range .Properties { = append(, ) }slices.Sort()for , := range {if , := .Properties[]; ! { .Properties[] = .Properties[].CloneSchemas() .PropertyOrder = append(.PropertyOrder, ) } } }continue }// Check to see if this field has been promoted from a replaced anonymous // type.if != nil { := falseiflen(.Index) >= len() { = truefor , := range {if .Index[] != {// If we're no longer in a subfield. = falsebreak } } }if {continue } else {// Anonymous fields are followed immediately by their promoted fields. // Once we encounter a field that *isn't* promoted, we can stop // checking. = nil } } := fieldJSONInfo()if .omit {continue } , := (.Type, , , )if != nil {returnnil, }if && == nil {// Skip fields of invalid type.continue }if , := .Tag.Lookup("jsonschema"); {if == "" {returnnil, fmt.Errorf("empty jsonschema tag on struct field %s.%s", , .Name) }ifdisallowedPrefixRegexp.MatchString() {returnnil, fmt.Errorf("tag must not begin with 'WORD=': %q", ) } .Description = } .Properties[.name] = .PropertyOrder = append(.PropertyOrder, .name)if !.settings["omitempty"] && !.settings["omitzero"] { .Required = append(.Required, .name) } }// Remove PropertyOrder duplicates, keeping the last occurrenceiflen(.PropertyOrder) > 1 { := make(map[string]bool)// Create a slice to hold the cleaned order (capacity = current length) := make([]string, 0, len(.PropertyOrder))// Iterate backwardsfor := len(.PropertyOrder) - 1; >= 0; -- { := .PropertyOrder[]if ![] { = append(, ) [] = true } }// Since we collected them backwards, we need to reverse the result // to restore the correct order.slices.Reverse() .PropertyOrder = }default:if {// Ignore.returnnil, nil }returnnil, fmt.Errorf("type %v is unsupported by jsonschema", ) }if && .Type != "" { .Types = []string{"null", .Type} .Type = "" }return , nil}// initialSchemaMap holds types from the standard library that have MarshalJSON methods.var initialSchemaMap = make(map[reflect.Type]*Schema)func init() { := &Schema{Type: "string"}initialSchemaMap[reflect.TypeFor[time.Time]()] = initialSchemaMap[reflect.TypeFor[slog.Level]()] = ifos.Getenv(debugEnv) == "typeschemasnull=1" {initialSchemaMap[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}} } else {initialSchemaMap[reflect.TypeFor[big.Int]()] = }initialSchemaMap[reflect.TypeFor[big.Rat]()] = initialSchemaMap[reflect.TypeFor[big.Float]()] = }// Disallow jsonschema tag values beginning "WORD=", for future expansion.var disallowedPrefixRegexp = regexp.MustCompile("^[^ \t\n]*=")
The pages are generated with Goldsv0.8.4. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.