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