1
2
3
4
5
6
7 package json
8
9 import (
10 "encoding"
11 "errors"
12 "reflect"
13 "testing"
14
15 "encoding/json/internal/jsontest"
16 "encoding/json/jsontext"
17 )
18
19 type unexported struct{}
20
21 func TestMakeStructFields(t *testing.T) {
22 type Embed struct {
23 Foo string
24 }
25 type Recursive struct {
26 A string
27 *Recursive `json:",embed"`
28 B string
29 }
30 type MapStringAny map[string]any
31 tests := []struct {
32 name jsontest.CaseName
33 in any
34 want structFields
35 wantErr error
36 }{{
37 name: jsontest.Name("Names"),
38 in: struct {
39 F1 string
40 F2 string `json:"-"`
41 F3 string `json:"json_name"`
42 f3 string
43 F5 string `json:"json_name_nocase,case:ignore"`
44 }{},
45 want: structFields{
46 flattened: []structField{
47 {id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{name: "F1", quotedName: `"F1"`}},
48 {id: 1, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "json_name", quotedName: `"json_name"`, hasName: true}},
49 {id: 2, index: []int{4}, typ: stringType, fieldOptions: fieldOptions{name: "json_name_nocase", quotedName: `"json_name_nocase"`, hasName: true, casing: caseIgnore}},
50 },
51 },
52 }, {
53 name: jsontest.Name("BreadthFirstSearch"),
54 in: struct {
55 L1A string
56 L1B struct {
57 L2A string
58 L2B struct {
59 L3A string
60 } `json:",embed"`
61 L2C string
62 } `json:",embed"`
63 L1C string
64 L1D struct {
65 L2D string
66 L2E struct {
67 L3B string
68 } `json:",embed"`
69 L2F string
70 } `json:",embed"`
71 L1E string
72 }{},
73 want: structFields{
74 flattened: []structField{
75 {id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{name: "L1A", quotedName: `"L1A"`}},
76 {id: 3, index: []int{1, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L2A", quotedName: `"L2A"`}},
77 {id: 7, index: []int{1, 1, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L3A", quotedName: `"L3A"`}},
78 {id: 4, index: []int{1, 2}, typ: stringType, fieldOptions: fieldOptions{name: "L2C", quotedName: `"L2C"`}},
79 {id: 1, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "L1C", quotedName: `"L1C"`}},
80 {id: 5, index: []int{3, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L2D", quotedName: `"L2D"`}},
81 {id: 8, index: []int{3, 1, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L3B", quotedName: `"L3B"`}},
82 {id: 6, index: []int{3, 2}, typ: stringType, fieldOptions: fieldOptions{name: "L2F", quotedName: `"L2F"`}},
83 {id: 2, index: []int{4}, typ: stringType, fieldOptions: fieldOptions{name: "L1E", quotedName: `"L1E"`}},
84 },
85 },
86 }, {
87 name: jsontest.Name("NameResolution"),
88 in: struct {
89 X1 struct {
90 X struct {
91 A string
92 B string
93 D string
94 } `json:",embed"`
95 } `json:",embed"`
96 X2 struct {
97 X struct {
98 B string
99 C string
100 D string
101 } `json:",embed"`
102 } `json:",embed"`
103 A string
104 D string
105 }{},
106 want: structFields{
107 flattened: []structField{
108 {id: 2, index: []int{1, 0, 1}, typ: stringType, fieldOptions: fieldOptions{name: "C", quotedName: `"C"`}},
109 {id: 0, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "A", quotedName: `"A"`}},
110 {id: 1, index: []int{3}, typ: stringType, fieldOptions: fieldOptions{name: "D", quotedName: `"D"`}},
111 },
112 },
113 }, {
114 name: jsontest.Name("NameResolution/ExplicitNameUniquePrecedence"),
115 in: struct {
116 X1 struct {
117 A string
118 } `json:",embed"`
119 X2 struct {
120 A string `json:"A"`
121 } `json:",embed"`
122 X3 struct {
123 A string
124 } `json:",embed"`
125 }{},
126 want: structFields{
127 flattened: []structField{
128 {id: 0, index: []int{1, 0}, typ: stringType, fieldOptions: fieldOptions{hasName: true, name: "A", quotedName: `"A"`}},
129 },
130 },
131 }, {
132 name: jsontest.Name("NameResolution/ExplicitNameCancelsOut"),
133 in: struct {
134 X1 struct {
135 A string
136 } `json:",embed"`
137 X2 struct {
138 A string `json:"A"`
139 } `json:",embed"`
140 X3 struct {
141 A string `json:"A"`
142 } `json:",embed"`
143 }{},
144 want: structFields{flattened: []structField{}},
145 }, {
146 name: jsontest.Name("Embed/Implicit"),
147 in: struct {
148 Embed
149 }{},
150 want: structFields{
151 flattened: []structField{
152 {id: 0, index: []int{0, 0}, typ: stringType, fieldOptions: fieldOptions{name: "Foo", quotedName: `"Foo"`}},
153 },
154 },
155 }, {
156 name: jsontest.Name("Embed/Explicit"),
157 in: struct {
158 Embed `json:",embed"`
159 }{},
160 want: structFields{
161 flattened: []structField{
162 {id: 0, index: []int{0, 0}, typ: stringType, fieldOptions: fieldOptions{name: "Foo", quotedName: `"Foo"`}},
163 },
164 },
165 }, {
166 name: jsontest.Name("Recursive"),
167 in: struct {
168 A string
169 Recursive `json:",embed"`
170 C string
171 }{},
172 want: structFields{
173 flattened: []structField{
174 {id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{name: "A", quotedName: `"A"`}},
175 {id: 2, index: []int{1, 2}, typ: stringType, fieldOptions: fieldOptions{name: "B", quotedName: `"B"`}},
176 {id: 1, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "C", quotedName: `"C"`}},
177 },
178 },
179 }, {
180 name: jsontest.Name("EmbeddedFallback/Cancelation"),
181 in: struct {
182 X1 struct {
183 X jsontext.Value `json:",embed"`
184 } `json:",embed"`
185 X2 struct {
186 X map[string]any `json:",embed"`
187 } `json:",embed"`
188 }{},
189 want: structFields{},
190 }, {
191 name: jsontest.Name("EmbeddedFallback/Precedence"),
192 in: struct {
193 X1 struct {
194 X jsontext.Value `json:",embed"`
195 } `json:",embed"`
196 X2 struct {
197 X map[string]any `json:",embed"`
198 } `json:",embed"`
199 X map[string]jsontext.Value `json:",embed"`
200 }{},
201 want: structFields{
202 embeddedFallback: &structField{id: 0, index: []int{2}, typ: T[map[string]jsontext.Value](), fieldOptions: fieldOptions{name: "X", quotedName: `"X"`, embed: true}},
203 },
204 }, {
205 name: jsontest.Name("EmbeddedFallback/InvalidImplicit"),
206 in: struct {
207 MapStringAny
208 }{},
209 want: structFields{
210 flattened: []structField{
211 {id: 0, index: []int{0}, typ: reflect.TypeOf(MapStringAny(nil)), fieldOptions: fieldOptions{name: "MapStringAny", quotedName: `"MapStringAny"`}},
212 },
213 },
214 wantErr: errors.New("embedded Go struct field MapStringAny of non-struct type must be explicitly given a JSON name"),
215 }, {
216 name: jsontest.Name("DuplicateName"),
217 in: struct {
218 A string `json:"same"`
219 B string `json:"same"`
220 }{},
221 want: structFields{flattened: []structField{}},
222 wantErr: errors.New(`Go struct fields A and B conflict over JSON object name "same"`),
223 }, {
224 name: jsontest.Name("EmbedWithOptions"),
225 in: struct {
226 A struct{} `json:",embed,omitempty"`
227 }{},
228 wantErr: errors.New("Go struct field A cannot have any options other than `embed` specified"),
229 }, {
230 name: jsontest.Name("UnknownWithOptions"),
231 in: struct {
232 A map[string]any `json:",embed,omitempty"`
233 }{},
234 want: structFields{embeddedFallback: &structField{
235 index: []int{0},
236 typ: reflect.TypeFor[map[string]any](),
237 fieldOptions: fieldOptions{
238 name: "A",
239 quotedName: `"A"`,
240 embed: true,
241 },
242 }},
243 wantErr: errors.New("Go struct field A cannot have any options other than `embed` specified"),
244 }, {
245 name: jsontest.Name("EmbedTextMarshaler"),
246 in: struct {
247 A struct{ encoding.TextMarshaler } `json:",embed"`
248 }{},
249 want: structFields{flattened: []structField{{
250 index: []int{0, 0},
251 typ: reflect.TypeFor[encoding.TextMarshaler](),
252 fieldOptions: fieldOptions{
253 name: "TextMarshaler",
254 quotedName: `"TextMarshaler"`,
255 },
256 }}},
257 wantErr: errors.New(`embedded Go struct field A of type struct { encoding.TextMarshaler } must not implement marshal or unmarshal methods`),
258 }, {
259 name: jsontest.Name("EmbedTextAppender"),
260 in: struct {
261 A struct{ encoding.TextAppender } `json:",embed"`
262 }{},
263 want: structFields{flattened: []structField{{
264 index: []int{0, 0},
265 typ: reflect.TypeFor[encoding.TextAppender](),
266 fieldOptions: fieldOptions{
267 name: "TextAppender",
268 quotedName: `"TextAppender"`,
269 },
270 }}},
271 wantErr: errors.New(`embedded Go struct field A of type struct { encoding.TextAppender } must not implement marshal or unmarshal methods`),
272 }, {
273 name: jsontest.Name("EmbedJSONMarshaler"),
274 in: struct {
275 A struct{ Marshaler } `json:",embed"`
276 }{},
277 want: structFields{flattened: []structField{{
278 index: []int{0, 0},
279 typ: reflect.TypeFor[Marshaler](),
280 fieldOptions: fieldOptions{
281 name: "Marshaler",
282 quotedName: `"Marshaler"`,
283 },
284 }}},
285 wantErr: errors.New(`embedded Go struct field A of type struct { json.Marshaler } must not implement marshal or unmarshal methods`),
286 }, {
287 name: jsontest.Name("EmbedJSONMarshalerTo"),
288 in: struct {
289 A struct{ MarshalerTo } `json:",embed"`
290 }{},
291 want: structFields{flattened: []structField{{
292 index: []int{0, 0},
293 typ: reflect.TypeFor[MarshalerTo](),
294 fieldOptions: fieldOptions{
295 name: "MarshalerTo",
296 quotedName: `"MarshalerTo"`,
297 },
298 }}},
299 wantErr: errors.New(`embedded Go struct field A of type struct { json.MarshalerTo } must not implement marshal or unmarshal methods`),
300 }, {
301 name: jsontest.Name("EmbedTextUnmarshaler"),
302 in: struct {
303 A *struct{ encoding.TextUnmarshaler } `json:",embed"`
304 }{},
305 want: structFields{flattened: []structField{{
306 index: []int{0, 0},
307 typ: reflect.TypeFor[encoding.TextUnmarshaler](),
308 fieldOptions: fieldOptions{
309 name: "TextUnmarshaler",
310 quotedName: `"TextUnmarshaler"`,
311 },
312 }}},
313 wantErr: errors.New(`embedded Go struct field A of type struct { encoding.TextUnmarshaler } must not implement marshal or unmarshal methods`),
314 }, {
315 name: jsontest.Name("EmbedJSONUnmarshaler"),
316 in: struct {
317 A *struct{ Unmarshaler } `json:",embed"`
318 }{},
319 want: structFields{flattened: []structField{{
320 index: []int{0, 0},
321 typ: reflect.TypeFor[Unmarshaler](),
322 fieldOptions: fieldOptions{
323 name: "Unmarshaler",
324 quotedName: `"Unmarshaler"`,
325 },
326 }}},
327 wantErr: errors.New(`embedded Go struct field A of type struct { json.Unmarshaler } must not implement marshal or unmarshal methods`),
328 }, {
329 name: jsontest.Name("EmbedJSONUnmarshalerFrom"),
330 in: struct {
331 A struct{ UnmarshalerFrom } `json:",embed"`
332 }{},
333 want: structFields{flattened: []structField{{
334 index: []int{0, 0},
335 typ: reflect.TypeFor[UnmarshalerFrom](),
336 fieldOptions: fieldOptions{
337 name: "UnmarshalerFrom",
338 quotedName: `"UnmarshalerFrom"`,
339 },
340 }}},
341 wantErr: errors.New(`embedded Go struct field A of type struct { json.UnmarshalerFrom } must not implement marshal or unmarshal methods`),
342 }, {
343 name: jsontest.Name("EmbedUnsupported/MapIntKey"),
344 in: struct {
345 A map[int]any `json:",embed"`
346 }{},
347 want: structFields{flattened: []structField{{
348 index: []int{0},
349 typ: reflect.TypeFor[map[int]any](),
350 fieldOptions: fieldOptions{name: "A", quotedName: `"A"`, embed: true},
351 }}},
352 wantErr: errors.New(`embedded Go struct field A of type map[int]interface {} must be a Go struct, Go map of string key, or jsontext.Value`),
353 }, {
354 name: jsontest.Name("EmbedUnsupported/MapTextMarshalerStringKey"),
355 in: struct {
356 A map[nocaseString]any `json:",embed"`
357 }{},
358 want: structFields{flattened: []structField{{
359 index: []int{0},
360 typ: reflect.TypeFor[map[nocaseString]any](),
361 fieldOptions: fieldOptions{name: "A", quotedName: `"A"`, embed: true},
362 }}},
363 wantErr: errors.New(`embedded map field A of type map[json.nocaseString]interface {} must have a string key that does not implement marshal or unmarshal methods`),
364 }, {
365 name: jsontest.Name("EmbedUnsupported/MapMarshalerStringKey"),
366 in: struct {
367 A map[stringMarshalEmpty]any `json:",embed"`
368 }{},
369 want: structFields{flattened: []structField{{
370 index: []int{0},
371 typ: reflect.TypeFor[map[stringMarshalEmpty]any](),
372 fieldOptions: fieldOptions{name: "A", quotedName: `"A"`, embed: true},
373 }}},
374 wantErr: errors.New(`embedded map field A of type map[json.stringMarshalEmpty]interface {} must have a string key that does not implement marshal or unmarshal methods`),
375 }, {
376 name: jsontest.Name("EmbedUnsupported/DoublePointer"),
377 in: struct {
378 A **struct{} `json:",embed"`
379 }{},
380 want: structFields{flattened: []structField{{
381 index: []int{0},
382 typ: reflect.TypeFor[**struct{}](),
383 fieldOptions: fieldOptions{name: "A", quotedName: `"A"`, embed: true},
384 }}},
385 wantErr: errors.New(`embedded Go struct field A of type *struct {} must be a Go struct, Go map of string key, or jsontext.Value`),
386 }, {
387 name: jsontest.Name("DuplicateEmbed"),
388 in: struct {
389 A map[string]any `json:",embed"`
390 B jsontext.Value `json:",embed"`
391 }{},
392 wantErr: errors.New(`embedded Go struct fields A and B cannot both be a Go map or jsontext.Value`),
393 }, {
394 name: jsontest.Name("DuplicateEmbedEmbed"),
395 in: struct {
396 A MapStringAny `json:",embed"`
397 B jsontext.Value `json:",embed"`
398 }{},
399 wantErr: errors.New(`embedded Go struct fields A and B cannot both be a Go map or jsontext.Value`),
400 }}
401
402 for _, tt := range tests {
403 t.Run(tt.name.Name, func(t *testing.T) {
404 got, err := makeStructFields(reflect.TypeOf(tt.in))
405
406
407 pointers := make(map[*structField]bool)
408 for i := range got.flattened {
409 pointers[&got.flattened[i]] = true
410 }
411 for _, f := range got.byActualName {
412 if !pointers[f] {
413 t.Errorf("%s: byActualName pointer not in flattened", tt.name.Where)
414 }
415 }
416 for _, fs := range got.byFoldedName {
417 for _, f := range fs {
418 if !pointers[f] {
419 t.Errorf("%s: byFoldedName pointer not in flattened", tt.name.Where)
420 }
421 }
422 }
423
424
425 for i := range got.flattened {
426 got.flattened[i].fncs = nil
427 got.flattened[i].isEmpty = nil
428 }
429 if got.embeddedFallback != nil {
430 got.embeddedFallback.fncs = nil
431 got.embeddedFallback.isEmpty = nil
432 }
433
434
435 tt.want.byActualName = make(map[string]*structField)
436 for i := range tt.want.flattened {
437 f := &tt.want.flattened[i]
438 tt.want.byActualName[f.name] = f
439 }
440 tt.want.byFoldedName = make(map[string][]*structField)
441 for i, f := range tt.want.flattened {
442 foldedName := string(foldName([]byte(f.name)))
443 tt.want.byFoldedName[foldedName] = append(tt.want.byFoldedName[foldedName], &tt.want.flattened[i])
444 }
445
446
447 var gotErr error
448 if err != nil {
449 gotErr = err.Err
450 }
451
452 tt.want.reindex()
453 if !reflect.DeepEqual(got, tt.want) || !reflect.DeepEqual(gotErr, tt.wantErr) {
454 t.Errorf("%s: makeStructFields(%T):\n\tgot (%v, %v)\n\twant (%v, %v)", tt.name.Where, tt.in, got, gotErr, tt.want, tt.wantErr)
455 }
456 })
457 }
458 }
459
460 func TestParseTagOptions(t *testing.T) {
461 tests := []struct {
462 name jsontest.CaseName
463 in any
464 wantOpts fieldOptions
465 wantIgnored bool
466 wantErr error
467 }{{
468 name: jsontest.Name("GoName"),
469 in: struct {
470 FieldName int
471 }{},
472 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
473 }, {
474 name: jsontest.Name("GoNameWithOptions"),
475 in: struct {
476 FieldName int `json:",embed"`
477 }{},
478 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, embed: true},
479 }, {
480 name: jsontest.Name("Empty"),
481 in: struct {
482 V int `json:""`
483 }{},
484 wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
485 }, {
486 name: jsontest.Name("Unexported"),
487 in: struct {
488 v int `json:"Hello"`
489 }{},
490 wantIgnored: true,
491 wantErr: errors.New("unexported Go struct field v cannot have non-ignored `json:\"Hello\"` tag"),
492 }, {
493 name: jsontest.Name("UnexportedEmpty"),
494 in: struct {
495 v int `json:""`
496 }{},
497 wantIgnored: true,
498 wantErr: errors.New("unexported Go struct field v cannot have non-ignored `json:\"\"` tag"),
499 }, {
500 name: jsontest.Name("EmbedUnexported"),
501 in: struct {
502 unexported
503 }{},
504 wantOpts: fieldOptions{name: "unexported", quotedName: `"unexported"`},
505 }, {
506 name: jsontest.Name("Ignored"),
507 in: struct {
508 V int `json:"-"`
509 }{},
510 wantIgnored: true,
511 }, {
512 name: jsontest.Name("IgnoredEmbedUnexported"),
513 in: struct {
514 unexported `json:"-"`
515 }{},
516 wantIgnored: true,
517 }, {
518 name: jsontest.Name("DashComma"),
519 in: struct {
520 V int `json:"-,"`
521 }{},
522 wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`},
523 wantErr: errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
524 }, {
525 name: jsontest.Name("LatinPunctuationName"),
526 in: struct {
527 V int `json:"$%-/"`
528 }{},
529 wantOpts: fieldOptions{hasName: true, name: "$%-/", quotedName: `"$%-/"`},
530 }, {
531 name: jsontest.Name("LatinDigitsName"),
532 in: struct {
533 V int `json:"0123456789"`
534 }{},
535 wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`},
536 }, {
537 name: jsontest.Name("LatinUppercaseName"),
538 in: struct {
539 V int `json:"ABCDEFGHIJKLMOPQRSTUVWXYZ"`
540 }{},
541 wantOpts: fieldOptions{hasName: true, name: "ABCDEFGHIJKLMOPQRSTUVWXYZ", quotedName: `"ABCDEFGHIJKLMOPQRSTUVWXYZ"`},
542 }, {
543 name: jsontest.Name("LatinLowercaseName"),
544 in: struct {
545 V int `json:"abcdefghijklmnopqrstuvwxyz_"`
546 }{},
547 wantOpts: fieldOptions{hasName: true, name: "abcdefghijklmnopqrstuvwxyz_", quotedName: `"abcdefghijklmnopqrstuvwxyz_"`},
548 }, {
549 name: jsontest.Name("GreekName"),
550 in: struct {
551 V string `json:"Ελλάδα"`
552 }{},
553 wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`},
554 }, {
555 name: jsontest.Name("ChineseName"),
556 in: struct {
557 V string `json:"世界"`
558 }{},
559 wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`},
560 }, {
561 name: jsontest.Name("PercentSlashName"),
562 in: struct {
563 V int `json:"text/html%"`
564 }{},
565 wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`},
566 }, {
567 name: jsontest.Name("PunctuationName"),
568 in: struct {
569 V string `json:"!#$%&()*+-./:;<=>?@[]^_{|}~ "`
570 }{},
571 wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`, nameNeedEscape: true},
572 }, {
573 name: jsontest.Name("SingleComma"),
574 in: struct {
575 V int `json:","`
576 }{},
577 wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
578 wantErr: errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
579 }, {
580 name: jsontest.Name("SuperfluousCommas"),
581 in: struct {
582 V int `json:",,,,\"\",,embed,,,,,"`
583 }{},
584 wantOpts: fieldOptions{name: "V", quotedName: `"V"`, embed: true},
585 wantErr: errors.New("Go struct field V has malformed `json` tag: invalid character ',' at start of option (expecting Unicode letter)"),
586 }, {
587 name: jsontest.Name("CaseAloneOption"),
588 in: struct {
589 FieldName int `json:",case"`
590 }{},
591 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
592 wantErr: errors.New("Go struct field FieldName is missing value for `case` tag option; specify `case:ignore` or `case:strict` instead"),
593 }, {
594 name: jsontest.Name("CaseIgnoreOption"),
595 in: struct {
596 FieldName int `json:",case:ignore"`
597 }{},
598 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore},
599 }, {
600 name: jsontest.Name("CaseStrictOption"),
601 in: struct {
602 FieldName int `json:",case:strict"`
603 }{},
604 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseStrict},
605 }, {
606 name: jsontest.Name("CaseUnknownOption"),
607 in: struct {
608 FieldName int `json:",case:unknown"`
609 }{},
610 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
611 wantErr: errors.New("Go struct field FieldName has unknown `case:unknown` tag value"),
612 }, {
613 name: jsontest.Name("CaseQuotedOption"),
614 in: struct {
615 FieldName int `json:",case:'ignore'"`
616 }{},
617 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
618 wantErr: errors.New("Go struct field FieldName has malformed value for `case` tag option: invalid character '\\'' at start of option (expecting Unicode letter)"),
619 }, {
620 name: jsontest.Name("BothCaseOptions"),
621 in: struct {
622 FieldName int `json:",case:ignore,case:strict"`
623 }{},
624 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore | caseStrict},
625 wantErr: errors.New("Go struct field FieldName cannot have both `case:ignore` and `case:strict` tag options"),
626 }, {
627 name: jsontest.Name("EmbedOption"),
628 in: struct {
629 FieldName int `json:",embed"`
630 }{},
631 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, embed: true},
632 }, {
633 name: jsontest.Name("OmitZeroOption"),
634 in: struct {
635 FieldName int `json:",omitzero"`
636 }{},
637 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitzero: true},
638 }, {
639 name: jsontest.Name("OmitEmptyOption"),
640 in: struct {
641 FieldName int `json:",omitempty"`
642 }{},
643 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitempty: true},
644 }, {
645 name: jsontest.Name("StringOption"),
646 in: struct {
647 FieldName int `json:",string"`
648 }{},
649 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, string: true},
650 }, {
651 name: jsontest.Name("FormatOptionEqual"),
652 in: struct {
653 FieldName int `json:",format=fizzbuzz"`
654 }{},
655 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
656 wantErr: errors.New("Go struct field FieldName is missing value for `format` tag option"),
657 }, {
658 name: jsontest.Name("FormatOptionColon"),
659 in: struct {
660 FieldName int `json:",format:fizzbuzz"`
661 }{},
662 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "fizzbuzz"},
663 }, {
664 name: jsontest.Name("FormatOptionQuoted"),
665 in: struct {
666 FieldName int `json:",format:'2006-01-02'"`
667 }{},
668 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "2006-01-02"},
669 }, {
670 name: jsontest.Name("FormatOptionInvalid"),
671 in: struct {
672 FieldName int `json:",format:'2006-01-02"`
673 }{},
674 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
675 wantErr: errors.New("Go struct field FieldName has malformed value for `format` tag option: single-quoted string not terminated: '2006-01-0..."),
676 }, {
677 name: jsontest.Name("FormatOptionNotLast"),
678 in: struct {
679 FieldName int `json:",format:alpha,ordered"`
680 }{},
681 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "alpha"},
682 wantErr: errors.New("Go struct field FieldName has `format` tag option that was not specified last"),
683 }, {
684 name: jsontest.Name("AllOptions"),
685 in: struct {
686 FieldName int `json:",case:ignore,embed,omitzero,omitempty,string,format:format"`
687 }{},
688 wantOpts: fieldOptions{
689 name: "FieldName",
690 quotedName: `"FieldName"`,
691 casing: caseIgnore,
692 embed: true,
693 omitzero: true,
694 omitempty: true,
695 string: true,
696 format: "format",
697 },
698 }, {
699 name: jsontest.Name("AllOptionsCaseSensitive"),
700 in: struct {
701 FieldName int `json:",CASE:IGNORE,INLINE,UNKNOWN,OMITZERO,OMITEMPTY,STRING,FORMAT:FORMAT"`
702 }{},
703 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
704 wantErr: errors.New("Go struct field FieldName has invalid appearance of `CASE` tag option; specify `case` instead"),
705 }, {
706 name: jsontest.Name("AllOptionsSpaceSensitive"),
707 in: struct {
708 FieldName int `json:", case:ignore , embed , omitzero , omitempty , string , format:format "`
709 }{},
710 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
711 wantErr: errors.New("Go struct field FieldName has malformed `json` tag: invalid character ' ' at start of option (expecting Unicode letter)"),
712 }, {
713 name: jsontest.Name("UnknownTagOption"),
714 in: struct {
715 FieldName int `json:",embed,whoknows,string"`
716 }{},
717 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, embed: true, string: true},
718 }, {
719 name: jsontest.Name("MisnamedTag"),
720 in: struct {
721 V int `jsom:"Misnamed"`
722 }{},
723 wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
724 }}
725
726 for _, tt := range tests {
727 t.Run(tt.name.Name, func(t *testing.T) {
728 fs := reflect.TypeOf(tt.in).Field(0)
729 gotOpts, gotIgnored, gotErr := parseFieldOptions(fs)
730 if !reflect.DeepEqual(gotOpts, tt.wantOpts) || gotIgnored != tt.wantIgnored || !reflect.DeepEqual(gotErr, tt.wantErr) {
731 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)
732 }
733 })
734 }
735 }
736
View as plain text