blob: ae58182f298ab4d05b7758a3533d7bfbb4d74033 [file] [log] [blame] [edit]
// 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("DashCommaOmitEmpty"),
in: struct {
V int `json:"-,omitempty"`
}{},
wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`, omitempty: true},
wantErr: errors.New("Go struct field V has JSON object name \"-\"; either use `json:\"-\"` to ignore the field or use `json:\"'-',omitempty\"` to specify \"-\" as the name"),
}, {
name: jsontest.Name("QuotedDashCommaOmitEmpty"),
in: struct {
V int `json:"'-',omitempty"`
}{},
wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`, omitempty: true},
}, {
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)
}
})
}
}