diff --git a/feature_config.go b/feature_config.go index a39b21b..878d362 100644 --- a/feature_config.go +++ b/feature_config.go @@ -12,26 +12,30 @@ type Config struct { IndentionStep int MarshalFloatWith6Digits bool SupportUnexportedStructFields bool - EscapeHtml bool + EscapeHtml bool + SortMapKeys bool } type frozenConfig struct { - configBeforeFrozen Config - indentionStep int - decoderCache unsafe.Pointer - encoderCache unsafe.Pointer - extensions []ExtensionFunc + configBeforeFrozen Config + sortMapKeys bool + indentionStep int + decoderCache unsafe.Pointer + encoderCache unsafe.Pointer + extensions []ExtensionFunc } var ConfigOfDefault = Config{}.Froze() // Trying to be 100% compatible with standard library behavior var ConfigCompatibleWithStandardLibrary = Config{ - EscapeHtml: true, + EscapeHtml: true, + SortMapKeys: true, }.Froze() func (cfg Config) Froze() *frozenConfig { frozenConfig := &frozenConfig{ + sortMapKeys: cfg.SortMapKeys, indentionStep: cfg.IndentionStep, } atomic.StorePointer(&frozenConfig.decoderCache, unsafe.Pointer(&map[string]Decoder{})) @@ -232,4 +236,4 @@ func (cfg *frozenConfig) Unmarshal(data []byte, v interface{}) error { iter.reportError("Unmarshal", "there are bytes left after unmarshal") } return iter.Error -} \ No newline at end of file +} diff --git a/feature_reflect.go b/feature_reflect.go index bd016e3..48b8dfa 100644 --- a/feature_reflect.go +++ b/feature_reflect.go @@ -487,6 +487,9 @@ func encoderOfMap(cfg *frozenConfig, typ reflect.Type) (Encoder, error) { return nil, err } mapInterface := reflect.New(typ).Elem().Interface() - encoderForMap := &mapEncoder{typ, elemType, encoder, *((*emptyInterface)(unsafe.Pointer(&mapInterface)))} - return encoderForMap, nil + if cfg.sortMapKeys { + return &sortKeysMapEncoder{typ, elemType, encoder, *((*emptyInterface)(unsafe.Pointer(&mapInterface)))}, nil + } else { + return &mapEncoder{typ, elemType, encoder, *((*emptyInterface)(unsafe.Pointer(&mapInterface)))}, nil + } } diff --git a/feature_reflect_map.go b/feature_reflect_map.go index 8b93de7..cfde9c9 100644 --- a/feature_reflect_map.go +++ b/feature_reflect_map.go @@ -6,6 +6,7 @@ import ( "reflect" "strconv" "unsafe" + "sort" ) type mapDecoder struct { @@ -136,3 +137,81 @@ func (encoder *mapEncoder) isEmpty(ptr unsafe.Pointer) bool { realVal := reflect.ValueOf(*realInterface) return realVal.Len() == 0 } + +type sortKeysMapEncoder struct { + mapType reflect.Type + elemType reflect.Type + elemEncoder Encoder + mapInterface emptyInterface +} + +func (encoder *sortKeysMapEncoder) encode(ptr unsafe.Pointer, stream *Stream) { + mapInterface := encoder.mapInterface + mapInterface.word = ptr + realInterface := (*interface{})(unsafe.Pointer(&mapInterface)) + realVal := reflect.ValueOf(*realInterface) + + + // Extract and sort the keys. + keys := realVal.MapKeys() + sv := make([]reflectWithString, len(keys)) + for i, v := range keys { + sv[i].v = v + if err := sv[i].resolve(); err != nil { + stream.Error = err + return + } + } + sort.Slice(sv, func(i, j int) bool { return sv[i].s < sv[j].s }) + + stream.WriteObjectStart() + for i, key := range sv { + if i != 0 { + stream.WriteMore() + } + encodeMapKey(key.v, stream) + stream.writeByte(':') + val := realVal.MapIndex(key.v).Interface() + encoder.elemEncoder.encodeInterface(val, stream) + } + stream.WriteObjectEnd() +} + + +type reflectWithString struct { + v reflect.Value + s string +} + +func (w *reflectWithString) resolve() error { + if w.v.Kind() == reflect.String { + w.s = w.v.String() + return nil + } + if tm, ok := w.v.Interface().(encoding.TextMarshaler); ok { + buf, err := tm.MarshalText() + w.s = string(buf) + return err + } + switch w.v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + w.s = strconv.FormatInt(w.v.Int(), 10) + return nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + w.s = strconv.FormatUint(w.v.Uint(), 10) + return nil + } + panic("unexpected map key type") +} + +func (encoder *sortKeysMapEncoder) encodeInterface(val interface{}, stream *Stream) { + writeToStream(val, stream, encoder) +} + +func (encoder *sortKeysMapEncoder) isEmpty(ptr unsafe.Pointer) bool { + mapInterface := encoder.mapInterface + mapInterface.word = ptr + realInterface := (*interface{})(unsafe.Pointer(&mapInterface)) + realVal := reflect.ValueOf(*realInterface) + return realVal.Len() == 0 +} diff --git a/jsoniter_map_test.go b/jsoniter_map_test.go index 61bd75d..8272fae 100644 --- a/jsoniter_map_test.go +++ b/jsoniter_map_test.go @@ -125,3 +125,17 @@ func Test_map_key_with_escaped_char(t *testing.T) { should.Equal(map[string]string{"k\"ey": "val"}, obj.Map) } } + +func Test_encode_map_with_sorted_keys(t *testing.T) { + should := require.New(t) + m := map[string]interface{}{ + "3": 3, + "1": 1, + "2": 2, + } + bytes, err := json.Marshal(m) + should.Nil(err) + output, err := ConfigCompatibleWithStandardLibrary.MarshalToString(m) + should.Nil(err) + should.Equal(string(bytes), output) +}