diff --git a/api_tests/config_test.go b/api_tests/config_test.go index d3e93e5..7aa349b 100644 --- a/api_tests/config_test.go +++ b/api_tests/config_test.go @@ -2,9 +2,10 @@ package test import ( "encoding/json" + "testing" + "github.com/json-iterator/go" "github.com/stretchr/testify/require" - "testing" ) func Test_use_number_for_unmarshal(t *testing.T) { @@ -45,3 +46,129 @@ func Test_read_large_number_as_interface(t *testing.T) { should.Nil(err) should.Equal(`123456789123456789123456789`, output) } + +type caseSensitiveStruct struct { + A string `json:"a"` + B string `json:"b,omitempty"` + C *C `json:"C,omitempty"` +} + +type C struct { + D int64 `json:"D,omitempty"` + E *E `json:"e,omitempty"` +} + +type E struct { + F string `json:"F,omitempty"` +} + +func Test_CaseSensitive(t *testing.T) { + should := require.New(t) + + testCases := []struct { + input string + expectedOutput string + caseSensitive bool + }{ + { + input: `{"A":"foo","B":"bar"}`, + expectedOutput: `{"a":"foo","b":"bar"}`, + caseSensitive: false, + }, + { + input: `{"a":"foo","b":"bar"}`, + expectedOutput: `{"a":"foo","b":"bar"}`, + caseSensitive: true, + }, + { + input: `{"a":"foo","b":"bar","C":{"D":10}}`, + expectedOutput: `{"a":"foo","b":"bar","C":{"D":10}}`, + caseSensitive: true, + }, + { + input: `{"a":"foo","B":"bar","c":{"d":10}}`, + expectedOutput: `{"a":"foo"}`, + caseSensitive: true, + }, + { + input: `{"a":"foo","C":{"d":10}}`, + expectedOutput: `{"a":"foo","C":{}}`, + caseSensitive: true, + }, + { + input: `{"a":"foo","C":{"D":10,"e":{"f":"baz"}}}`, + expectedOutput: `{"a":"foo","C":{"D":10,"e":{}}}`, + caseSensitive: true, + }, + { + input: `{"a":"foo","C":{"D":10,"e":{"F":"baz"}}}`, + expectedOutput: `{"a":"foo","C":{"D":10,"e":{"F":"baz"}}}`, + caseSensitive: true, + }, + { + input: `{"A":"foo","c":{"d":10,"E":{"f":"baz"}}}`, + expectedOutput: `{"a":"foo","C":{"D":10,"e":{"F":"baz"}}}`, + caseSensitive: false, + }, + } + + for _, tc := range testCases { + val := caseSensitiveStruct{} + err := jsoniter.Config{CaseSensitive: tc.caseSensitive}.Froze().UnmarshalFromString(tc.input, &val) + should.Nil(err) + + output, err := jsoniter.MarshalToString(val) + should.Nil(err) + should.Equal(tc.expectedOutput, output) + } +} + +type structWithElevenFields struct { + A string `json:"A,omitempty"` + B string `json:"B,omitempty"` + C string `json:"C,omitempty"` + D string `json:"d,omitempty"` + E string `json:"e,omitempty"` + F string `json:"f,omitempty"` + G string `json:"g,omitempty"` + H string `json:"h,omitempty"` + I string `json:"i,omitempty"` + J string `json:"j,omitempty"` + K string `json:"k,omitempty"` +} + +func Test_CaseSensitive_MoreThanTenFields(t *testing.T) { + should := require.New(t) + + testCases := []struct { + input string + expectedOutput string + caseSensitive bool + }{ + { + input: `{"A":"1","B":"2","C":"3","d":"4","e":"5","f":"6","g":"7","h":"8","i":"9","j":"10","k":"11"}`, + expectedOutput: `{"A":"1","B":"2","C":"3","d":"4","e":"5","f":"6","g":"7","h":"8","i":"9","j":"10","k":"11"}`, + caseSensitive: true, + }, + { + input: `{"a":"1","b":"2","c":"3","D":"4","E":"5","F":"6"}`, + expectedOutput: `{"A":"1","B":"2","C":"3","d":"4","e":"5","f":"6"}`, + caseSensitive: false, + }, + { + input: `{"A":"1","b":"2","d":"4","E":"5"}`, + expectedOutput: `{"A":"1","d":"4"}`, + caseSensitive: true, + }, + } + + for _, tc := range testCases { + val := structWithElevenFields{} + err := jsoniter.Config{CaseSensitive: tc.caseSensitive}.Froze().UnmarshalFromString(tc.input, &val) + should.Nil(err) + + output, err := jsoniter.MarshalToString(val) + should.Nil(err) + should.Equal(tc.expectedOutput, output) + } +} diff --git a/config.go b/config.go index bd66947..8358191 100644 --- a/config.go +++ b/config.go @@ -2,12 +2,13 @@ package jsoniter import ( "encoding/json" - "github.com/modern-go/concurrent" - "github.com/modern-go/reflect2" "io" "reflect" "sync" "unsafe" + + "github.com/modern-go/concurrent" + "github.com/modern-go/reflect2" ) // Config customize how the API should behave. @@ -23,6 +24,7 @@ type Config struct { OnlyTaggedField bool ValidateJsonRawMessage bool ObjectFieldMustBeSimpleString bool + CaseSensitive bool } // API the public interface of this package. @@ -75,6 +77,7 @@ type frozenConfig struct { extensions []Extension streamPool *sync.Pool iteratorPool *sync.Pool + caseSensitive bool } func (cfg *frozenConfig) initCache() { @@ -128,6 +131,7 @@ func (cfg Config) Froze() API { objectFieldMustBeSimpleString: cfg.ObjectFieldMustBeSimpleString, onlyTaggedField: cfg.OnlyTaggedField, disallowUnknownFields: cfg.DisallowUnknownFields, + caseSensitive: cfg.CaseSensitive, } api.streamPool = &sync.Pool{ New: func() interface{} { diff --git a/iter_object.go b/iter_object.go index ebd3da8..6e7c370 100644 --- a/iter_object.go +++ b/iter_object.go @@ -60,7 +60,7 @@ func (iter *Iterator) readFieldHash() int64 { if b == '\\' { iter.head = i for _, b := range iter.readStringSlowPath() { - if 'A' <= b && b <= 'Z' { + if 'A' <= b && b <= 'Z' && !iter.cfg.caseSensitive { b += 'a' - 'A' } hash ^= int64(b) @@ -82,7 +82,7 @@ func (iter *Iterator) readFieldHash() int64 { } return hash } - if 'A' <= b && b <= 'Z' { + if 'A' <= b && b <= 'Z' && !iter.cfg.caseSensitive { b += 'a' - 'A' } hash ^= int64(b) @@ -95,10 +95,14 @@ func (iter *Iterator) readFieldHash() int64 { } } -func calcHash(str string) int64 { +func calcHash(str string, caseSensitive bool) int64 { hash := int64(0x811c9dc5) for _, b := range str { - hash ^= int64(unicode.ToLower(b)) + if caseSensitive { + hash ^= int64(b) + } else { + hash ^= int64(unicode.ToLower(b)) + } hash *= 0x1000193 } return int64(hash) diff --git a/reflect.go b/reflect.go index 5c7d3a8..be7a0e2 100644 --- a/reflect.go +++ b/reflect.go @@ -2,9 +2,10 @@ package jsoniter import ( "fmt" - "github.com/modern-go/reflect2" "reflect" "unsafe" + + "github.com/modern-go/reflect2" ) // ValDecoder is an internal type registered to cache as needed. @@ -40,6 +41,14 @@ type ctx struct { decoders map[reflect2.Type]ValDecoder } +func (b *ctx) caseSensitive() bool { + if b.frozenConfig == nil { + // default is case-insensitive + return false + } + return b.frozenConfig.caseSensitive +} + func (b *ctx) append(prefix string) *ctx { return &ctx{ frozenConfig: b.frozenConfig, diff --git a/reflect_struct_decoder.go b/reflect_struct_decoder.go index fd76647..355d2d1 100644 --- a/reflect_struct_decoder.go +++ b/reflect_struct_decoder.go @@ -2,10 +2,11 @@ package jsoniter import ( "fmt" - "github.com/modern-go/reflect2" "io" "strings" "unsafe" + + "github.com/modern-go/reflect2" ) func decoderOfStruct(ctx *ctx, typ reflect2.Type) ValDecoder { @@ -31,11 +32,15 @@ func decoderOfStruct(ctx *ctx, typ reflect2.Type) ValDecoder { for k, binding := range bindings { fields[k] = binding.Decoder.(*structFieldDecoder) } - for k, binding := range bindings { - if _, found := fields[strings.ToLower(k)]; !found { - fields[strings.ToLower(k)] = binding.Decoder.(*structFieldDecoder) + + if !ctx.caseSensitive() { + for k, binding := range bindings { + if _, found := fields[strings.ToLower(k)]; !found { + fields[strings.ToLower(k)] = binding.Decoder.(*structFieldDecoder) + } } } + return createStructDecoder(ctx, typ, fields) } @@ -46,12 +51,13 @@ func createStructDecoder(ctx *ctx, typ reflect2.Type, fields map[string]*structF knownHash := map[int64]struct{}{ 0: {}, } + switch len(fields) { case 0: return &skipObjectDecoder{typ} case 1: for fieldName, fieldDecoder := range fields { - fieldHash := calcHash(fieldName) + fieldHash := calcHash(fieldName, ctx.caseSensitive()) _, known := knownHash[fieldHash] if known { return &generalStructDecoder{typ, fields, false} @@ -65,7 +71,7 @@ func createStructDecoder(ctx *ctx, typ reflect2.Type, fields map[string]*structF var fieldDecoder1 *structFieldDecoder var fieldDecoder2 *structFieldDecoder for fieldName, fieldDecoder := range fields { - fieldHash := calcHash(fieldName) + fieldHash := calcHash(fieldName, ctx.caseSensitive()) _, known := knownHash[fieldHash] if known { return &generalStructDecoder{typ, fields, false} @@ -88,7 +94,7 @@ func createStructDecoder(ctx *ctx, typ reflect2.Type, fields map[string]*structF var fieldDecoder2 *structFieldDecoder var fieldDecoder3 *structFieldDecoder for fieldName, fieldDecoder := range fields { - fieldHash := calcHash(fieldName) + fieldHash := calcHash(fieldName, ctx.caseSensitive()) _, known := knownHash[fieldHash] if known { return &generalStructDecoder{typ, fields, false} @@ -119,7 +125,7 @@ func createStructDecoder(ctx *ctx, typ reflect2.Type, fields map[string]*structF var fieldDecoder3 *structFieldDecoder var fieldDecoder4 *structFieldDecoder for fieldName, fieldDecoder := range fields { - fieldHash := calcHash(fieldName) + fieldHash := calcHash(fieldName, ctx.caseSensitive()) _, known := knownHash[fieldHash] if known { return &generalStructDecoder{typ, fields, false} @@ -156,7 +162,7 @@ func createStructDecoder(ctx *ctx, typ reflect2.Type, fields map[string]*structF var fieldDecoder4 *structFieldDecoder var fieldDecoder5 *structFieldDecoder for fieldName, fieldDecoder := range fields { - fieldHash := calcHash(fieldName) + fieldHash := calcHash(fieldName, ctx.caseSensitive()) _, known := knownHash[fieldHash] if known { return &generalStructDecoder{typ, fields, false} @@ -199,7 +205,7 @@ func createStructDecoder(ctx *ctx, typ reflect2.Type, fields map[string]*structF var fieldDecoder5 *structFieldDecoder var fieldDecoder6 *structFieldDecoder for fieldName, fieldDecoder := range fields { - fieldHash := calcHash(fieldName) + fieldHash := calcHash(fieldName, ctx.caseSensitive()) _, known := knownHash[fieldHash] if known { return &generalStructDecoder{typ, fields, false} @@ -248,7 +254,7 @@ func createStructDecoder(ctx *ctx, typ reflect2.Type, fields map[string]*structF var fieldDecoder6 *structFieldDecoder var fieldDecoder7 *structFieldDecoder for fieldName, fieldDecoder := range fields { - fieldHash := calcHash(fieldName) + fieldHash := calcHash(fieldName, ctx.caseSensitive()) _, known := knownHash[fieldHash] if known { return &generalStructDecoder{typ, fields, false} @@ -303,7 +309,7 @@ func createStructDecoder(ctx *ctx, typ reflect2.Type, fields map[string]*structF var fieldDecoder7 *structFieldDecoder var fieldDecoder8 *structFieldDecoder for fieldName, fieldDecoder := range fields { - fieldHash := calcHash(fieldName) + fieldHash := calcHash(fieldName, ctx.caseSensitive()) _, known := knownHash[fieldHash] if known { return &generalStructDecoder{typ, fields, false} @@ -364,7 +370,7 @@ func createStructDecoder(ctx *ctx, typ reflect2.Type, fields map[string]*structF var fieldDecoder8 *structFieldDecoder var fieldDecoder9 *structFieldDecoder for fieldName, fieldDecoder := range fields { - fieldHash := calcHash(fieldName) + fieldHash := calcHash(fieldName, ctx.caseSensitive()) _, known := knownHash[fieldHash] if known { return &generalStructDecoder{typ, fields, false} @@ -431,7 +437,7 @@ func createStructDecoder(ctx *ctx, typ reflect2.Type, fields map[string]*structF var fieldDecoder9 *structFieldDecoder var fieldDecoder10 *structFieldDecoder for fieldName, fieldDecoder := range fields { - fieldHash := calcHash(fieldName) + fieldHash := calcHash(fieldName, ctx.caseSensitive()) _, known := knownHash[fieldHash] if known { return &generalStructDecoder{typ, fields, false} @@ -513,13 +519,13 @@ func (decoder *generalStructDecoder) decodeOneField(ptr unsafe.Pointer, iter *It fieldBytes := iter.ReadStringAsSlice() field = *(*string)(unsafe.Pointer(&fieldBytes)) fieldDecoder = decoder.fields[field] - if fieldDecoder == nil { + if fieldDecoder == nil && !iter.cfg.caseSensitive { fieldDecoder = decoder.fields[strings.ToLower(field)] } } else { field = iter.ReadString() fieldDecoder = decoder.fields[field] - if fieldDecoder == nil { + if fieldDecoder == nil && !iter.cfg.caseSensitive { fieldDecoder = decoder.fields[strings.ToLower(field)] } }