fix: allow setting and deleting an empty object key

An empty string is a valid JSON object key and gjson reads it, but set()
rejected an empty path outright, so such a key could neither be changed
nor removed through sjson.

Nothing below that guard needed a non-empty path. parsePath("") already
returns a well-formed simple pathResult and gjson.Get resolves the empty
key. What did break was atoui: its digit loop never runs for an empty
part, so it fell through and reported the empty key as numeric index 0.

That also affected paths the guard never blocked. A nested empty key was
built as an array, so Set(`{"x":1}`, "a.", 2) produced
{"x":1,"a":[2]}, which gjson cannot read back.

Return false from atoui for an empty part and drop the guard. An empty
key on an array now reports the same error as any other non-numeric key
instead of a special-cased one.

Fixes #77
This commit is contained in:
dualfroz committed 2026-09-03 14:27:28 +02:00
1 parent 23241c5ce0
commit 5b70965fcb
2 files changed
+48 -3

No files matched your search

+4 -3
View File
@@ -166,6 +166,10 @@ func atoui(r pathResult) (n int, ok bool) {
if r.force { if r.force {
return 0, false return 0, false
} }
if len(r.part) == 0 {
// An empty key is a valid object key, not the number zero.
return 0, false
}
for i := 0; i < len(r.part); i++ { for i := 0; i < len(r.part); i++ {
if r.part[i] < '0' || r.part[i] > '9' { if r.part[i] < '0' || r.part[i] > '9' {
return 0, false return 0, false
@@ -515,9 +519,6 @@ type sliceHeader struct {
func set(jstr, path, raw string, func set(jstr, path, raw string,
stringify, del, optimistic, inplace bool) ([]byte, error) { stringify, del, optimistic, inplace bool) ([]byte, error) {
if path == "" {
return []byte(jstr), &errorType{"path cannot be empty"}
}
if !del && optimistic && isOptimisticPath(path) { if !del && optimistic && isOptimisticPath(path) {
res := gjson.Get(jstr, path) res := gjson.Get(jstr, path)
if res.Exists() && res.Index > 0 { if res.Exists() && res.Index > 0 {
+44
View File
@@ -351,3 +351,47 @@ func TestIssue61(t *testing.T) {
t.Fail() t.Fail()
} }
} }
func TestEmptyKeyIssue77(t *testing.T) {
const json = `{"name":{"first":"Janet"},"age":47,"":"foo"}`
// An empty key is a valid object key and gjson reads it, so sjson has to
// be able to write and delete it too.
if gjson.Get(json, "").String() != "foo" {
t.Fatalf("gjson cannot read the empty key")
}
testRaw(t, setString, `{"name":{"first":"Janet"},"age":47,"":"bar"}`,
json, "", "bar")
testRaw(t, setDelete, `{"name":{"first":"Janet"},"age":47}`,
json, "", nil)
// setting an empty key that is not there yet adds it
testRaw(t, setString, `{"a":1,"":"bar"}`, `{"a":1}`, "", "bar")
testRaw(t, setString, `{"":"bar"}`, `{}`, "", "bar")
// deleting an empty key that is not there is a no-op, like any other
// missing key
testRaw(t, setDelete, `{"a":1}`, `{"a":1}`, "", nil)
// an empty key is a key, never the array index zero
testRaw(t, setInt, `{"x":1,"a":{"":2}}`, `{"x":1}`, "a.", 2)
testRaw(t, setInt, `{"a":{"b":{"":2}}}`, `{}`, "a.b.", 2)
testRaw(t, setInt, `{"a":{"":{"b":2}}}`, `{}`, "a..b", 2)
// whatever sjson writes for an empty key, gjson has to read back
out, err := Set(`{"x":1}`, "a.", 2)
if err != nil {
t.Fatal(err)
}
if res := gjson.Get(out, "a."); !res.Exists() || res.Int() != 2 {
t.Fatalf("expected gjson to read back '2', got '%v' from '%v'",
res.Raw, out)
}
// an empty key still cannot address an array element, and reports the
// same error as any other non-numeric key
if _, err := Set(`[1,2,3]`, "", "x"); err == nil {
t.Fatalf("expected an error for an empty key on an array")
}
}