Skip to content

Commit 38d60d9

Browse files
alecsammonmweibel
authored andcommitted
Allow using a custom KV store to keep consistent JSON ordering
1 parent 60a4260 commit 38d60d9

File tree

4 files changed

+95
-14
lines changed

4 files changed

+95
-14
lines changed

README.md

+42
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,48 @@ func main() {
217217
// ]
218218
```
219219

220+
## Output ordering
221+
222+
Sheriff converts the input struct into a basic structure using `map[string]interface{}`. This means that the generated
223+
JSON will not have the same ordering as the input struct. If you need to have a specific ordering then a custom
224+
implementation of the `KVStoreFactory` can be passed as an option.
225+
```go
226+
package main
227+
228+
import (
229+
"github.com/liip/sheriff/v2"
230+
orderedmap "github.com/wk8/go-ordered-map/v2"
231+
)
232+
233+
type OrderedMap struct {
234+
*orderedmap.OrderedMap[string, interface{}]
235+
}
236+
237+
func NewOrderedMap() *OrderedMap {
238+
return &OrderedMap{orderedmap.New[string, interface{}]()}
239+
}
240+
241+
func (om *OrderedMap) Set(k string, v interface{}) {
242+
om.OrderedMap.Set(k, v)
243+
}
244+
245+
func (om *OrderedMap) Each(f func(k string, v interface{})) {
246+
for pair := om.Newest(); pair != nil; pair = pair.Prev() {
247+
f(pair.Key, pair.Value)
248+
}
249+
}
250+
251+
func main() {
252+
opt := &sheriff.Options{
253+
KVStoreFactory: func() sheriff.KVStore {
254+
return NewOrderedMap()
255+
},
256+
}
257+
258+
// ...
259+
}
260+
```
261+
220262
## Benchmarks
221263

222264
There's a simple benchmark in `bench_test.go` which compares running sheriff -> JSON versus just marshalling into JSON

kvstore.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package sheriff
2+
3+
// kvStore is the default implementation of the KVStore interface that sheriff converts a struct into.
4+
// It is the fastest option, but does result in a re-ordering of the final JSON properties.
5+
type kvStore map[string]interface{}
6+
7+
// Set inserts the value into the map at the given key.
8+
func (m kvStore) Set(k string, v interface{}) {
9+
m[k] = v
10+
}
11+
12+
// Each applies the callback function to each element in the map.
13+
func (m kvStore) Each(f func(k string, v interface{})) {
14+
for k, v := range m {
15+
f(k, v)
16+
}
17+
}

sheriff.go

+30-8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ import (
1010
"github.com/hashicorp/go-version"
1111
)
1212

13+
// A KVStore is a simple key-value store.
14+
// The default implementation uses a map[string]interface{}.
15+
// This is fast, however will lead to inconsistent ordering of the keys in the generated JSON.
16+
// A custom implementation can be used to maintain the order of the keys.
17+
type KVStore interface {
18+
Set(k string, v interface{})
19+
Each(f func(k string, v interface{}))
20+
}
21+
1322
// A FieldFilter is a function that decides whether a field should be marshalled or not.
1423
// If it returns true, the field will be marshalled, otherwise it will be skipped.
1524
type FieldFilter func(field reflect.StructField) (bool, error)
@@ -38,6 +47,12 @@ type Options struct {
3847
// This option is false by default.
3948
IncludeEmptyTag bool
4049

50+
// The KVStoreFactory is a function that returns a new KVStore.
51+
// The default implementation uses a map[string]interface{}, which is fast but does not maintain the order of the
52+
// keys.
53+
// A custom implementation can be used to maintain the order of the keys, i.e. using github.com/wk8/go-ordered-map
54+
KVStoreFactory func() KVStore
55+
4156
// This is used internally so that we can propagate anonymous fields groups tag to all child field.
4257
nestedGroupsMap map[string][]string
4358
}
@@ -81,6 +96,12 @@ func Marshal(options *Options, data interface{}) (interface{}, error) {
8196
options.FieldFilter = createDefaultFieldFilter(options)
8297
}
8398

99+
if options.KVStoreFactory == nil {
100+
options.KVStoreFactory = func() KVStore {
101+
return kvStore{}
102+
}
103+
}
104+
84105
if t.Kind() == reflect.Ptr {
85106
// follow pointer
86107
t = t.Elem()
@@ -94,7 +115,7 @@ func Marshal(options *Options, data interface{}) (interface{}, error) {
94115
return marshalValue(options, v)
95116
}
96117

97-
dest := make(map[string]interface{})
118+
dest := options.KVStoreFactory()
98119

99120
for i := 0; i < t.NumField(); i++ {
100121
field := t.Field(i)
@@ -172,13 +193,13 @@ func Marshal(options *Options, data interface{}) (interface{}, error) {
172193

173194
// when a composition field we want to bring the child
174195
// nodes to the top
175-
nestedVal, ok := v.(map[string]interface{})
196+
nestedVal, ok := v.(KVStore)
176197
if isEmbeddedField && ok {
177-
for key, value := range nestedVal {
178-
dest[key] = value
179-
}
198+
nestedVal.Each(func(k string, v interface{}) {
199+
dest.Set(k, v)
200+
})
180201
} else {
181-
dest[jsonTag] = v
202+
dest.Set(jsonTag, v)
182203
}
183204
}
184205

@@ -303,13 +324,14 @@ func marshalValue(options *Options, v reflect.Value) (interface{}, error) {
303324
if mapKeys[0].Kind() != reflect.String {
304325
return nil, MarshalInvalidTypeError{t: mapKeys[0].Kind(), data: val}
305326
}
306-
dest := make(map[string]interface{})
327+
328+
dest := options.KVStoreFactory()
307329
for _, key := range mapKeys {
308330
d, err := marshalValue(options, v.MapIndex(key))
309331
if err != nil {
310332
return nil, err
311333
}
312-
dest[key.String()] = d
334+
dest.Set(key.String(), d)
313335
}
314336
return dest, nil
315337
}

sheriff_test.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -846,13 +846,12 @@ func TestMarshal_User(t *testing.T) {
846846
TestS: "test",
847847
}
848848

849-
v, err := Marshal(&Options{}, j)
849+
m, err := Marshal(&Options{}, j)
850850
assert.NoError(t, err)
851-
assert.Equal(t, map[string]interface{}{"test": "12", "testb": "true", "testf": "12", "tests": "test"}, v)
852851

853-
d, err := json.Marshal(j)
852+
d, err := json.Marshal(m)
854853
assert.NoError(t, err)
855-
assert.Equal(t, `{"test":"12","testb":"true","testf":"12","tests":"\"test\""}`, string(d))
854+
assert.Equal(t, `{"test":"12","testb":"true","testf":"12","tests":"test"}`, string(d))
856855
}
857856

858857
func TestMarshal_CustomFieldFilter(t *testing.T) {
@@ -873,6 +872,7 @@ func TestMarshal_CustomFieldFilter(t *testing.T) {
873872
m, err := Marshal(o, v)
874873
assert.NoError(t, err)
875874

876-
// ensure the "secret" value is not present in the marshalled map
877-
assert.Equal(t, map[string]interface{}{"test": "teststring"}, m)
875+
d, err := json.Marshal(m)
876+
assert.NoError(t, err)
877+
assert.Equal(t, `{"test":"teststring"}`, string(d))
878878
}

0 commit comments

Comments
 (0)