diff --git a/arshal_test.go b/arshal_test.go index 8379e22..df6acf9 100644 --- a/arshal_test.go +++ b/arshal_test.go @@ -158,10 +158,10 @@ type ( Quote string `json:"'\"'"` } structNoCase struct { - Aaa string `json:",strictcase"` + Aaa string `json:",case:strict"` AA_A string - AaA string `json:",nocase"` - AAa string `json:",nocase"` + AaA string `json:",case:ignore"` + AAa string `json:",case:ignore"` AAA string } structScalars struct { @@ -476,17 +476,17 @@ type ( B int `json:",omitzero"` } structNoCaseInlineTextValue struct { - AAA string `json:",omitempty,strictcase"` + AAA string `json:",omitempty,case:strict"` AA_b string `json:",omitempty"` - AaA string `json:",omitempty,nocase"` - AAa string `json:",omitempty,nocase"` + AaA string `json:",omitempty,case:ignore"` + AAa string `json:",omitempty,case:ignore"` Aaa string `json:",omitempty"` X jsontext.Value `json:",inline"` } structNoCaseInlineMapStringAny struct { AAA string `json:",omitempty"` - AaA string `json:",omitempty,nocase"` - AAa string `json:",omitempty,nocase"` + AaA string `json:",omitempty,case:ignore"` + AAa string `json:",omitempty,case:ignore"` Aaa string `json:",omitempty"` X jsonObject `json:",inline"` } diff --git a/doc.go b/doc.go index dfb4605..886e048 100644 --- a/doc.go +++ b/doc.go @@ -88,19 +88,15 @@ // This extra level of encoding is often necessary since // many JSON parsers cannot precisely represent 64-bit integers. // -// - nocase: When unmarshaling, the "nocase" option specifies that -// if the JSON object name does not exactly match the JSON name -// for any of the struct fields, then it attempts to match the struct field -// using a case-insensitive match that also ignores dashes and underscores. -// If multiple fields match, +// - case: When unmarshaling, the "case" option specifies how +// JSON object names are matched with the JSON name for Go struct fields. +// The option is a key-value pair specified as "case:value" where +// the value must either be 'ignore' or 'strict'. +// The 'ignore' value specifies that matching is case-insensitive +// where dashes and underscores are also ignored. If multiple fields match, // the first declared field in breadth-first order takes precedence. -// This takes precedence even if [MatchCaseInsensitiveNames] is set to false. -// This cannot be specified together with the "strictcase" option. -// -// - strictcase: When unmarshaling, the "strictcase" option specifies that the -// JSON object name must exactly match the JSON name for the struct field. -// This takes precedence even if [MatchCaseInsensitiveNames] is set to true. -// This cannot be specified together with the "nocase" option. +// The 'strict' value specifies that matching is case-sensitive. +// This takes precedence over the [MatchCaseInsensitiveNames] option. // // - inline: The "inline" option specifies that // the JSON representable content of this field type is to be promoted diff --git a/example_test.go b/example_test.go index 6dc71d2..570b875 100644 --- a/example_test.go +++ b/example_test.go @@ -74,7 +74,7 @@ func Example_fieldNames() { // A JSON name is provided without any special characters. JSONName any `json:"jsonName"` // No JSON name is not provided, so the Go field name is used. - Option any `json:",nocase"` + Option any `json:",case:ignore"` // An empty JSON name specified using an single-quoted string literal. Empty any `json:"''"` // A dash JSON name specified using an single-quoted string literal. @@ -108,8 +108,8 @@ func Example_fieldNames() { // Unmarshal matches JSON object names with Go struct fields using // a case-sensitive match, but can be configured to use a case-insensitive -// match with the "nocase" option. This permits unmarshaling from inputs that -// use naming conventions such as camelCase, snake_case, or kebab-case. +// match with the "case:ignore" option. This permits unmarshaling from inputs +// that use naming conventions such as camelCase, snake_case, or kebab-case. func Example_caseSensitivity() { // JSON input using various naming conventions. const input = `[ @@ -124,24 +124,24 @@ func Example_caseSensitivity() { {"unknown": true} ]` - // Without "nocase", Unmarshal looks for an exact match. - var withcase []struct { + // Without "case:ignore", Unmarshal looks for an exact match. + var caseStrict []struct { X bool `json:"firstName"` } - if err := json.Unmarshal([]byte(input), &withcase); err != nil { + if err := json.Unmarshal([]byte(input), &caseStrict); err != nil { log.Fatal(err) } - fmt.Println(withcase) // exactly 1 match found + fmt.Println(caseStrict) // exactly 1 match found - // With "nocase", Unmarshal looks first for an exact match, + // With "case:ignore", Unmarshal looks first for an exact match, // then for a case-insensitive match if none found. - var nocase []struct { - X bool `json:"firstName,nocase"` + var caseIgnore []struct { + X bool `json:"firstName,case:ignore"` } - if err := json.Unmarshal([]byte(input), &nocase); err != nil { + if err := json.Unmarshal([]byte(input), &caseIgnore); err != nil { log.Fatal(err) } - fmt.Println(nocase) // 8 matches found + fmt.Println(caseIgnore) // 8 matches found // Output: // [{false} {true} {false} {false} {false} {false} {false} {false} {false}] diff --git a/fields.go b/fields.go index 15535b2..082f320 100644 --- a/fields.go +++ b/fields.go @@ -331,7 +331,7 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError) } for foldedName, fields := range fs.byFoldedName { if len(fields) > 1 { - // The precedence order for conflicting nocase names + // The precedence order for conflicting ignoreCase names // is by breadth-first order, rather than depth-first order. slices.SortFunc(fields, func(x, y *structField) int { return cmp.Compare(x.id, y.id) @@ -359,10 +359,10 @@ func indirectType(t reflect.Type) reflect.Type { // matchFoldedName matches a case-insensitive name depending on the options. // It assumes that foldName(f.name) == foldName(name). // -// Case-insensitive matching is used if the `nocase` tag option is specified +// Case-insensitive matching is used if the `case:ignore` tag option is specified // or the MatchCaseInsensitiveNames call option is specified -// (and the `strictcase` tag option is not specified). -// Functionally, the `nocase` and `strictcase` tag options take precedence. +// (and the `case:strict` tag option is not specified). +// Functionally, the `case:ignore` and `case:strict` tag options take precedence. // // The v1 definition of case-insensitivity operated under strings.EqualFold // and would strictly compare dashes and underscores, @@ -370,7 +370,7 @@ func indirectType(t reflect.Type) reflect.Type { // Thus, if the MatchCaseSensitiveDelimiter call option is specified, // the match is further restricted to using strings.EqualFold. func (f *structField) matchFoldedName(name []byte, flags *jsonflags.Flags) bool { - if f.casing == nocase || (flags.Get(jsonflags.MatchCaseInsensitiveNames) && f.casing != strictcase) { + if f.casing == caseIgnore || (flags.Get(jsonflags.MatchCaseInsensitiveNames) && f.casing != caseStrict) { if !flags.Get(jsonflags.MatchCaseSensitiveDelimiter) || strings.EqualFold(string(name), f.name) { return true } @@ -379,8 +379,8 @@ func (f *structField) matchFoldedName(name []byte, flags *jsonflags.Flags) bool } const ( - nocase = 1 - strictcase = 2 + caseIgnore = 1 + caseStrict = 2 ) type fieldOptions struct { @@ -388,7 +388,7 @@ type fieldOptions struct { quotedName string // quoted name per RFC 8785, section 3.2.2.2. hasName bool nameNeedEscape bool - casing int8 // either 0, nocase, or strictcase + casing int8 // either 0, caseIgnore, or caseStrict inline bool unknown bool omitzero bool @@ -490,10 +490,30 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool, err = cmp.Or(err, fmt.Errorf("Go struct field %s has unnecessarily quoted appearance of `%s` tag option; specify `%s` instead", sf.Name, rawOpt, opt)) } switch opt { - case "nocase": - out.casing |= nocase - case "strictcase": - out.casing |= strictcase + case "case": + if !strings.HasPrefix(tag, ":") { + err = cmp.Or(err, fmt.Errorf("Go struct field %s is missing value for `case` tag option; specify `case:ignore` or `case:strict` instead", sf.Name)) + break + } + tag = tag[len(":"):] + opt, n, err2 := consumeTagOption(tag) + if err2 != nil { + err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed value for `case` tag option: %v", sf.Name, err2)) + break + } + rawOpt := tag[:n] + tag = tag[n:] + if strings.HasPrefix(rawOpt, "'") { + err = cmp.Or(err, fmt.Errorf("Go struct field %s has unnecessarily quoted appearance of `case:%s` tag option; specify `case:%s` instead", sf.Name, rawOpt, opt)) + } + switch opt { + case "ignore": + out.casing |= caseIgnore + case "strict": + out.casing |= caseStrict + default: + err = cmp.Or(err, fmt.Errorf("Go struct field %s has unknown `case:%s` tag value", sf.Name, rawOpt)) + } case "inline": out.inline = true case "unknown": @@ -523,7 +543,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool, // This catches invalid mutants such as "omitEmpty" or "omit_empty". normOpt := strings.ReplaceAll(strings.ToLower(opt), "_", "") switch normOpt { - case "nocase", "strictcase", "inline", "unknown", "omitzero", "omitempty", "string", "format": + case "case", "inline", "unknown", "omitzero", "omitempty", "string", "format": err = cmp.Or(err, fmt.Errorf("Go struct field %s has invalid appearance of `%s` tag option; specify `%s` instead", sf.Name, opt, normOpt)) } @@ -534,8 +554,8 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool, // Reject duplicates. switch { - case out.casing == nocase|strictcase: - err = cmp.Or(err, fmt.Errorf("Go struct field %s cannot have both `nocase` and `strictcase` tag options", sf.Name)) + case out.casing == caseIgnore|caseStrict: + err = cmp.Or(err, fmt.Errorf("Go struct field %s cannot have both `case:ignore` and `case:strict` tag options", sf.Name)) case seenOpts[opt]: err = cmp.Or(err, fmt.Errorf("Go struct field %s has duplicate appearance of `%s` tag option", sf.Name, rawOpt)) } diff --git a/fields_test.go b/fields_test.go index 29a51d8..2c87098 100644 --- a/fields_test.go +++ b/fields_test.go @@ -38,13 +38,13 @@ func TestMakeStructFields(t *testing.T) { F2 string `json:"-"` F3 string `json:"json_name"` f3 string - F5 string `json:"json_name_nocase,nocase"` + 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: nocase}}, + {id: 2, index: []int{4}, typ: stringType, fieldOptions: fieldOptions{name: "json_name_nocase", quotedName: `"json_name_nocase"`, hasName: true, casing: caseIgnore}}, }, }, }, { @@ -615,24 +615,45 @@ func TestParseTagOptions(t *testing.T) { 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("NoCaseOption"), + name: jsontest.Name("CaseAloneOption"), in: struct { - FieldName int `json:",nocase"` + FieldName int `json:",case"` }{}, - wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: nocase}, + 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("StrictCaseOption"), + name: jsontest.Name("CaseQuotedOption"), in: struct { - FieldName int `json:",strictcase"` + FieldName int `json:",case:'ignore'"` }{}, - wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: strictcase}, + 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:",nocase,strictcase"` + FieldName int `json:",case:ignore,case:strict"` }{}, - wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: nocase | strictcase}, - wantErr: errors.New("Go struct field FieldName cannot have both `nocase` and `strictcase` tag options"), + 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 { @@ -699,12 +720,12 @@ func TestParseTagOptions(t *testing.T) { }, { name: jsontest.Name("AllOptions"), in: struct { - FieldName int `json:",nocase,inline,unknown,omitzero,omitempty,string,format:format"` + FieldName int `json:",case:ignore,inline,unknown,omitzero,omitempty,string,format:format"` }{}, wantOpts: fieldOptions{ name: "FieldName", quotedName: `"FieldName"`, - casing: nocase, + casing: caseIgnore, inline: true, unknown: true, omitzero: true, @@ -715,12 +736,12 @@ func TestParseTagOptions(t *testing.T) { }, { name: jsontest.Name("AllOptionsQuoted"), in: struct { - FieldName int `json:",'nocase','inline','unknown','omitzero','omitempty','string','format':'format'"` + FieldName int `json:",'case':'ignore','inline','unknown','omitzero','omitempty','string','format':'format'"` }{}, wantOpts: fieldOptions{ name: "FieldName", quotedName: `"FieldName"`, - casing: nocase, + casing: caseIgnore, inline: true, unknown: true, omitzero: true, @@ -728,18 +749,18 @@ func TestParseTagOptions(t *testing.T) { string: true, format: "format", }, - wantErr: errors.New("Go struct field FieldName has unnecessarily quoted appearance of `'nocase'` tag option; specify `nocase` instead"), + 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:",NOCASE,INLINE,UNKNOWN,OMITZERO,OMITEMPTY,STRING,FORMAT:FORMAT"` + 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 `NOCASE` tag option; specify `nocase` instead"), + 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:", nocase , inline , unknown , omitzero , omitempty , string , format:format "` + 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)"), diff --git a/fold_test.go b/fold_test.go index fc7e421..5dbfcd4 100644 --- a/fold_test.go +++ b/fold_test.go @@ -111,7 +111,7 @@ func TestBenchmarkUnmarshalUnknown(t *testing.T) { fields = append(fields, reflect.StructField{ Name: fmt.Sprintf("Name%d", i), Type: T[int](), - Tag: `json:",nocase"`, + Tag: `json:",case:ignore"`, }) } out := reflect.New(reflect.StructOf(fields)).Interface() diff --git a/options.go b/options.go index b5fa424..787b96d 100644 --- a/options.go +++ b/options.go @@ -185,7 +185,7 @@ func OmitZeroStructFields(v bool) Options { // MatchCaseInsensitiveNames specifies that JSON object members are matched // against Go struct fields using a case-insensitive match of the name. -// Go struct fields explicitly marked with `strictcase` or `nocase` +// Go struct fields explicitly marked with `case:strict` or `case:ignore` // always use case-sensitive (or case-insensitive) name matching, // regardless of the value of this option. // diff --git a/v1/diff_test.go b/v1/diff_test.go index ae801bb..7879d6d 100644 --- a/v1/diff_test.go +++ b/v1/diff_test.go @@ -37,7 +37,7 @@ var jsonPackages = []struct { // // Case-insensitive matching is a surprising default and // incurs significant performance cost when unmarshaling unknown fields. -// In v2, we can opt into v1-like behavior with the `nocase` tag option. +// In v2, we can opt into v1-like behavior with the `case:ignore` tag option. // The case-insensitive matching performed by v2 is looser than that of v1 // where it also ignores dashes and underscores. // This allows v2 to match fields regardless of whether the name is in @@ -50,7 +50,7 @@ func TestCaseSensitivity(t *testing.T) { type Fields struct { FieldA bool FieldB bool `json:"fooBar"` - FieldC bool `json:"fizzBuzz,nocase"` // `nocase` is used by v2 to explicitly enable case-insensitive matching + FieldC bool `json:"fizzBuzz,case:ignore"` // `case:ignore` is used by v2 to explicitly enable case-insensitive matching } for _, json := range jsonPackages { @@ -82,8 +82,8 @@ func TestCaseSensitivity(t *testing.T) { }, "FieldC": { "fizzBuzz": true, // exact match for explicitly specified JSON name - "fizzbuzz": true, // v2 is case-insensitive due to `nocase` tag - "FIZZBUZZ": true, // v2 is case-insensitive due to `nocase` tag + "fizzbuzz": true, // v2 is case-insensitive due to `case:ignore` tag + "FIZZBUZZ": true, // v2 is case-insensitive due to `case:ignore` tag "fizz_buzz": onlyV2, // case-insensitivity in v2 ignores dashes and underscores "fizz-buzz": onlyV2, // case-insensitivity in v2 ignores dashes and underscores "fooBar": false, diff --git a/v1/options.go b/v1/options.go index 663ceb1..cb2a0b0 100644 --- a/v1/options.go +++ b/v1/options.go @@ -24,8 +24,8 @@ // In contrast, v2 matches fields using an exact, case-sensitive match. // The [jsonv2.MatchCaseInsensitiveNames] and [MatchCaseSensitiveDelimiter] // options control this behavior difference. To explicitly specify a Go struct -// field to use a particular name matching scheme, either the `nocase` -// or the `strictcase` field option can be specified. +// field to use a particular name matching scheme, either the `case:ignore` +// or the `case:strict` field option can be specified. // Field-specified options take precedence over caller-specified options. // // - In v1, when marshaling a Go struct, a field marked as `omitempty` @@ -358,7 +358,7 @@ func FormatTimeWithLegacySemantics(v bool) Options { // MatchCaseSensitiveDelimiter specifies that underscores and dashes are // not to be ignored when performing case-insensitive name matching which -// occurs under [jsonv2.MatchCaseInsensitiveNames] or the `nocase` tag option. +// occurs under [jsonv2.MatchCaseInsensitiveNames] or the `case:ignore` tag option. // Thus, case-insensitive name matching is identical to [strings.EqualFold]. // Use of this option diminishes the ability of case-insensitive matching // to be able to match common case variants (e.g, "foo_bar" with "fooBar").