// 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.jsonv2 package json import ( "encoding" "errors" "reflect" "testing" "encoding/json/internal/jsontest" "encoding/json/jsontext" ) type unexported struct{} func TestMakeStructFields(t *testing.T) { type Embed struct { Foo string } type Recursive struct { A string *Recursive `json:",inline"` B string } type MapStringAny map[string]any tests := []struct { name jsontest.CaseName in any want structFields wantErr error }{{ name: jsontest.Name("Names"), in: struct { F1 string F2 string `json:"-"` F3 string `json:"json_name"` f3 string F5 string `json:"json_name_nocase,case:ignore"` }{}, want: structFields{ flattened: []structField{ {id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{name: "F1", quotedName: `"F1"`}}, {id: 1, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "json_name", quotedName: `"json_name"`, hasName: true}}, {id: 2, index: []int{4}, typ: stringType, fieldOptions: fieldOptions{name: "json_name_nocase", quotedName: `"json_name_nocase"`, hasName: true, casing: caseIgnore}}, }, }, }, { name: jsontest.Name("BreadthFirstSearch"), in: struct { L1A string L1B struct { L2A string L2B struct { L3A string } `json:",inline"` L2C string } `json:",inline"` L1C string L1D struct { L2D string L2E struct { L3B string } `json:",inline"` L2F string } `json:",inline"` L1E string }{}, want: structFields{ flattened: []structField{ {id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{name: "L1A", quotedName: `"L1A"`}}, {id: 3, index: []int{1, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L2A", quotedName: `"L2A"`}}, {id: 7, index: []int{1, 1, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L3A", quotedName: `"L3A"`}}, {id: 4, index: []int{1, 2}, typ: stringType, fieldOptions: fieldOptions{name: "L2C", quotedName: `"L2C"`}}, {id: 1, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "L1C", quotedName: `"L1C"`}}, {id: 5, index: []int{3, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L2D", quotedName: `"L2D"`}}, {id: 8, index: []int{3, 1, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L3B", quotedName: `"L3B"`}}, {id: 6, index: []int{3, 2}, typ: stringType, fieldOptions: fieldOptions{name: "L2F", quotedName: `"L2F"`}}, {id: 2, index: []int{4}, typ: stringType, fieldOptions: fieldOptions{name: "L1E", quotedName: `"L1E"`}}, }, }, }, { name: jsontest.Name("NameResolution"), in: struct { X1 struct { X struct { A string // loses in precedence to A B string // cancels out with X2.X.B D string // loses in precedence to D } `json:",inline"` } `json:",inline"` X2 struct { X struct { B string // cancels out with X1.X.B C string D string // loses in precedence to D } `json:",inline"` } `json:",inline"` A string // takes precedence over X1.X.A D string // takes precedence over X1.X.D and X2.X.D }{}, want: structFields{ flattened: []structField{ {id: 2, index: []int{1, 0, 1}, typ: stringType, fieldOptions: fieldOptions{name: "C", quotedName: `"C"`}}, {id: 0, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "A", quotedName: `"A"`}}, {id: 1, index: []int{3}, typ: stringType, fieldOptions: fieldOptions{name: "D", quotedName: `"D"`}}, }, }, }, { name: jsontest.Name("NameResolution/ExplicitNameUniquePrecedence"), in: struct { X1 struct { A string // loses in precedence to X2.A } `json:",inline"` X2 struct { A string `json:"A"` } `json:",inline"` X3 struct { A string // loses in precedence to X2.A } `json:",inline"` }{}, want: structFields{ flattened: []structField{ {id: 0, index: []int{1, 0}, typ: stringType, fieldOptions: fieldOptions{hasName: true, name: "A", quotedName: `"A"`}}, }, }, }, { name: jsontest.Name("NameResolution/ExplicitNameCancelsOut"), in: struct { X1 struct { A string // loses in precedence to X2.A or X3.A } `json:",inline"` X2 struct { A string `json:"A"` // cancels out with X3.A } `json:",inline"` X3 struct { A string `json:"A"` // cancels out with X2.A } `json:",inline"` }{}, want: structFields{flattened: []structField{}}, }, { name: jsontest.Name("Embed/Implicit"), in: struct { Embed }{}, want: structFields{ flattened: []structField{ {id: 0, index: []int{0, 0}, typ: stringType, fieldOptions: fieldOptions{name: "Foo", quotedName: `"Foo"`}}, }, }, }, { name: jsontest.Name("Embed/Explicit"), in: struct { Embed `json:",inline"` }{}, want: structFields{ flattened: []structField{ {id: 0, index: []int{0, 0}, typ: stringType, fieldOptions: fieldOptions{name: "Foo", quotedName: `"Foo"`}}, }, }, }, { name: jsontest.Name("Recursive"), in: struct { A string Recursive `json:",inline"` C string }{}, want: structFields{ flattened: []structField{ {id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{name: "A", quotedName: `"A"`}}, {id: 2, index: []int{1, 2}, typ: stringType, fieldOptions: fieldOptions{name: "B", quotedName: `"B"`}}, {id: 1, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "C", quotedName: `"C"`}}, }, }, }, { name: jsontest.Name("InlinedFallback/Cancelation"), in: struct { X1 struct { X jsontext.Value `json:",inline"` } `json:",inline"` X2 struct { X map[string]any `json:",unknown"` } `json:",inline"` }{}, want: structFields{}, }, { name: jsontest.Name("InlinedFallback/Precedence"), in: struct { X1 struct { X jsontext.Value `json:",inline"` } `json:",inline"` X2 struct { X map[string]any `json:",unknown"` } `json:",inline"` X map[string]jsontext.Value `json:",unknown"` }{}, want: structFields{ inlinedFallback: &structField{id: 0, index: []int{2}, typ: T[map[string]jsontext.Value](), fieldOptions: fieldOptions{name: "X", quotedName: `"X"`, unknown: true}}, }, }, { name: jsontest.Name("InlinedFallback/InvalidImplicit"), in: struct { MapStringAny }{}, want: structFields{ flattened: []structField{ {id: 0, index: []int{0}, typ: reflect.TypeOf(MapStringAny(nil)), fieldOptions: fieldOptions{name: "MapStringAny", quotedName: `"MapStringAny"`}}, }, }, wantErr: errors.New("embedded Go struct field MapStringAny of non-struct type must be explicitly given a JSON name"), }, { name: jsontest.Name("InvalidUTF8"), in: struct { Name string `json:"'\\xde\\xad\\xbe\\xef'"` }{}, want: structFields{ flattened: []structField{ {id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{hasName: true, name: "\u07ad\ufffd\ufffd", quotedName: "\"\u07ad\ufffd\ufffd\"", nameNeedEscape: true}}, }, }, wantErr: errors.New(`Go struct field Name has JSON object name "ޭ\xbe\xef" with invalid UTF-8`), }, { name: jsontest.Name("DuplicateName"), in: struct { A string `json:"same"` B string `json:"same"` }{}, want: structFields{flattened: []structField{}}, wantErr: errors.New(`Go struct fields A and B conflict over JSON object name "same"`), }, { name: jsontest.Name("BothInlineAndUnknown"), in: struct { A struct{} `json:",inline,unknown"` }{}, wantErr: errors.New("Go struct field A cannot have both `inline` and `unknown` specified"), }, { name: jsontest.Name("InlineWithOptions"), in: struct { A struct{} `json:",inline,omitempty"` }{}, wantErr: errors.New("Go struct field A cannot have any options other than `inline` or `unknown` specified"), }, { name: jsontest.Name("UnknownWithOptions"), in: struct { A map[string]any `json:",inline,omitempty"` }{}, want: structFields{inlinedFallback: &structField{ index: []int{0}, typ: reflect.TypeFor[map[string]any](), fieldOptions: fieldOptions{ name: "A", quotedName: `"A"`, inline: true, }, }}, wantErr: errors.New("Go struct field A cannot have any options other than `inline` or `unknown` specified"), }, { name: jsontest.Name("InlineTextMarshaler"), in: struct { A struct{ encoding.TextMarshaler } `json:",inline"` }{}, want: structFields{flattened: []structField{{ index: []int{0, 0}, typ: reflect.TypeFor[encoding.TextMarshaler](), fieldOptions: fieldOptions{ name: "TextMarshaler", quotedName: `"TextMarshaler"`, }, }}}, wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextMarshaler } must not implement marshal or unmarshal methods`), }, { name: jsontest.Name("InlineTextAppender"), in: struct { A struct{ encoding.TextAppender } `json:",inline"` }{}, want: structFields{flattened: []structField{{ index: []int{0, 0}, typ: reflect.TypeFor[encoding.TextAppender](), fieldOptions: fieldOptions{ name: "TextAppender", quotedName: `"TextAppender"`, }, }}}, wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextAppender } must not implement marshal or unmarshal methods`), }, { name: jsontest.Name("UnknownJSONMarshaler"), in: struct { A struct{ Marshaler } `json:",unknown"` }{}, wantErr: errors.New(`inlined Go struct field A of type struct { json.Marshaler } must not implement marshal or unmarshal methods`), }, { name: jsontest.Name("InlineJSONMarshalerTo"), in: struct { A struct{ MarshalerTo } `json:",inline"` }{}, want: structFields{flattened: []structField{{ index: []int{0, 0}, typ: reflect.TypeFor[MarshalerTo](), fieldOptions: fieldOptions{ name: "MarshalerTo", quotedName: `"MarshalerTo"`, }, }}}, wantErr: errors.New(`inlined Go struct field A of type struct { json.MarshalerTo } must not implement marshal or unmarshal methods`), }, { name: jsontest.Name("UnknownTextUnmarshaler"), in: struct { A *struct{ encoding.TextUnmarshaler } `json:",unknown"` }{}, wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextUnmarshaler } must not implement marshal or unmarshal methods`), }, { name: jsontest.Name("InlineJSONUnmarshaler"), in: struct { A *struct{ Unmarshaler } `json:",inline"` }{}, want: structFields{flattened: []structField{{ index: []int{0, 0}, typ: reflect.TypeFor[Unmarshaler](), fieldOptions: fieldOptions{ name: "Unmarshaler", quotedName: `"Unmarshaler"`, }, }}}, wantErr: errors.New(`inlined Go struct field A of type struct { json.Unmarshaler } must not implement marshal or unmarshal methods`), }, { name: jsontest.Name("UnknownJSONUnmarshalerFrom"), in: struct { A struct{ UnmarshalerFrom } `json:",unknown"` }{}, wantErr: errors.New(`inlined Go struct field A of type struct { json.UnmarshalerFrom } must not implement marshal or unmarshal methods`), }, { name: jsontest.Name("UnknownStruct"), in: struct { A struct { X, Y, Z string } `json:",unknown"` }{}, wantErr: errors.New("inlined Go struct field A of type struct { X string; Y string; Z string } with `unknown` tag must be a Go map of string key or a jsontext.Value"), }, { name: jsontest.Name("InlineUnsupported/MapIntKey"), in: struct { A map[int]any `json:",unknown"` }{}, wantErr: errors.New(`inlined Go struct field A of type map[int]interface {} must be a Go struct, Go map of string key, or jsontext.Value`), }, { name: jsontest.Name("InlineUnsupported/MapTextMarshalerStringKey"), in: struct { A map[nocaseString]any `json:",inline"` }{}, wantErr: errors.New(`inlined map field A of type map[json.nocaseString]interface {} must have a string key that does not implement marshal or unmarshal methods`), }, { name: jsontest.Name("InlineUnsupported/MapMarshalerStringKey"), in: struct { A map[stringMarshalEmpty]any `json:",inline"` }{}, wantErr: errors.New(`inlined map field A of type map[json.stringMarshalEmpty]interface {} must have a string key that does not implement marshal or unmarshal methods`), }, { name: jsontest.Name("InlineUnsupported/DoublePointer"), in: struct { A **struct{} `json:",inline"` }{}, wantErr: errors.New(`inlined Go struct field A of type *struct {} must be a Go struct, Go map of string key, or jsontext.Value`), }, { name: jsontest.Name("DuplicateInline"), in: struct { A map[string]any `json:",inline"` B jsontext.Value `json:",inline"` }{}, wantErr: errors.New(`inlined Go struct fields A and B cannot both be a Go map or jsontext.Value`), }, { name: jsontest.Name("DuplicateEmbedInline"), in: struct { A MapStringAny `json:",inline"` B jsontext.Value `json:",inline"` }{}, wantErr: errors.New(`inlined Go struct fields A and B cannot both be a Go map or jsontext.Value`), }} for _, tt := range tests { t.Run(tt.name.Name, func(t *testing.T) { got, err := makeStructFields(reflect.TypeOf(tt.in)) // Sanity check that pointers are consistent. pointers := make(map[*structField]bool) for i := range got.flattened { pointers[&got.flattened[i]] = true } for _, f := range got.byActualName { if !pointers[f] { t.Errorf("%s: byActualName pointer not in flattened", tt.name.Where) } } for _, fs := range got.byFoldedName { for _, f := range fs { if !pointers[f] { t.Errorf("%s: byFoldedName pointer not in flattened", tt.name.Where) } } } // Zero out fields that are incomparable. for i := range got.flattened { got.flattened[i].fncs = nil got.flattened[i].isEmpty = nil } if got.inlinedFallback != nil { got.inlinedFallback.fncs = nil got.inlinedFallback.isEmpty = nil } // Reproduce maps in want. tt.want.byActualName = make(map[string]*structField) for i := range tt.want.flattened { f := &tt.want.flattened[i] tt.want.byActualName[f.name] = f } tt.want.byFoldedName = make(map[string][]*structField) for i, f := range tt.want.flattened { foldedName := string(foldName([]byte(f.name))) tt.want.byFoldedName[foldedName] = append(tt.want.byFoldedName[foldedName], &tt.want.flattened[i]) } // Only compare underlying error to simplify test logic. var gotErr error if err != nil { gotErr = err.Err } tt.want.reindex() if !reflect.DeepEqual(got, tt.want) || !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("%s: makeStructFields(%T):\n\tgot (%v, %v)\n\twant (%v, %v)", tt.name.Where, tt.in, got, gotErr, tt.want, tt.wantErr) } }) } } func TestParseTagOptions(t *testing.T) { tests := []struct { name jsontest.CaseName in any // must be a struct with a single field wantOpts fieldOptions wantIgnored bool wantErr error }{{ name: jsontest.Name("GoName"), in: struct { FieldName int }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`}, }, { name: jsontest.Name("GoNameWithOptions"), in: struct { FieldName int `json:",inline"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true}, }, { name: jsontest.Name("Empty"), in: struct { V int `json:""` }{}, wantOpts: fieldOptions{name: "V", quotedName: `"V"`}, }, { name: jsontest.Name("Unexported"), in: struct { v int `json:"Hello"` }{}, wantIgnored: true, wantErr: errors.New("unexported Go struct field v cannot have non-ignored `json:\"Hello\"` tag"), }, { name: jsontest.Name("UnexportedEmpty"), in: struct { v int `json:""` }{}, wantIgnored: true, wantErr: errors.New("unexported Go struct field v cannot have non-ignored `json:\"\"` tag"), }, { name: jsontest.Name("EmbedUnexported"), in: struct { unexported }{}, wantOpts: fieldOptions{name: "unexported", quotedName: `"unexported"`}, }, { name: jsontest.Name("Ignored"), in: struct { V int `json:"-"` }{}, wantIgnored: true, }, { name: jsontest.Name("IgnoredEmbedUnexported"), in: struct { unexported `json:"-"` }{}, wantIgnored: true, }, { name: jsontest.Name("DashComma"), in: struct { V int `json:"-,"` }{}, wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`}, wantErr: errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"), }, { name: jsontest.Name("QuotedDashName"), in: struct { V int `json:"'-'"` }{}, wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`}, }, { name: jsontest.Name("LatinPunctuationName"), in: struct { V int `json:"$%-/"` }{}, wantOpts: fieldOptions{hasName: true, name: "$%-/", quotedName: `"$%-/"`}, }, { name: jsontest.Name("QuotedLatinPunctuationName"), in: struct { V int `json:"'$%-/'"` }{}, wantOpts: fieldOptions{hasName: true, name: "$%-/", quotedName: `"$%-/"`}, }, { name: jsontest.Name("LatinDigitsName"), in: struct { V int `json:"0123456789"` }{}, wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`}, }, { name: jsontest.Name("QuotedLatinDigitsName"), in: struct { V int `json:"'0123456789'"` }{}, wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`}, }, { name: jsontest.Name("LatinUppercaseName"), in: struct { V int `json:"ABCDEFGHIJKLMOPQRSTUVWXYZ"` }{}, wantOpts: fieldOptions{hasName: true, name: "ABCDEFGHIJKLMOPQRSTUVWXYZ", quotedName: `"ABCDEFGHIJKLMOPQRSTUVWXYZ"`}, }, { name: jsontest.Name("LatinLowercaseName"), in: struct { V int `json:"abcdefghijklmnopqrstuvwxyz_"` }{}, wantOpts: fieldOptions{hasName: true, name: "abcdefghijklmnopqrstuvwxyz_", quotedName: `"abcdefghijklmnopqrstuvwxyz_"`}, }, { name: jsontest.Name("GreekName"), in: struct { V string `json:"Ελλάδα"` }{}, wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`}, }, { name: jsontest.Name("QuotedGreekName"), in: struct { V string `json:"'Ελλάδα'"` }{}, wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`}, }, { name: jsontest.Name("ChineseName"), in: struct { V string `json:"世界"` }{}, wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`}, }, { name: jsontest.Name("QuotedChineseName"), in: struct { V string `json:"'世界'"` }{}, wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`}, }, { name: jsontest.Name("PercentSlashName"), in: struct { V int `json:"text/html%"` }{}, wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`}, }, { name: jsontest.Name("QuotedPercentSlashName"), in: struct { V int `json:"'text/html%'"` }{}, wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`}, }, { name: jsontest.Name("PunctuationName"), in: struct { V string `json:"!#$%&()*+-./:;<=>?@[]^_{|}~ "` }{}, wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`, nameNeedEscape: true}, }, { name: jsontest.Name("QuotedPunctuationName"), in: struct { V string `json:"'!#$%&()*+-./:;<=>?@[]^_{|}~ '"` }{}, wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`, nameNeedEscape: true}, }, { name: jsontest.Name("EmptyName"), in: struct { V int `json:"''"` }{}, wantOpts: fieldOptions{hasName: true, name: "", quotedName: `""`}, }, { name: jsontest.Name("SpaceName"), in: struct { V int `json:"' '"` }{}, wantOpts: fieldOptions{hasName: true, name: " ", quotedName: `" "`}, }, { name: jsontest.Name("CommaQuotes"), in: struct { V int `json:"',\\'\"\\\"'"` }{}, wantOpts: fieldOptions{hasName: true, name: `,'""`, quotedName: `",'\"\""`, nameNeedEscape: true}, }, { name: jsontest.Name("SingleComma"), in: struct { V int `json:","` }{}, wantOpts: fieldOptions{name: "V", quotedName: `"V"`}, wantErr: errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"), }, { name: jsontest.Name("SuperfluousCommas"), in: struct { V int `json:",,,,\"\",,inline,unknown,,,,"` }{}, wantOpts: fieldOptions{name: "V", quotedName: `"V"`, inline: true, unknown: true}, wantErr: errors.New("Go struct field V has malformed `json` tag: invalid character ',' at start of option (expecting Unicode letter or single quote)"), }, { name: jsontest.Name("CaseAloneOption"), in: struct { FieldName int `json:",case"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`}, wantErr: errors.New("Go struct field FieldName is missing value for `case` tag option; specify `case:ignore` or `case:strict` instead"), }, { name: jsontest.Name("CaseIgnoreOption"), in: struct { FieldName int `json:",case:ignore"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore}, }, { name: jsontest.Name("CaseStrictOption"), in: struct { FieldName int `json:",case:strict"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseStrict}, }, { name: jsontest.Name("CaseUnknownOption"), in: struct { FieldName int `json:",case:unknown"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`}, wantErr: errors.New("Go struct field FieldName has unknown `case:unknown` tag value"), }, { name: jsontest.Name("CaseQuotedOption"), in: struct { FieldName int `json:",case:'ignore'"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore}, wantErr: errors.New("Go struct field FieldName has unnecessarily quoted appearance of `case:'ignore'` tag option; specify `case:ignore` instead"), }, { name: jsontest.Name("BothCaseOptions"), in: struct { FieldName int `json:",case:ignore,case:strict"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore | caseStrict}, wantErr: errors.New("Go struct field FieldName cannot have both `case:ignore` and `case:strict` tag options"), }, { name: jsontest.Name("InlineOption"), in: struct { FieldName int `json:",inline"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true}, }, { name: jsontest.Name("UnknownOption"), in: struct { FieldName int `json:",unknown"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, unknown: true}, }, { name: jsontest.Name("OmitZeroOption"), in: struct { FieldName int `json:",omitzero"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitzero: true}, }, { name: jsontest.Name("OmitEmptyOption"), in: struct { FieldName int `json:",omitempty"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitempty: true}, }, { name: jsontest.Name("StringOption"), in: struct { FieldName int `json:",string"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, string: true}, }, { name: jsontest.Name("FormatOptionEqual"), in: struct { FieldName int `json:",format=fizzbuzz"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`}, wantErr: errors.New("Go struct field FieldName is missing value for `format` tag option"), }, { name: jsontest.Name("FormatOptionColon"), in: struct { FieldName int `json:",format:fizzbuzz"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "fizzbuzz"}, }, { name: jsontest.Name("FormatOptionQuoted"), in: struct { FieldName int `json:",format:'2006-01-02'"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "2006-01-02"}, }, { name: jsontest.Name("FormatOptionInvalid"), in: struct { FieldName int `json:",format:'2006-01-02"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`}, wantErr: errors.New("Go struct field FieldName has malformed value for `format` tag option: single-quoted string not terminated: '2006-01-0..."), }, { name: jsontest.Name("FormatOptionNotLast"), in: struct { FieldName int `json:",format:alpha,ordered"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "alpha"}, wantErr: errors.New("Go struct field FieldName has `format` tag option that was not specified last"), }, { name: jsontest.Name("AllOptions"), in: struct { FieldName int `json:",case:ignore,inline,unknown,omitzero,omitempty,string,format:format"` }{}, wantOpts: fieldOptions{ name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore, inline: true, unknown: true, omitzero: true, omitempty: true, string: true, format: "format", }, }, { name: jsontest.Name("AllOptionsQuoted"), in: struct { FieldName int `json:",'case':'ignore','inline','unknown','omitzero','omitempty','string','format':'format'"` }{}, wantOpts: fieldOptions{ name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore, inline: true, unknown: true, omitzero: true, omitempty: true, string: true, format: "format", }, wantErr: errors.New("Go struct field FieldName has unnecessarily quoted appearance of `'case'` tag option; specify `case` instead"), }, { name: jsontest.Name("AllOptionsCaseSensitive"), in: struct { FieldName int `json:",CASE:IGNORE,INLINE,UNKNOWN,OMITZERO,OMITEMPTY,STRING,FORMAT:FORMAT"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`}, wantErr: errors.New("Go struct field FieldName has invalid appearance of `CASE` tag option; specify `case` instead"), }, { name: jsontest.Name("AllOptionsSpaceSensitive"), in: struct { FieldName int `json:", case:ignore , inline , unknown , omitzero , omitempty , string , format:format "` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`}, wantErr: errors.New("Go struct field FieldName has malformed `json` tag: invalid character ' ' at start of option (expecting Unicode letter or single quote)"), }, { name: jsontest.Name("UnknownTagOption"), in: struct { FieldName int `json:",inline,whoknows,string"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true, string: true}, }, { name: jsontest.Name("MalformedQuotedString/MissingQuote"), in: struct { FieldName int `json:"'hello,string"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, string: true}, wantErr: errors.New("Go struct field FieldName has malformed `json` tag: single-quoted string not terminated: 'hello,str..."), }, { name: jsontest.Name("MalformedQuotedString/MissingComma"), in: struct { FieldName int `json:"'hello'inline,string"` }{}, wantOpts: fieldOptions{hasName: true, name: "hello", quotedName: `"hello"`, inline: true, string: true}, wantErr: errors.New("Go struct field FieldName has malformed `json` tag: invalid character 'i' before next option (expecting ',')"), }, { name: jsontest.Name("MalformedQuotedString/InvalidEscape"), in: struct { FieldName int `json:"'hello\\u####',inline,string"` }{}, wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true, string: true}, wantErr: errors.New("Go struct field FieldName has malformed `json` tag: invalid single-quoted string: 'hello\\u####'"), }, { name: jsontest.Name("MisnamedTag"), in: struct { V int `jsom:"Misnamed"` }{}, wantOpts: fieldOptions{name: "V", quotedName: `"V"`}, }} for _, tt := range tests { t.Run(tt.name.Name, func(t *testing.T) { fs := reflect.TypeOf(tt.in).Field(0) gotOpts, gotIgnored, gotErr := parseFieldOptions(fs) if !reflect.DeepEqual(gotOpts, tt.wantOpts) || gotIgnored != tt.wantIgnored || !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("%s: parseFieldOptions(%T) = (\n\t%v,\n\t%v,\n\t%v\n), want (\n\t%v,\n\t%v,\n\t%v\n)", tt.name.Where, tt.in, gotOpts, gotIgnored, gotErr, tt.wantOpts, tt.wantIgnored, tt.wantErr) } }) } }