feat(gjson): add encode_sort_keys option for stable JSON output

Add configurable key sorting for JSON object encoding. When enabled,
object keys are sorted alphabetically for deterministic output.
Default is disabled for better performance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-09 16:09:49 +08:00
parent 62be8bc557
commit ce80352e79
6 changed files with 123 additions and 11 deletions

View File

@ -111,6 +111,23 @@ func (g *GJSON) cfgEncodeKeepBuffer(L *glua.LState) int {
return 1
}
// cfgEncodeSortKeys configures whether to sort object keys for stable output.
// Lua: gjson.encode_sort_keys([sort])
// Returns current value when called without arguments.
// When enabled, object keys are sorted alphabetically for deterministic output.
// When disabled (default), keys are output in arbitrary order for better performance.
func (g *GJSON) cfgEncodeSortKeys(L *glua.LState) int {
if L.GetTop() == 0 {
L.Push(glua.LBool(g.config.encodeSortKeys))
return 1
}
g.config.encodeSortKeys = L.CheckBool(1)
L.Push(glua.LBool(g.config.encodeSortKeys))
return 1
}
// encode is the Lua function for gjson.encode(value).
// Returns (json_string, nil) on success or (nil, error_message) on failure.
func (g *GJSON) encode(L *glua.LState) int {

View File

@ -9,6 +9,7 @@ const (
defaultDecodeMaxDepth = 1000
defaultEncodeNumberPrecision = 14
defaultEncodeKeepBuffer = true
defaultEncodeSortKeys = false // 默认不排序,保持高性能
)
// sparseArrayConfig controls sparse array handling during encoding.
@ -25,6 +26,7 @@ type Config struct {
decodeMaxDepth int
encodeNumberPrecision int
encodeKeepBuffer bool
encodeSortKeys bool // 是否对 object 键排序(稳定输出)
}
// defaultConfig returns a new Config with default values.
@ -39,6 +41,7 @@ func defaultConfig() *Config {
decodeMaxDepth: defaultDecodeMaxDepth,
encodeNumberPrecision: defaultEncodeNumberPrecision,
encodeKeepBuffer: defaultEncodeKeepBuffer,
encodeSortKeys: defaultEncodeSortKeys,
}
}
@ -54,5 +57,6 @@ func (c *Config) Clone() *Config {
decodeMaxDepth: c.decodeMaxDepth,
encodeNumberPrecision: c.encodeNumberPrecision,
encodeKeepBuffer: c.encodeKeepBuffer,
encodeSortKeys: c.encodeSortKeys,
}
}

View File

@ -2,6 +2,7 @@ package gjson
import (
"fmt"
"sort"
"strconv"
"strings"
@ -159,7 +160,51 @@ func encodeArray(L *glua.LState, tbl *glua.LTable, maxIndex int, config *Config,
// encodeObject encodes a Lua table as a JSON object.
func encodeObject(L *glua.LState, tbl *glua.LTable, config *Config, depth int) (string, error) {
elements := make([]string, 0)
// 快速路径:不需要排序时直接编码
if !config.encodeSortKeys {
elements := make([]string, 0)
var encodeErr error
tbl.ForEach(func(key, value glua.LValue) {
if encodeErr != nil {
return
}
// Encode key
var keyStr string
switch k := key.(type) {
case glua.LString:
encoded, _ := json.Marshal(string(k))
keyStr = string(encoded)
case glua.LNumber:
keyStr = formatNumber(float64(k), config.encodeNumberPrecision)
keyStr = "\"" + keyStr + "\""
default:
return
}
// Encode value
valStr, err := encodeLuaValue(L, value, config, depth+1)
if err != nil {
encodeErr = err
return
}
elements = append(elements, keyStr+":"+valStr)
})
if encodeErr != nil {
return "", encodeErr
}
return "{" + strings.Join(elements, ",") + "}", nil
}
// 排序路径:收集所有键值对后排序
type kv struct {
key string
value glua.LValue
}
pairs := make([]kv, 0)
var encodeErr error
tbl.ForEach(func(key, value glua.LValue) {
@ -174,28 +219,34 @@ func encodeObject(L *glua.LState, tbl *glua.LTable, config *Config, depth int) (
encoded, _ := json.Marshal(string(k))
keyStr = string(encoded)
case glua.LNumber:
// Numeric keys are converted to strings
keyStr = formatNumber(float64(k), config.encodeNumberPrecision)
// Quote the numeric key for JSON object
keyStr = "\"" + keyStr + "\""
default:
return // Skip invalid keys
}
// Encode value
valStr, err := encodeLuaValue(L, value, config, depth+1)
if err != nil {
encodeErr = err
return
}
elements = append(elements, keyStr+":"+valStr)
pairs = append(pairs, kv{key: keyStr, value: value})
})
if encodeErr != nil {
return "", encodeErr
}
// 按键排序(保证输出顺序稳定)
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].key < pairs[j].key
})
// 编码值
elements := make([]string, 0, len(pairs))
for _, p := range pairs {
valStr, err := encodeLuaValue(L, p.value, config, depth+1)
if err != nil {
return "", err
}
elements = append(elements, p.key+":"+valStr)
}
return "{" + strings.Join(elements, ",") + "}", nil
}

