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:",inline"`
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:",inline"`
61 L2C string
62 } `json:",inline"`
63 L1C string
64 L1D struct {
65 L2D string
66 L2E struct {
67 L3B string
68 } `json:",inline"`
69 L2F string
70 } `json:",inline"`
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:",inline"`
95 } `json:",inline"`
96 X2 struct {
97 X struct {
98 B string
99 C string
100 D string
101 } `json:",inline"`
102 } `json:",inline"`
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:",inline"`
119 X2 struct {
120 A string `json:"A"`
121 } `json:",inline"`
122 X3 struct {
123 A string
124 } `json:",inline"`
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:",inline"`
137 X2 struct {
138 A string `json:"A"`
139 } `json:",inline"`
140 X3 struct {
141 A string `json:"A"`
142 } `json:",inline"`
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:",inline"`
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:",inline"`
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("InlinedFallback/Cancelation"),
181 in: struct {
182 X1 struct {
183 X jsontext.Value `json:",inline"`
184 } `json:",inline"`
185 X2 struct {
186 X map[string]any `json:",unknown"`
187 } `json:",inline"`
188 }{},
189 want: structFields{},
190 }, {
191 name: jsontest.Name("InlinedFallback/Precedence"),
192 in: struct {
193 X1 struct {
194 X jsontext.Value `json:",inline"`
195 } `json:",inline"`
196 X2 struct {
197 X map[string]any `json:",unknown"`
198 } `json:",inline"`
199 X map[string]jsontext.Value `json:",unknown"`
200 }{},
201 want: structFields{
202 inlinedFallback: &structField{id: 0, index: []int{2}, typ: T[map[string]jsontext.Value](), fieldOptions: fieldOptions{name: "X", quotedName: `"X"`, unknown: true}},
203 },
204 }, {
205 name: jsontest.Name("InlinedFallback/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("InvalidUTF8"),
217 in: struct {
218 Name string `json:"'\\xde\\xad\\xbe\\xef'"`
219 }{},
220 want: structFields{
221 flattened: []structField{
222 {id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{hasName: true, name: "\u07ad\ufffd\ufffd", quotedName: "\"\u07ad\ufffd\ufffd\"", nameNeedEscape: true}},
223 },
224 },
225 wantErr: errors.New(`Go struct field Name has JSON object name "ޭ\xbe\xef" with invalid UTF-8`),
226 }, {
227 name: jsontest.Name("DuplicateName"),
228 in: struct {
229 A string `json:"same"`
230 B string `json:"same"`
231 }{},
232 want: structFields{flattened: []structField{}},
233 wantErr: errors.New(`Go struct fields A and B conflict over JSON object name "same"`),
234 }, {
235 name: jsontest.Name("BothInlineAndUnknown"),
236 in: struct {
237 A struct{} `json:",inline,unknown"`
238 }{},
239 wantErr: errors.New("Go struct field A cannot have both `inline` and `unknown` specified"),
240 }, {
241 name: jsontest.Name("InlineWithOptions"),
242 in: struct {
243 A struct{} `json:",inline,omitempty"`
244 }{},
245 wantErr: errors.New("Go struct field A cannot have any options other than `inline` or `unknown` specified"),
246 }, {
247 name: jsontest.Name("UnknownWithOptions"),
248 in: struct {
249 A map[string]any `json:",inline,omitempty"`
250 }{},
251 want: structFields{inlinedFallback: &structField{
252 index: []int{0},
253 typ: reflect.TypeFor[map[string]any](),
254 fieldOptions: fieldOptions{
255 name: "A",
256 quotedName: `"A"`,
257 inline: true,
258 },
259 }},
260 wantErr: errors.New("Go struct field A cannot have any options other than `inline` or `unknown` specified"),
261 }, {
262 name: jsontest.Name("InlineTextMarshaler"),
263 in: struct {
264 A struct{ encoding.TextMarshaler } `json:",inline"`
265 }{},
266 want: structFields{flattened: []structField{{
267 index: []int{0, 0},
268 typ: reflect.TypeFor[encoding.TextMarshaler](),
269 fieldOptions: fieldOptions{
270 name: "TextMarshaler",
271 quotedName: `"TextMarshaler"`,
272 },
273 }}},
274 wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextMarshaler } must not implement marshal or unmarshal methods`),
275 }, {
276 name: jsontest.Name("InlineTextAppender"),
277 in: struct {
278 A struct{ encoding.TextAppender } `json:",inline"`
279 }{},
280 want: structFields{flattened: []structField{{
281 index: []int{0, 0},
282 typ: reflect.TypeFor[encoding.TextAppender](),
283 fieldOptions: fieldOptions{
284 name: "TextAppender",
285 quotedName: `"TextAppender"`,
286 },
287 }}},
288 wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextAppender } must not implement marshal or unmarshal methods`),
289 }, {
290 name: jsontest.Name("UnknownJSONMarshaler"),
291 in: struct {
292 A struct{ Marshaler } `json:",unknown"`
293 }{},
294 wantErr: errors.New(`inlined Go struct field A of type struct { json.Marshaler } must not implement marshal or unmarshal methods`),
295 }, {
296 name: jsontest.Name("InlineJSONMarshalerTo"),
297 in: struct {
298 A struct{ MarshalerTo } `json:",inline"`
299 }{},
300 want: structFields{flattened: []structField{{
301 index: []int{0, 0},
302 typ: reflect.TypeFor[MarshalerTo](),
303 fieldOptions: fieldOptions{
304 name: "MarshalerTo",
305 quotedName: `"MarshalerTo"`,
306 },
307 }}},
308 wantErr: errors.New(`inlined Go struct field A of type struct { json.MarshalerTo } must not implement marshal or unmarshal methods`),
309 }, {
310 name: jsontest.Name("UnknownTextUnmarshaler"),
311 in: struct {
312 A *struct{ encoding.TextUnmarshaler } `json:",unknown"`
313 }{},
314 wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextUnmarshaler } must not implement marshal or unmarshal methods`),
315 }, {
316 name: jsontest.Name("InlineJSONUnmarshaler"),
317 in: struct {
318 A *struct{ Unmarshaler } `json:",inline"`
319 }{},
320 want: structFields{flattened: []structField{{
321 index: []int{0, 0},
322 typ: reflect.TypeFor[Unmarshaler](),
323 fieldOptions: fieldOptions{
324 name: "Unmarshaler",
325 quotedName: `"Unmarshaler"`,
326 },
327 }}},
328 wantErr: errors.New(`inlined Go struct field A of type struct { json.Unmarshaler } must not implement marshal or unmarshal methods`),
329 }, {
330 name: jsontest.Name("UnknownJSONUnmarshalerFrom"),
331 in: struct {
332 A struct{ UnmarshalerFrom } `json:",unknown"`
333 }{},
334 wantErr: errors.New(`inlined Go struct field A of type struct { json.UnmarshalerFrom } must not implement marshal or unmarshal methods`),
335 }, {
336 name: jsontest.Name("UnknownStruct"),
337 in: struct {
338 A struct {
339 X, Y, Z string
340 } `json:",unknown"`
341 }{},
342 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"),
343 }, {
344 name: jsontest.Name("InlineUnsupported/MapIntKey"),
345 in: struct {
346 A map[int]any `json:",unknown"`
347 }{},
348 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`),
349 }, {
350 name: jsontest.Name("InlineUnsupported/MapTextMarshalerStringKey"),
351 in: struct {
352 A map[nocaseString]any `json:",inline"`
353 }{},
354 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`),
355 }, {
356 name: jsontest.Name("InlineUnsupported/MapMarshalerStringKey"),
357 in: struct {
358 A map[stringMarshalEmpty]any `json:",inline"`
359 }{},
360 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`),
361 }, {
362 name: jsontest.Name("InlineUnsupported/DoublePointer"),
363 in: struct {
364 A **struct{} `json:",inline"`
365 }{},
366 wantErr: errors.New(`inlined Go struct field A of type *struct {} must be a Go struct, Go map of string key, or jsontext.Value`),
367 }, {
368 name: jsontest.Name("DuplicateInline"),
369 in: struct {
370 A map[string]any `json:",inline"`
371 B jsontext.Value `json:",inline"`
372 }{},
373 wantErr: errors.New(`inlined Go struct fields A and B cannot both be a Go map or jsontext.Value`),
374 }, {
375 name: jsontest.Name("DuplicateEmbedInline"),
376 in: struct {
377 A MapStringAny `json:",inline"`
378 B jsontext.Value `json:",inline"`
379 }{},
380 wantErr: errors.New(`inlined Go struct fields A and B cannot both be a Go map or jsontext.Value`),
381 }}
382
383 for _, tt := range tests {
384 t.Run(tt.name.Name, func(t *testing.T) {
385 got, err := makeStructFields(reflect.TypeOf(tt.in))
386
387
388 pointers := make(map[*structField]bool)
389 for i := range got.flattened {
390 pointers[&got.flattened[i]] = true
391 }
392 for _, f := range got.byActualName {
393 if !pointers[f] {
394 t.Errorf("%s: byActualName pointer not in flattened", tt.name.Where)
395 }
396 }
397 for _, fs := range got.byFoldedName {
398 for _, f := range fs {
399 if !pointers[f] {
400 t.Errorf("%s: byFoldedName pointer not in flattened", tt.name.Where)
401 }
402 }
403 }
404
405
406 for i := range got.flattened {
407 got.flattened[i].fncs = nil
408 got.flattened[i].isEmpty = nil
409 }
410 if got.inlinedFallback != nil {
411 got.inlinedFallback.fncs = nil
412 got.inlinedFallback.isEmpty = nil
413 }
414
415
416 tt.want.byActualName = make(map[string]*structField)
417 for i := range tt.want.flattened {
418 f := &tt.want.flattened[i]
419 tt.want.byActualName[f.name] = f
420 }
421 tt.want.byFoldedName = make(map[string][]*structField)
422 for i, f := range tt.want.flattened {
423 foldedName := string(foldName([]byte(f.name)))
424 tt.want.byFoldedName[foldedName] = append(tt.want.byFoldedName[foldedName], &tt.want.flattened[i])
425 }
426
427
428 var gotErr error
429 if err != nil {
430 gotErr = err.Err
431 }
432
433 tt.want.reindex()
434 if !reflect.DeepEqual(got, tt.want) || !reflect.DeepEqual(gotErr, tt.wantErr) {
435 t.Errorf("%s: makeStructFields(%T):\n\tgot (%v, %v)\n\twant (%v, %v)", tt.name.Where, tt.in, got, gotErr, tt.want, tt.wantErr)
436 }
437 })
438 }
439 }
440
441 func TestParseTagOptions(t *testing.T) {
442 tests := []struct {
443 name jsontest.CaseName
444 in any
445 wantOpts fieldOptions
446 wantIgnored bool
447 wantErr error
448 }{{
449 name: jsontest.Name("GoName"),
450 in: struct {
451 FieldName int
452 }{},
453 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
454 }, {
455 name: jsontest.Name("GoNameWithOptions"),
456 in: struct {
457 FieldName int `json:",inline"`
458 }{},
459 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true},
460 }, {
461 name: jsontest.Name("Empty"),
462 in: struct {
463 V int `json:""`
464 }{},
465 wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
466 }, {
467 name: jsontest.Name("Unexported"),
468 in: struct {
469 v int `json:"Hello"`
470 }{},
471 wantIgnored: true,
472 wantErr: errors.New("unexported Go struct field v cannot have non-ignored `json:\"Hello\"` tag"),
473 }, {
474 name: jsontest.Name("UnexportedEmpty"),
475 in: struct {
476 v int `json:""`
477 }{},
478 wantIgnored: true,
479 wantErr: errors.New("unexported Go struct field v cannot have non-ignored `json:\"\"` tag"),
480 }, {
481 name: jsontest.Name("EmbedUnexported"),
482 in: struct {
483 unexported
484 }{},
485 wantOpts: fieldOptions{name: "unexported", quotedName: `"unexported"`},
486 }, {
487 name: jsontest.Name("Ignored"),
488 in: struct {
489 V int `json:"-"`
490 }{},
491 wantIgnored: true,
492 }, {
493 name: jsontest.Name("IgnoredEmbedUnexported"),
494 in: struct {
495 unexported `json:"-"`
496 }{},
497 wantIgnored: true,
498 }, {
499 name: jsontest.Name("DashComma"),
500 in: struct {
501 V int `json:"-,"`
502 }{},
503 wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`},
504 wantErr: errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
505 }, {
506 name: jsontest.Name("DashCommaOmitEmpty"),
507 in: struct {
508 V int `json:"-,omitempty"`
509 }{},
510 wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`, omitempty: true},
511 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"),
512 }, {
513 name: jsontest.Name("QuotedDashCommaOmitEmpty"),
514 in: struct {
515 V int `json:"'-',omitempty"`
516 }{},
517 wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`, omitempty: true},
518 }, {
519 name: jsontest.Name("QuotedDashName"),
520 in: struct {
521 V int `json:"'-'"`
522 }{},
523 wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`},
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("QuotedLatinPunctuationName"),
532 in: struct {
533 V int `json:"'$%-/'"`
534 }{},
535 wantOpts: fieldOptions{hasName: true, name: "$%-/", quotedName: `"$%-/"`},
536 }, {
537 name: jsontest.Name("LatinDigitsName"),
538 in: struct {
539 V int `json:"0123456789"`
540 }{},
541 wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`},
542 }, {
543 name: jsontest.Name("QuotedLatinDigitsName"),
544 in: struct {
545 V int `json:"'0123456789'"`
546 }{},
547 wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`},
548 }, {
549 name: jsontest.Name("LatinUppercaseName"),
550 in: struct {
551 V int `json:"ABCDEFGHIJKLMOPQRSTUVWXYZ"`
552 }{},
553 wantOpts: fieldOptions{hasName: true, name: "ABCDEFGHIJKLMOPQRSTUVWXYZ", quotedName: `"ABCDEFGHIJKLMOPQRSTUVWXYZ"`},
554 }, {
555 name: jsontest.Name("LatinLowercaseName"),
556 in: struct {
557 V int `json:"abcdefghijklmnopqrstuvwxyz_"`
558 }{},
559 wantOpts: fieldOptions{hasName: true, name: "abcdefghijklmnopqrstuvwxyz_", quotedName: `"abcdefghijklmnopqrstuvwxyz_"`},
560 }, {
561 name: jsontest.Name("GreekName"),
562 in: struct {
563 V string `json:"Ελλάδα"`
564 }{},
565 wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`},
566 }, {
567 name: jsontest.Name("QuotedGreekName"),
568 in: struct {
569 V string `json:"'Ελλάδα'"`
570 }{},
571 wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`},
572 }, {
573 name: jsontest.Name("ChineseName"),
574 in: struct {
575 V string `json:"世界"`
576 }{},
577 wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`},
578 }, {
579 name: jsontest.Name("QuotedChineseName"),
580 in: struct {
581 V string `json:"'世界'"`
582 }{},
583 wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`},
584 }, {
585 name: jsontest.Name("PercentSlashName"),
586 in: struct {
587 V int `json:"text/html%"`
588 }{},
589 wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`},
590 }, {
591 name: jsontest.Name("QuotedPercentSlashName"),
592 in: struct {
593 V int `json:"'text/html%'"`
594 }{},
595 wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`},
596 }, {
597 name: jsontest.Name("PunctuationName"),
598 in: struct {
599 V string `json:"!#$%&()*+-./:;<=>?@[]^_{|}~ "`
600 }{},
601 wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`, nameNeedEscape: true},
602 }, {
603 name: jsontest.Name("QuotedPunctuationName"),
604 in: struct {
605 V string `json:"'!#$%&()*+-./:;<=>?@[]^_{|}~ '"`
606 }{},
607 wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`, nameNeedEscape: true},
608 }, {
609 name: jsontest.Name("EmptyName"),
610 in: struct {
611 V int `json:"''"`
612 }{},
613 wantOpts: fieldOptions{hasName: true, name: "", quotedName: `""`},
614 }, {
615 name: jsontest.Name("SpaceName"),
616 in: struct {
617 V int `json:"' '"`
618 }{},
619 wantOpts: fieldOptions{hasName: true, name: " ", quotedName: `" "`},
620 }, {
621 name: jsontest.Name("CommaQuotes"),
622 in: struct {
623 V int `json:"',\\'\"\\\"'"`
624 }{},
625 wantOpts: fieldOptions{hasName: true, name: `,'""`, quotedName: `",'\"\""`, nameNeedEscape: true},
626 }, {
627 name: jsontest.Name("SingleComma"),
628 in: struct {
629 V int `json:","`
630 }{},
631 wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
632 wantErr: errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
633 }, {
634 name: jsontest.Name("SuperfluousCommas"),
635 in: struct {
636 V int `json:",,,,\"\",,inline,unknown,,,,"`
637 }{},
638 wantOpts: fieldOptions{name: "V", quotedName: `"V"`, inline: true, unknown: true},
639 wantErr: errors.New("Go struct field V has malformed `json` tag: invalid character ',' at start of option (expecting Unicode letter or single quote)"),
640 }, {
641 name: jsontest.Name("CaseAloneOption"),
642 in: struct {
643 FieldName int `json:",case"`
644 }{},
645 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
646 wantErr: errors.New("Go struct field FieldName is missing value for `case` tag option; specify `case:ignore` or `case:strict` instead"),
647 }, {
648 name: jsontest.Name("CaseIgnoreOption"),
649 in: struct {
650 FieldName int `json:",case:ignore"`
651 }{},
652 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore},
653 }, {
654 name: jsontest.Name("CaseStrictOption"),
655 in: struct {
656 FieldName int `json:",case:strict"`
657 }{},
658 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseStrict},
659 }, {
660 name: jsontest.Name("CaseUnknownOption"),
661 in: struct {
662 FieldName int `json:",case:unknown"`
663 }{},
664 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
665 wantErr: errors.New("Go struct field FieldName has unknown `case:unknown` tag value"),
666 }, {
667 name: jsontest.Name("CaseQuotedOption"),
668 in: struct {
669 FieldName int `json:",case:'ignore'"`
670 }{},
671 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore},
672 wantErr: errors.New("Go struct field FieldName has unnecessarily quoted appearance of `case:'ignore'` tag option; specify `case:ignore` instead"),
673 }, {
674 name: jsontest.Name("BothCaseOptions"),
675 in: struct {
676 FieldName int `json:",case:ignore,case:strict"`
677 }{},
678 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore | caseStrict},
679 wantErr: errors.New("Go struct field FieldName cannot have both `case:ignore` and `case:strict` tag options"),
680 }, {
681 name: jsontest.Name("InlineOption"),
682 in: struct {
683 FieldName int `json:",inline"`
684 }{},
685 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true},
686 }, {
687 name: jsontest.Name("UnknownOption"),
688 in: struct {
689 FieldName int `json:",unknown"`
690 }{},
691 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, unknown: true},
692 }, {
693 name: jsontest.Name("OmitZeroOption"),
694 in: struct {
695 FieldName int `json:",omitzero"`
696 }{},
697 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitzero: true},
698 }, {
699 name: jsontest.Name("OmitEmptyOption"),
700 in: struct {
701 FieldName int `json:",omitempty"`
702 }{},
703 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitempty: true},
704 }, {
705 name: jsontest.Name("StringOption"),
706 in: struct {
707 FieldName int `json:",string"`
708 }{},
709 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, string: true},
710 }, {
711 name: jsontest.Name("FormatOptionEqual"),
712 in: struct {
713 FieldName int `json:",format=fizzbuzz"`
714 }{},
715 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
716 wantErr: errors.New("Go struct field FieldName is missing value for `format` tag option"),
717 }, {
718 name: jsontest.Name("FormatOptionColon"),
719 in: struct {
720 FieldName int `json:",format:fizzbuzz"`
721 }{},
722 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "fizzbuzz"},
723 }, {
724 name: jsontest.Name("FormatOptionQuoted"),
725 in: struct {
726 FieldName int `json:",format:'2006-01-02'"`
727 }{},
728 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "2006-01-02"},
729 }, {
730 name: jsontest.Name("FormatOptionInvalid"),
731 in: struct {
732 FieldName int `json:",format:'2006-01-02"`
733 }{},
734 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
735 wantErr: errors.New("Go struct field FieldName has malformed value for `format` tag option: single-quoted string not terminated: '2006-01-0..."),
736 }, {
737 name: jsontest.Name("FormatOptionNotLast"),
738 in: struct {
739 FieldName int `json:",format:alpha,ordered"`
740 }{},
741 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "alpha"},
742 wantErr: errors.New("Go struct field FieldName has `format` tag option that was not specified last"),
743 }, {
744 name: jsontest.Name("AllOptions"),
745 in: struct {
746 FieldName int `json:",case:ignore,inline,unknown,omitzero,omitempty,string,format:format"`
747 }{},
748 wantOpts: fieldOptions{
749 name: "FieldName",
750 quotedName: `"FieldName"`,
751 casing: caseIgnore,
752 inline: true,
753 unknown: true,
754 omitzero: true,
755 omitempty: true,
756 string: true,
757 format: "format",
758 },
759 }, {
760 name: jsontest.Name("AllOptionsQuoted"),
761 in: struct {
762 FieldName int `json:",'case':'ignore','inline','unknown','omitzero','omitempty','string','format':'format'"`
763 }{},
764 wantOpts: fieldOptions{
765 name: "FieldName",
766 quotedName: `"FieldName"`,
767 casing: caseIgnore,
768 inline: true,
769 unknown: true,
770 omitzero: true,
771 omitempty: true,
772 string: true,
773 format: "format",
774 },
775 wantErr: errors.New("Go struct field FieldName has unnecessarily quoted appearance of `'case'` tag option; specify `case` instead"),
776 }, {
777 name: jsontest.Name("AllOptionsCaseSensitive"),
778 in: struct {
779 FieldName int `json:",CASE:IGNORE,INLINE,UNKNOWN,OMITZERO,OMITEMPTY,STRING,FORMAT:FORMAT"`
780 }{},
781 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
782 wantErr: errors.New("Go struct field FieldName has invalid appearance of `CASE` tag option; specify `case` instead"),
783 }, {
784 name: jsontest.Name("AllOptionsSpaceSensitive"),
785 in: struct {
786 FieldName int `json:", case:ignore , inline , unknown , omitzero , omitempty , string , format:format "`
787 }{},
788 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
789 wantErr: errors.New("Go struct field FieldName has malformed `json` tag: invalid character ' ' at start of option (expecting Unicode letter or single quote)"),
790 }, {
791 name: jsontest.Name("UnknownTagOption"),
792 in: struct {
793 FieldName int `json:",inline,whoknows,string"`
794 }{},
795 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true, string: true},
796 }, {
797 name: jsontest.Name("MalformedQuotedString/MissingQuote"),
798 in: struct {
799 FieldName int `json:"'hello,string"`
800 }{},
801 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, string: true},
802 wantErr: errors.New("Go struct field FieldName has malformed `json` tag: single-quoted string not terminated: 'hello,str..."),
803 }, {
804 name: jsontest.Name("MalformedQuotedString/MissingComma"),
805 in: struct {
806 FieldName int `json:"'hello'inline,string"`
807 }{},
808 wantOpts: fieldOptions{hasName: true, name: "hello", quotedName: `"hello"`, inline: true, string: true},
809 wantErr: errors.New("Go struct field FieldName has malformed `json` tag: invalid character 'i' before next option (expecting ',')"),
810 }, {
811 name: jsontest.Name("MalformedQuotedString/InvalidEscape"),
812 in: struct {
813 FieldName int `json:"'hello\\u####',inline,string"`
814 }{},
815 wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true, string: true},
816 wantErr: errors.New("Go struct field FieldName has malformed `json` tag: invalid single-quoted string: 'hello\\u####'"),
817 }, {
818 name: jsontest.Name("MisnamedTag"),
819 in: struct {
820 V int `jsom:"Misnamed"`
821 }{},
822 wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
823 }}
824
825 for _, tt := range tests {
826 t.Run(tt.name.Name, func(t *testing.T) {
827 fs := reflect.TypeOf(tt.in).Field(0)
828 gotOpts, gotIgnored, gotErr := parseFieldOptions(fs)
829 if !reflect.DeepEqual(gotOpts, tt.wantOpts) || gotIgnored != tt.wantIgnored || !reflect.DeepEqual(gotErr, tt.wantErr) {
830 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)
831 }
832 })
833 }
834 }
835
View as plain text