// Copyright 2021 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.//go:build goexperiment.jsonv2package jsonimport ()type isZeroer interface { IsZero() bool}var isZeroerType = reflect.TypeFor[isZeroer]()type structFields struct { flattened []structField// listed in depth-first ordering byActualName map[string]*structField byFoldedName map[string][]*structField inlinedFallback *structField}// reindex recomputes index to avoid bounds check during runtime.//// During the construction of each [structField] in [makeStructFields],// the index field is 0-indexed. However, before it returns,// the 0th field is stored in index0 and index stores the remainder.func ( *structFields) () { := func( *structField) { .index0 = .index[0] .index = .index[1:]iflen(.index) == 0 { .index = nil// avoid pinning the backing slice } }for := range .flattened { (&.flattened[]) }if .inlinedFallback != nil { (.inlinedFallback) }}// lookupByFoldedName looks up name by a case-insensitive match// that also ignores the presence of dashes and underscores.func ( *structFields) ( []byte) []*structField {return .byFoldedName[string(foldName())]}type structField struct { id int// unique numeric ID in breadth-first ordering index0 int// 0th index into a struct according to [reflect.Type.FieldByIndex] index []int// 1st index and remainder according to [reflect.Type.FieldByIndex] typ reflect.Type fncs *arshaler isZero func(addressableValue) bool isEmpty func(addressableValue) boolfieldOptions}var errNoExportedFields = errors.New("Go struct has no exported fields")func makeStructFields( reflect.Type) ( structFields, *SemanticError) { := func( *SemanticError, reflect.Type, string, ...any) *SemanticError {returncmp.Or(, &SemanticError{GoType: , Err: fmt.Errorf(, ...)}) }// Setup a queue for a breath-first search.varinttypestruct {reflect.Type []intbool// whether to recursively visit inlined field in this struct } := []{{, nil, true}} := map[reflect.Type]bool{: true}// Perform a breadth-first search over all reachable fields. // This ensures that len(f.index) will be monotonically increasing.var , []structFieldfor < len() { := [] ++ := . := -1// index of last inlined fallback field in current struct := make(map[string]int) // index of each field with a given JSON object name in current structvarbool// whether any Go struct field has a `json` tagvarbool// whether any JSON serializable fields exist in current structfor := range .NumField() { := .Field() , := .Tag.Lookup("json") = || , , := parseFieldOptions()if != nil { = cmp.Or(, &SemanticError{GoType: , Err: }) }if {continue } = true := structField{// Allocate a new slice (len=N+1) to hold both // the parent index (len=N) and the current index (len=1). // Do this to avoid clobbering the memory of the parent index.index: append(append(make([]int, 0, len(.)+1), ....), ),typ: .Type,fieldOptions: , }if .Anonymous && !.hasName {ifindirectType(.typ).Kind() != reflect.Struct { = (, , "embedded Go struct field %s of non-struct type must be explicitly given a JSON name", .Name) } else { .inline = true// implied by use of Go embedding without an explicit name } }if .inline || .unknown {// Handle an inlined field that serializes to/from // zero or more JSON object members.switch .fieldOptions {casefieldOptions{name: .name, quotedName: .quotedName, inline: true}:casefieldOptions{name: .name, quotedName: .quotedName, unknown: true}:casefieldOptions{name: .name, quotedName: .quotedName, inline: true, unknown: true}: = (, , "Go struct field %s cannot have both `inline` and `unknown` specified", .Name) .inline = false// let `unknown` take precedencedefault: = (, , "Go struct field %s cannot have any options other than `inline` or `unknown` specified", .Name)if .hasName {continue// invalid inlined field; treat as ignored } .fieldOptions = fieldOptions{name: .name, quotedName: .quotedName, inline: .inline, unknown: .unknown}if .inline && .unknown { .inline = false// let `unknown` take precedence } }// Reject any types with custom serialization otherwise // it becomes impossible to know what sub-fields to inline. := indirectType(.typ)ifimplementsAny(, allMethodTypes...) && != jsontextValueType { = (, , "inlined Go struct field %s of type %s must not implement marshal or unmarshal methods", .Name, ) }// Handle an inlined field that serializes to/from // a finite number of JSON object members backed by a Go struct.if .Kind() == reflect.Struct {if .unknown { = (, , "inlined Go struct field %s of type %s with `unknown` tag must be a Go map of string key or a jsontext.Value", .Name, )continue// invalid inlined field; treat as ignored }if . { = append(, {, .index, ![]}) } [] = truecontinue } elseif !.IsExported() { = (, , "inlined Go struct field %s is not exported", .Name)continue// invalid inlined field; treat as ignored }// Handle an inlined field that serializes to/from any number of // JSON object members back by a Go map or jsontext.Value.switch {case == jsontextValueType: .fncs = nil// specially handled in arshal_inlined.gocase .Kind() == reflect.Map && .Key().Kind() == reflect.String:ifimplementsAny(.Key(), allMethodTypes...) { = (, , "inlined map field %s of type %s must have a string key that does not implement marshal or unmarshal methods", .Name, )continue// invalid inlined field; treat as ignored } .fncs = lookupArshaler(.Elem())default: = (, , "inlined Go struct field %s of type %s must be a Go struct, Go map of string key, or jsontext.Value", .Name, )continue// invalid inlined field; treat as ignored }// Reject multiple inlined fallback fields within the same struct.if >= 0 { = (, , "inlined Go struct fields %s and %s cannot both be a Go map or jsontext.Value", .Field().Name, .Name)// Still append f to inlinedFallbacks as there is still a // check for a dominant inlined fallback before returning. } = = append(, ) } else {// Handle normal Go struct field that serializes to/from // a single JSON object member.// Unexported fields cannot be serialized except for // embedded fields of a struct type, // which might promote exported fields of their own.if !.IsExported() { := indirectType(.typ)if !(.Anonymous && .Kind() == reflect.Struct) { = (, , "Go struct field %s is not exported", .Name)continue }// Unfortunately, methods on the unexported field // still cannot be called.ifimplementsAny(, allMethodTypes...) || (.omitzero && implementsAny(, isZeroerType)) { = (, , "Go struct field %s is not exported for method calls", .Name)continue } }// Provide a function that uses a type's IsZero method.switch {case .Type.Kind() == reflect.Interface && .Type.Implements(isZeroerType): .isZero = func( addressableValue) bool {// Avoid panics calling IsZero on a nil interface or // non-nil interface with nil pointer.return .IsNil() || (.Elem().Kind() == reflect.Pointer && .Elem().IsNil()) || .Interface().(isZeroer).IsZero() }case .Type.Kind() == reflect.Pointer && .Type.Implements(isZeroerType): .isZero = func( addressableValue) bool {// Avoid panics calling IsZero on nil pointer.return .IsNil() || .Interface().(isZeroer).IsZero() }case .Type.Implements(isZeroerType): .isZero = func( addressableValue) bool { return .Interface().(isZeroer).IsZero() }casereflect.PointerTo(.Type).Implements(isZeroerType): .isZero = func( addressableValue) bool { return .Addr().Interface().(isZeroer).IsZero() } }// Provide a function that can determine whether the value would // serialize as an empty JSON value.switch .Type.Kind() {casereflect.String, reflect.Map, reflect.Array, reflect.Slice: .isEmpty = func( addressableValue) bool { return .Len() == 0 }casereflect.Pointer, reflect.Interface: .isEmpty = func( addressableValue) bool { return .IsNil() } }// Reject multiple fields with same name within the same struct.if , := [.name]; { = (, , "Go struct fields %s and %s conflict over JSON object name %q", .Field().Name, .Name, .name)// Still append f to allFields as there is still a // check for a dominant field before returning. } [.name] = .id = len() .fncs = lookupArshaler(.Type) = append(, ) } }// NOTE: New users to the json package are occasionally surprised that // unexported fields are ignored. This occurs by necessity due to our // inability to directly introspect such fields with Go reflection // without the use of unsafe. // // To reduce friction here, refuse to serialize any Go struct that // has no JSON serializable fields, has at least one Go struct field, // and does not have any `json` tags present. For example, // errors returned by errors.New would fail to serialize. := .NumField() == 0if ! && ! && ! { = cmp.Or(, &SemanticError{GoType: , Err: errNoExportedFields}) } }// Sort the fields by exact name (breaking ties by depth and // then by presence of an explicitly provided JSON name). // Select the dominant field from each set of fields with the same name. // If multiple fields have the same name, then the dominant field // is the one that exists alone at the shallowest depth, // or the one that is uniquely tagged with a JSON name. // Otherwise, no dominant field exists for the set. := [:0]slices.SortStableFunc(, func(, structField) int {returncmp.Or(strings.Compare(.name, .name),cmp.Compare(len(.index), len(.index)),boolsCompare(!.hasName, !.hasName)) })forlen() > 0 { := 1// number of fields with the same exact namefor < len() && [-1].name == [].name { ++ }if == 1 || len([0].index) != len([1].index) || [0].hasName != [1].hasName { = append(, [0]) // only keep field if there is a dominant field } = [:] }// Sort the fields according to a breadth-first ordering // so that we can re-number IDs with the smallest possible values. // This optimizes use of uintSet such that it fits in the 64-entry bit set.slices.SortFunc(, func(, structField) int {returncmp.Compare(.id, .id) })for := range { [].id = }// Sort the fields according to a depth-first ordering // as the typical order that fields are marshaled.slices.SortFunc(, func(, structField) int {returnslices.Compare(.index, .index) })// Compute the mapping of fields in the byActualName map. // Pre-fold all names so that we can lookup folded names quickly. = structFields{flattened: ,byActualName: make(map[string]*structField, len()),byFoldedName: make(map[string][]*structField, len()), }for , := range .flattened { := string(foldName([]byte(.name))) .byActualName[.name] = &.flattened[] .byFoldedName[] = append(.byFoldedName[], &.flattened[]) }for , := range .byFoldedName {iflen() > 1 {// The precedence order for conflicting ignoreCase names // is by breadth-first order, rather than depth-first order.slices.SortFunc(, func(, *structField) int {returncmp.Compare(.id, .id) }) .byFoldedName[] = } }if := len(); == 1 || ( > 1 && len([0].index) != len([1].index)) { .inlinedFallback = &[0] // dominant inlined fallback field } .reindex()return , }// indirectType unwraps one level of pointer indirection// similar to how Go only allows embedding either T or *T,// but not **T or P (which is a named pointer).func indirectType( reflect.Type) reflect.Type {if .Kind() == reflect.Pointer && .Name() == "" { = .Elem() }return}// matchFoldedName matches a case-insensitive name depending on the options.// It assumes that foldName(f.name) == foldName(name).//// Case-insensitive matching is used if the `case:ignore` tag option is specified// or the MatchCaseInsensitiveNames call option is specified// (and the `case:strict` tag option is not specified).// Functionally, the `case:ignore` and `case:strict` tag options take precedence.//// The v1 definition of case-insensitivity operated under strings.EqualFold// and would strictly compare dashes and underscores,// while the v2 definition would ignore the presence of dashes and underscores.// Thus, if the MatchCaseSensitiveDelimiter call option is specified,// the match is further restricted to using strings.EqualFold.func ( *structField) ( []byte, *jsonflags.Flags) bool {if .casing == caseIgnore || (.Get(jsonflags.MatchCaseInsensitiveNames) && .casing != caseStrict) {if !.Get(jsonflags.MatchCaseSensitiveDelimiter) || strings.EqualFold(string(), .name) {returntrue } }returnfalse}const ( caseIgnore = 1 caseStrict = 2)type fieldOptions struct { name string quotedName string// quoted name per RFC 8785, section 3.2.2.2. hasName bool nameNeedEscape bool casing int8// either 0, caseIgnore, or caseStrict inline bool unknown bool omitzero bool omitempty bool string bool format string}// parseFieldOptions parses the `json` tag in a Go struct field as// a structured set of options configuring parameters such as// the JSON member name and other features.func parseFieldOptions( reflect.StructField) ( fieldOptions, bool, error) { , := .Tag.Lookup("json")// Check whether this field is explicitly ignored.if == "-" {returnfieldOptions{}, true, nil }// Check whether this field is unexported and not embedded, // which Go reflection cannot mutate for the sake of serialization. // // An embedded field of an unexported type is still capable of // forwarding exported fields, which may be JSON serialized. // This technically operates on the edge of what is permissible by // the Go language, but the most recent decision is to permit this. // // See https://go.dev/issue/24153 and https://go.dev/issue/32772.if !.IsExported() && !.Anonymous {// Tag options specified on an unexported field suggests user error.if { = cmp.Or(, fmt.Errorf("unexported Go struct field %s cannot have non-ignored `json:%q` tag", .Name, )) }returnfieldOptions{}, true, }// Determine the JSON member name for this Go field. A user-specified name // may be provided as either an identifier or a single-quoted string. // The single-quoted string allows arbitrary characters in the name. // See https://go.dev/issue/2718 and https://go.dev/issue/3546. .name = .Name// always starts with an uppercase characteriflen() > 0 && !strings.HasPrefix(, ",") {// For better compatibility with v1, accept almost any unescaped name. := len() - len(strings.TrimLeftFunc(, func( rune) bool {return !strings.ContainsRune(",\\'\"`", ) // reserve comma, backslash, and quotes })) := [:]// If the next character is not a comma, then the name is either // malformed (if n > 0) or a single-quoted name. // In either case, call consumeTagOption to handle it further.varerrorif !strings.HasPrefix([:], ",") && len() != len() { , , = consumeTagOption()if != nil { = cmp.Or(, fmt.Errorf("Go struct field %s has malformed `json` tag: %v", .Name, )) } }if !utf8.ValidString() { = cmp.Or(, fmt.Errorf("Go struct field %s has JSON object name %q with invalid UTF-8", .Name, )) = string([]rune()) // replace invalid UTF-8 with utf8.RuneError }if == nil { .hasName = true .name = } = [:] } , := jsonwire.AppendQuote(nil, .name, &jsonflags.Flags{}) .quotedName = string() .nameNeedEscape = jsonwire.NeedEscape(.name)// Handle any additional tag options (if any).varbool := make(map[string]bool)forlen() > 0 {// Consume comma delimiter.if [0] != ',' { = cmp.Or(, fmt.Errorf("Go struct field %s has malformed `json` tag: invalid character %q before next option (expecting ',')", .Name, [0])) } else { = [len(","):]iflen() == 0 { = cmp.Or(, fmt.Errorf("Go struct field %s has malformed `json` tag: invalid trailing ',' character", .Name))break } }// Consume and process the tag option. , , := consumeTagOption()if != nil { = cmp.Or(, fmt.Errorf("Go struct field %s has malformed `json` tag: %v", .Name, )) } := [:] = [:]switch {case : = cmp.Or(, fmt.Errorf("Go struct field %s has `format` tag option that was not specified last", .Name))casestrings.HasPrefix(, "'") && strings.TrimFunc(, isLetterOrDigit) == "": = cmp.Or(, fmt.Errorf("Go struct field %s has unnecessarily quoted appearance of `%s` tag option; specify `%s` instead", .Name, , )) }switch {case"case":if !strings.HasPrefix(, ":") { = cmp.Or(, fmt.Errorf("Go struct field %s is missing value for `case` tag option; specify `case:ignore` or `case:strict` instead", .Name))break } = [len(":"):] , , := consumeTagOption()if != nil { = cmp.Or(, fmt.Errorf("Go struct field %s has malformed value for `case` tag option: %v", .Name, ))break } := [:] = [:]ifstrings.HasPrefix(, "'") { = cmp.Or(, fmt.Errorf("Go struct field %s has unnecessarily quoted appearance of `case:%s` tag option; specify `case:%s` instead", .Name, , )) }switch {case"ignore": .casing |= caseIgnorecase"strict": .casing |= caseStrictdefault: = cmp.Or(, fmt.Errorf("Go struct field %s has unknown `case:%s` tag value", .Name, )) }case"inline": .inline = truecase"unknown": .unknown = truecase"omitzero": .omitzero = truecase"omitempty": .omitempty = truecase"string": .string = truecase"format":if !strings.HasPrefix(, ":") { = cmp.Or(, fmt.Errorf("Go struct field %s is missing value for `format` tag option", .Name))break } = [len(":"):] , , := consumeTagOption()if != nil { = cmp.Or(, fmt.Errorf("Go struct field %s has malformed value for `format` tag option: %v", .Name, ))break } = [:] .format = = truedefault:// Reject keys that resemble one of the supported options. // This catches invalid mutants such as "omitEmpty" or "omit_empty". := strings.ReplaceAll(strings.ToLower(), "_", "")switch {case"case", "inline", "unknown", "omitzero", "omitempty", "string", "format": = cmp.Or(, fmt.Errorf("Go struct field %s has invalid appearance of `%s` tag option; specify `%s` instead", .Name, , )) }// NOTE: Everything else is ignored. This does not mean it is // forward compatible to insert arbitrary tag options since // a future version of this package may understand that tag. }// Reject duplicates.switch {case .casing == caseIgnore|caseStrict: = cmp.Or(, fmt.Errorf("Go struct field %s cannot have both `case:ignore` and `case:strict` tag options", .Name))case []: = cmp.Or(, fmt.Errorf("Go struct field %s has duplicate appearance of `%s` tag option", .Name, )) } [] = true }return , false, }// consumeTagOption consumes the next option,// which is either a Go identifier or a single-quoted string.// If the next option is invalid, it returns all of in until the next comma,// and reports an error.func consumeTagOption( string) (string, int, error) {// For legacy compatibility with v1, assume options are comma-separated. := strings.IndexByte(, ',')if < 0 { = len() }switch , := utf8.DecodeRuneInString(); {// Option as a Go identifier.case == '_' || unicode.IsLetter(): := len() - len(strings.TrimLeftFunc(, isLetterOrDigit))return [:], , nil// Option as a single-quoted string.case == '\'':// The grammar is nearly identical to a double-quoted Go string literal, // but uses single quotes as the terminators. The reason for a custom // grammar is because both backtick and double quotes cannot be used // verbatim in a struct tag. // // Convert a single-quoted string to a double-quote string and rely on // strconv.Unquote to handle the rest.varbool := []byte{'"'} := len(`'`)forlen() > { , := utf8.DecodeRuneInString([:])switch {case :if == '\'' { = [:len()-1] // remove escape character: `\'` => `'` } = falsecase == '\\': = truecase == '"': = append(, '\\') // insert escape character: `"` => `\"`case == '\'': = append(, '"') += len(`'`) , := strconv.Unquote(string())if != nil {return [:], , fmt.Errorf("invalid single-quoted string: %s", [:]) }return , , nil } = append(, [:][:]...) += }if > 10 { = 10// limit the amount of context printed in the error }return [:], , fmt.Errorf("single-quoted string not terminated: %s...", [:])caselen() == 0:return [:], , io.ErrUnexpectedEOFdefault:return [:], , fmt.Errorf("invalid character %q at start of option (expecting Unicode letter or single quote)", ) }}func isLetterOrDigit( rune) bool {return == '_' || unicode.IsLetter() || unicode.IsNumber()}// boolsCompare compares x and y, ordering false before true.func boolsCompare(, bool) int {switch {case ! && :return -1default:return0case && !:return +1 }}
The pages are generated with Goldsv0.7.7-preview. (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.