View File

@ -63,6 +63,7 @@ func Loader(L *glua.LState) int {
L.SetField(mod, "decode_max_depth", L.NewFunction(instance.cfgDecodeMaxDepth))
L.SetField(mod, "encode_number_precision", L.NewFunction(instance.cfgEncodeNumberPrecision))
L.SetField(mod, "encode_keep_buffer", L.NewFunction(instance.cfgEncodeKeepBuffer))
L.SetField(mod, "encode_sort_keys", L.NewFunction(instance.cfgEncodeSortKeys))
L.SetField(mod, "new", L.NewFunction(gjsonNew))
// Set gjson.null (lightuserdata representing JSON null)
@ -94,6 +95,7 @@ func RegisterGlobal(L *glua.LState) {
L.SetField(mod, "decode_max_depth", L.NewFunction(instance.cfgDecodeMaxDepth))
L.SetField(mod, "encode_number_precision", L.NewFunction(instance.cfgEncodeNumberPrecision))
L.SetField(mod, "encode_keep_buffer", L.NewFunction(instance.cfgEncodeKeepBuffer))
L.SetField(mod, "encode_sort_keys", L.NewFunction(instance.cfgEncodeSortKeys))
L.SetField(mod, "new", L.NewFunction(gjsonNew))
L.SetField(mod, "null", instance.null)
L.SetField(mod, "_NAME", glua.LString(ModuleName))

View File

@ -349,3 +349,40 @@ func TestEncodeSparseArray(t *testing.T) {
t.Fatalf("test failed: %v", err)
}
}
func TestEncodeSortKeys(t *testing.T) {
L := glua.NewState()
defer L.Close()
Preload(L)
err := L.DoString(`
local gjson = require("gjson")
-- Test default (no sorting)
local default_sort = gjson.encode_sort_keys()
assert(default_sort == false, "default should be false")
-- Enable sorting (returns new value)
local new_val = gjson.encode_sort_keys(true)
assert(new_val == true, "should return new value")
assert(gjson.encode_sort_keys() == true, "should be true now")
-- Test sorted output
local data = {c = 3, a = 1, b = 2}
local result = gjson.encode(data)
-- Keys should be in alphabetical order: a, b, c
local a_pos = string.find(result, '"a"')
local b_pos = string.find(result, '"b"')
local c_pos = string.find(result, '"c"')
assert(a_pos < b_pos, "a should come before b")
assert(b_pos < c_pos, "b should come before c")
-- Disable sorting (back to fast mode)
gjson.encode_sort_keys(false)
assert(gjson.encode_sort_keys() == false, "should be false again")
`)
if err != nil {
t.Fatalf("test failed: %v", err)
}
}

View File

@ -49,6 +49,7 @@ func gjsonNew(L *glua.LState) int {
L.SetField(tbl, "decode_max_depth", L.NewFunction(instance.cfgDecodeMaxDepth))
L.SetField(tbl, "encode_number_precision", L.NewFunction(instance.cfgEncodeNumberPrecision))
L.SetField(tbl, "encode_keep_buffer", L.NewFunction(instance.cfgEncodeKeepBuffer))
L.SetField(tbl, "encode_sort_keys", L.NewFunction(instance.cfgEncodeSortKeys))
L.SetField(tbl, "new", L.NewFunction(gjsonNew))
// Set null