From 5b70965fcbbf43faa99c59c7cebc6ac9d0e89ab5 Mon Sep 17 00:00:00 2001 From: dualfroz Date: Thu, 3 Sep 2026 14:27:28 +0200 Subject: [PATCH] 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 --- sjson.go | 7 ++++--- sjson_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/sjson.go b/sjson.go index 9cf3f93..ff34826 100644 --- a/sjson.go +++ b/sjson.go @@ -166,6 +166,10 @@ func atoui(r pathResult) (n int, ok bool) { if r.force { 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++ { if r.part[i] < '0' || r.part[i] > '9' { return 0, false @@ -515,9 +519,6 @@ type sliceHeader struct { func set(jstr, path, raw string, stringify, del, optimistic, inplace bool) ([]byte, error) { - if path == "" { - return []byte(jstr), &errorType{"path cannot be empty"} - } if !del && optimistic && isOptimisticPath(path) { res := gjson.Get(jstr, path) if res.Exists() && res.Index > 0 { diff --git a/sjson_test.go b/sjson_test.go index aa3d968..f0c5373 100644 --- a/sjson_test.go +++ b/sjson_test.go @@ -351,3 +351,47 @@ func TestIssue61(t *testing.T) { 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") + } +}