| | |
| | |
| | |
| |
|
| | |
| |
|
| | package json |
| |
|
| | import ( |
| | "cmp" |
| | "errors" |
| | "fmt" |
| | "io" |
| | "reflect" |
| | "slices" |
| | "strconv" |
| | "strings" |
| | "unicode" |
| | "unicode/utf8" |
| |
|
| | "encoding/json/internal/jsonflags" |
| | "encoding/json/internal/jsonwire" |
| | ) |
| |
|
| | type isZeroer interface { |
| | IsZero() bool |
| | } |
| |
|
| | var isZeroerType = reflect.TypeFor[isZeroer]() |
| |
|
| | type structFields struct { |
| | flattened []structField |
| | byActualName map[string]*structField |
| | byFoldedName map[string][]*structField |
| | inlinedFallback *structField |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func (sf *structFields) reindex() { |
| | reindex := func(f *structField) { |
| | f.index0 = f.index[0] |
| | f.index = f.index[1:] |
| | if len(f.index) == 0 { |
| | f.index = nil |
| | } |
| | } |
| | for i := range sf.flattened { |
| | reindex(&sf.flattened[i]) |
| | } |
| | if sf.inlinedFallback != nil { |
| | reindex(sf.inlinedFallback) |
| | } |
| | } |
| |
|
| | |
| | |
| | func (fs *structFields) lookupByFoldedName(name []byte) []*structField { |
| | return fs.byFoldedName[string(foldName(name))] |
| | } |
| |
|
| | type structField struct { |
| | id int |
| | index0 int |
| | index []int |
| | typ reflect.Type |
| | fncs *arshaler |
| | isZero func(addressableValue) bool |
| | isEmpty func(addressableValue) bool |
| | fieldOptions |
| | } |
| |
|
| | var errNoExportedFields = errors.New("Go struct has no exported fields") |
| |
|
| | func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError) { |
| | orErrorf := func(serr *SemanticError, t reflect.Type, f string, a ...any) *SemanticError { |
| | return cmp.Or(serr, &SemanticError{GoType: t, Err: fmt.Errorf(f, a...)}) |
| | } |
| |
|
| | |
| | var queueIndex int |
| | type queueEntry struct { |
| | typ reflect.Type |
| | index []int |
| | visitChildren bool |
| | } |
| | queue := []queueEntry{{root, nil, true}} |
| | seen := map[reflect.Type]bool{root: true} |
| |
|
| | |
| | |
| | var allFields, inlinedFallbacks []structField |
| | for queueIndex < len(queue) { |
| | qe := queue[queueIndex] |
| | queueIndex++ |
| |
|
| | t := qe.typ |
| | inlinedFallbackIndex := -1 |
| | namesIndex := make(map[string]int) |
| | var hasAnyJSONTag bool |
| | var hasAnyJSONField bool |
| | for i := range t.NumField() { |
| | sf := t.Field(i) |
| | _, hasTag := sf.Tag.Lookup("json") |
| | hasAnyJSONTag = hasAnyJSONTag || hasTag |
| | options, ignored, err := parseFieldOptions(sf) |
| | if err != nil { |
| | serr = cmp.Or(serr, &SemanticError{GoType: t, Err: err}) |
| | } |
| | if ignored { |
| | continue |
| | } |
| | hasAnyJSONField = true |
| | f := structField{ |
| | |
| | |
| | |
| | index: append(append(make([]int, 0, len(qe.index)+1), qe.index...), i), |
| | typ: sf.Type, |
| | fieldOptions: options, |
| | } |
| | if sf.Anonymous && !f.hasName { |
| | if indirectType(f.typ).Kind() != reflect.Struct { |
| | serr = orErrorf(serr, t, "embedded Go struct field %s of non-struct type must be explicitly given a JSON name", sf.Name) |
| | } else { |
| | f.inline = true |
| | } |
| | } |
| | if f.inline || f.unknown { |
| | |
| | |
| |
|
| | switch f.fieldOptions { |
| | case fieldOptions{name: f.name, quotedName: f.quotedName, inline: true}: |
| | case fieldOptions{name: f.name, quotedName: f.quotedName, unknown: true}: |
| | case fieldOptions{name: f.name, quotedName: f.quotedName, inline: true, unknown: true}: |
| | serr = orErrorf(serr, t, "Go struct field %s cannot have both `inline` and `unknown` specified", sf.Name) |
| | f.inline = false |
| | default: |
| | serr = orErrorf(serr, t, "Go struct field %s cannot have any options other than `inline` or `unknown` specified", sf.Name) |
| | if f.hasName { |
| | continue |
| | } |
| | f.fieldOptions = fieldOptions{name: f.name, quotedName: f.quotedName, inline: f.inline, unknown: f.unknown} |
| | if f.inline && f.unknown { |
| | f.inline = false |
| | } |
| | } |
| |
|
| | |
| | |
| | tf := indirectType(f.typ) |
| | if implementsAny(tf, allMethodTypes...) && tf != jsontextValueType { |
| | serr = orErrorf(serr, t, "inlined Go struct field %s of type %s must not implement marshal or unmarshal methods", sf.Name, tf) |
| | } |
| |
|
| | |
| | |
| | if tf.Kind() == reflect.Struct { |
| | if f.unknown { |
| | serr = orErrorf(serr, t, "inlined Go struct field %s of type %s with `unknown` tag must be a Go map of string key or a jsontext.Value", sf.Name, tf) |
| | continue |
| | } |
| | if qe.visitChildren { |
| | queue = append(queue, queueEntry{tf, f.index, !seen[tf]}) |
| | } |
| | seen[tf] = true |
| | continue |
| | } else if !sf.IsExported() { |
| | serr = orErrorf(serr, t, "inlined Go struct field %s is not exported", sf.Name) |
| | continue |
| | } |
| |
|
| | |
| | |
| | switch { |
| | case tf == jsontextValueType: |
| | f.fncs = nil |
| | case tf.Kind() == reflect.Map && tf.Key().Kind() == reflect.String: |
| | if implementsAny(tf.Key(), allMethodTypes...) { |
| | serr = orErrorf(serr, t, "inlined map field %s of type %s must have a string key that does not implement marshal or unmarshal methods", sf.Name, tf) |
| | continue |
| | } |
| | f.fncs = lookupArshaler(tf.Elem()) |
| | default: |
| | serr = orErrorf(serr, t, "inlined Go struct field %s of type %s must be a Go struct, Go map of string key, or jsontext.Value", sf.Name, tf) |
| | continue |
| | } |
| |
|
| | |
| | if inlinedFallbackIndex >= 0 { |
| | serr = orErrorf(serr, t, "inlined Go struct fields %s and %s cannot both be a Go map or jsontext.Value", t.Field(inlinedFallbackIndex).Name, sf.Name) |
| | |
| | |
| | } |
| | inlinedFallbackIndex = i |
| |
|
| | inlinedFallbacks = append(inlinedFallbacks, f) |
| | } else { |
| | |
| | |
| |
|
| | |
| | |
| | |
| | if !sf.IsExported() { |
| | tf := indirectType(f.typ) |
| | if !(sf.Anonymous && tf.Kind() == reflect.Struct) { |
| | serr = orErrorf(serr, t, "Go struct field %s is not exported", sf.Name) |
| | continue |
| | } |
| | |
| | |
| | if implementsAny(tf, allMethodTypes...) || |
| | (f.omitzero && implementsAny(tf, isZeroerType)) { |
| | serr = orErrorf(serr, t, "Go struct field %s is not exported for method calls", sf.Name) |
| | continue |
| | } |
| | } |
| |
|
| | |
| | switch { |
| | case sf.Type.Kind() == reflect.Interface && sf.Type.Implements(isZeroerType): |
| | f.isZero = func(va addressableValue) bool { |
| | |
| | |
| | return va.IsNil() || (va.Elem().Kind() == reflect.Pointer && va.Elem().IsNil()) || va.Interface().(isZeroer).IsZero() |
| | } |
| | case sf.Type.Kind() == reflect.Pointer && sf.Type.Implements(isZeroerType): |
| | f.isZero = func(va addressableValue) bool { |
| | |
| | return va.IsNil() || va.Interface().(isZeroer).IsZero() |
| | } |
| | case sf.Type.Implements(isZeroerType): |
| | f.isZero = func(va addressableValue) bool { return va.Interface().(isZeroer).IsZero() } |
| | case reflect.PointerTo(sf.Type).Implements(isZeroerType): |
| | f.isZero = func(va addressableValue) bool { return va.Addr().Interface().(isZeroer).IsZero() } |
| | } |
| |
|
| | |
| | |
| | switch sf.Type.Kind() { |
| | case reflect.String, reflect.Map, reflect.Array, reflect.Slice: |
| | f.isEmpty = func(va addressableValue) bool { return va.Len() == 0 } |
| | case reflect.Pointer, reflect.Interface: |
| | f.isEmpty = func(va addressableValue) bool { return va.IsNil() } |
| | } |
| |
|
| | |
| | if j, ok := namesIndex[f.name]; ok { |
| | serr = orErrorf(serr, t, "Go struct fields %s and %s conflict over JSON object name %q", t.Field(j).Name, sf.Name, f.name) |
| | |
| | |
| | } |
| | namesIndex[f.name] = i |
| |
|
| | f.id = len(allFields) |
| | f.fncs = lookupArshaler(sf.Type) |
| | allFields = append(allFields, f) |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | isEmptyStruct := t.NumField() == 0 |
| | if !isEmptyStruct && !hasAnyJSONTag && !hasAnyJSONField { |
| | serr = cmp.Or(serr, &SemanticError{GoType: t, Err: errNoExportedFields}) |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | flattened := allFields[:0] |
| | slices.SortStableFunc(allFields, func(x, y structField) int { |
| | return cmp.Or( |
| | strings.Compare(x.name, y.name), |
| | cmp.Compare(len(x.index), len(y.index)), |
| | boolsCompare(!x.hasName, !y.hasName)) |
| | }) |
| | for len(allFields) > 0 { |
| | n := 1 |
| | for n < len(allFields) && allFields[n-1].name == allFields[n].name { |
| | n++ |
| | } |
| | if n == 1 || len(allFields[0].index) != len(allFields[1].index) || allFields[0].hasName != allFields[1].hasName { |
| | flattened = append(flattened, allFields[0]) |
| | } |
| | allFields = allFields[n:] |
| | } |
| |
|
| | |
| | |
| | |
| | slices.SortFunc(flattened, func(x, y structField) int { |
| | return cmp.Compare(x.id, y.id) |
| | }) |
| | for i := range flattened { |
| | flattened[i].id = i |
| | } |
| |
|
| | |
| | |
| | slices.SortFunc(flattened, func(x, y structField) int { |
| | return slices.Compare(x.index, y.index) |
| | }) |
| |
|
| | |
| | |
| | fs = structFields{ |
| | flattened: flattened, |
| | byActualName: make(map[string]*structField, len(flattened)), |
| | byFoldedName: make(map[string][]*structField, len(flattened)), |
| | } |
| | for i, f := range fs.flattened { |
| | foldedName := string(foldName([]byte(f.name))) |
| | fs.byActualName[f.name] = &fs.flattened[i] |
| | fs.byFoldedName[foldedName] = append(fs.byFoldedName[foldedName], &fs.flattened[i]) |
| | } |
| | for foldedName, fields := range fs.byFoldedName { |
| | if len(fields) > 1 { |
| | |
| | |
| | slices.SortFunc(fields, func(x, y *structField) int { |
| | return cmp.Compare(x.id, y.id) |
| | }) |
| | fs.byFoldedName[foldedName] = fields |
| | } |
| | } |
| | if n := len(inlinedFallbacks); n == 1 || (n > 1 && len(inlinedFallbacks[0].index) != len(inlinedFallbacks[1].index)) { |
| | fs.inlinedFallback = &inlinedFallbacks[0] |
| | } |
| | fs.reindex() |
| | return fs, serr |
| | } |
| |
|
| | |
| | |
| | |
| | func indirectType(t reflect.Type) reflect.Type { |
| | if t.Kind() == reflect.Pointer && t.Name() == "" { |
| | t = t.Elem() |
| | } |
| | return t |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | func (f *structField) matchFoldedName(name []byte, flags *jsonflags.Flags) bool { |
| | if f.casing == caseIgnore || (flags.Get(jsonflags.MatchCaseInsensitiveNames) && f.casing != caseStrict) { |
| | if !flags.Get(jsonflags.MatchCaseSensitiveDelimiter) || strings.EqualFold(string(name), f.name) { |
| | return true |
| | } |
| | } |
| | return false |
| | } |
| |
|
| | const ( |
| | caseIgnore = 1 |
| | caseStrict = 2 |
| | ) |
| |
|
| | type fieldOptions struct { |
| | name string |
| | quotedName string |
| | hasName bool |
| | nameNeedEscape bool |
| | casing int8 |
| | inline bool |
| | unknown bool |
| | omitzero bool |
| | omitempty bool |
| | string bool |
| | format string |
| | } |
| |
|
| | |
| | |
| | |
| | func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool, err error) { |
| | tag, hasTag := sf.Tag.Lookup("json") |
| | tagOrig := tag |
| |
|
| | |
| | if tag == "-" { |
| | return fieldOptions{}, true, nil |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if !sf.IsExported() && !sf.Anonymous { |
| | |
| | if hasTag { |
| | err = cmp.Or(err, fmt.Errorf("unexported Go struct field %s cannot have non-ignored `json:%q` tag", sf.Name, tag)) |
| | } |
| | return fieldOptions{}, true, err |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | out.name = sf.Name |
| | if len(tag) > 0 && !strings.HasPrefix(tag, ",") { |
| | |
| | n := len(tag) - len(strings.TrimLeftFunc(tag, func(r rune) bool { |
| | return !strings.ContainsRune(",\\'\"`", r) |
| | })) |
| | name := tag[:n] |
| |
|
| | |
| | |
| | |
| | var err2 error |
| | if !strings.HasPrefix(tag[n:], ",") && len(name) != len(tag) { |
| | name, n, err2 = consumeTagOption(tag) |
| | if err2 != nil { |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: %v", sf.Name, err2)) |
| | } |
| | } |
| | if !utf8.ValidString(name) { |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has JSON object name %q with invalid UTF-8", sf.Name, name)) |
| | name = string([]rune(name)) |
| | } |
| | if name == "-" && tag[0] == '-' { |
| | defer func() { |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has JSON object name %q; either "+ |
| | "use `json:\"-\"` to ignore the field or "+ |
| | "use `json:\"'-'%s` to specify %q as the name", sf.Name, out.name, strings.TrimPrefix(strconv.Quote(tagOrig), `"-`), name)) |
| | }() |
| | } |
| | if err2 == nil { |
| | out.hasName = true |
| | out.name = name |
| | } |
| | tag = tag[n:] |
| | } |
| | b, _ := jsonwire.AppendQuote(nil, out.name, &jsonflags.Flags{}) |
| | out.quotedName = string(b) |
| | out.nameNeedEscape = jsonwire.NeedEscape(out.name) |
| |
|
| | |
| | var wasFormat bool |
| | seenOpts := make(map[string]bool) |
| | for len(tag) > 0 { |
| | |
| | if tag[0] != ',' { |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: invalid character %q before next option (expecting ',')", sf.Name, tag[0])) |
| | } else { |
| | tag = tag[len(","):] |
| | if len(tag) == 0 { |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: invalid trailing ',' character", sf.Name)) |
| | break |
| | } |
| | } |
| |
|
| | |
| | opt, n, err2 := consumeTagOption(tag) |
| | if err2 != nil { |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: %v", sf.Name, err2)) |
| | } |
| | rawOpt := tag[:n] |
| | tag = tag[n:] |
| | switch { |
| | case wasFormat: |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has `format` tag option that was not specified last", sf.Name)) |
| | case strings.HasPrefix(rawOpt, "'") && strings.TrimFunc(opt, isLetterOrDigit) == "": |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has unnecessarily quoted appearance of `%s` tag option; specify `%s` instead", sf.Name, rawOpt, opt)) |
| | } |
| | switch opt { |
| | case "case": |
| | if !strings.HasPrefix(tag, ":") { |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s is missing value for `case` tag option; specify `case:ignore` or `case:strict` instead", sf.Name)) |
| | break |
| | } |
| | tag = tag[len(":"):] |
| | opt, n, err2 := consumeTagOption(tag) |
| | if err2 != nil { |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed value for `case` tag option: %v", sf.Name, err2)) |
| | break |
| | } |
| | rawOpt := tag[:n] |
| | tag = tag[n:] |
| | if strings.HasPrefix(rawOpt, "'") { |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has unnecessarily quoted appearance of `case:%s` tag option; specify `case:%s` instead", sf.Name, rawOpt, opt)) |
| | } |
| | switch opt { |
| | case "ignore": |
| | out.casing |= caseIgnore |
| | case "strict": |
| | out.casing |= caseStrict |
| | default: |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has unknown `case:%s` tag value", sf.Name, rawOpt)) |
| | } |
| | case "inline": |
| | out.inline = true |
| | case "unknown": |
| | out.unknown = true |
| | case "omitzero": |
| | out.omitzero = true |
| | case "omitempty": |
| | out.omitempty = true |
| | case "string": |
| | out.string = true |
| | case "format": |
| | if !strings.HasPrefix(tag, ":") { |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s is missing value for `format` tag option", sf.Name)) |
| | break |
| | } |
| | tag = tag[len(":"):] |
| | opt, n, err2 := consumeTagOption(tag) |
| | if err2 != nil { |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed value for `format` tag option: %v", sf.Name, err2)) |
| | break |
| | } |
| | tag = tag[n:] |
| | out.format = opt |
| | wasFormat = true |
| | default: |
| | |
| | |
| | normOpt := strings.ReplaceAll(strings.ToLower(opt), "_", "") |
| | switch normOpt { |
| | case "case", "inline", "unknown", "omitzero", "omitempty", "string", "format": |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has invalid appearance of `%s` tag option; specify `%s` instead", sf.Name, opt, normOpt)) |
| | } |
| |
|
| | |
| | |
| | |
| | } |
| |
|
| | |
| | switch { |
| | case out.casing == caseIgnore|caseStrict: |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s cannot have both `case:ignore` and `case:strict` tag options", sf.Name)) |
| | case seenOpts[opt]: |
| | err = cmp.Or(err, fmt.Errorf("Go struct field %s has duplicate appearance of `%s` tag option", sf.Name, rawOpt)) |
| | } |
| | seenOpts[opt] = true |
| | } |
| | return out, false, err |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func consumeTagOption(in string) (string, int, error) { |
| | |
| | i := strings.IndexByte(in, ',') |
| | if i < 0 { |
| | i = len(in) |
| | } |
| |
|
| | switch r, _ := utf8.DecodeRuneInString(in); { |
| | |
| | case r == '_' || unicode.IsLetter(r): |
| | n := len(in) - len(strings.TrimLeftFunc(in, isLetterOrDigit)) |
| | return in[:n], n, nil |
| | |
| | case r == '\'': |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | var inEscape bool |
| | b := []byte{'"'} |
| | n := len(`'`) |
| | for len(in) > n { |
| | r, rn := utf8.DecodeRuneInString(in[n:]) |
| | switch { |
| | case inEscape: |
| | if r == '\'' { |
| | b = b[:len(b)-1] |
| | } |
| | inEscape = false |
| | case r == '\\': |
| | inEscape = true |
| | case r == '"': |
| | b = append(b, '\\') |
| | case r == '\'': |
| | b = append(b, '"') |
| | n += len(`'`) |
| | out, err := strconv.Unquote(string(b)) |
| | if err != nil { |
| | return in[:i], i, fmt.Errorf("invalid single-quoted string: %s", in[:n]) |
| | } |
| | return out, n, nil |
| | } |
| | b = append(b, in[n:][:rn]...) |
| | n += rn |
| | } |
| | if n > 10 { |
| | n = 10 |
| | } |
| | return in[:i], i, fmt.Errorf("single-quoted string not terminated: %s...", in[:n]) |
| | case len(in) == 0: |
| | return in[:i], i, io.ErrUnexpectedEOF |
| | default: |
| | return in[:i], i, fmt.Errorf("invalid character %q at start of option (expecting Unicode letter or single quote)", r) |
| | } |
| | } |
| |
|
| | func isLetterOrDigit(r rune) bool { |
| | return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r) |
| | } |
| |
|
| | |
| | func boolsCompare(x, y bool) int { |
| | switch { |
| | case !x && y: |
| | return -1 |
| | default: |
| | return 0 |
| | case x && !y: |
| | return +1 |
| | } |
| | } |
| |
|