diff --git a/json.go b/json.go index b3cadf4b7..2b9fa527e 100644 --- a/json.go +++ b/json.go @@ -82,6 +82,7 @@ func (cmd *JSONCmd) SetVal(val string) { cmd.val = val } +// Val returns the result of the JSON.GET command as a string. func (cmd *JSONCmd) Val() string { if len(cmd.val) == 0 && cmd.expanded != nil { val, err := json.Marshal(cmd.expanded) @@ -100,6 +101,7 @@ func (cmd *JSONCmd) Result() (string, error) { return cmd.Val(), cmd.Err() } +// Expanded returns the result of the JSON.GET command as unmarshalled JSON. func (cmd *JSONCmd) Expanded() (interface{}, error) { if len(cmd.val) != 0 && cmd.expanded == nil { err := json.Unmarshal([]byte(cmd.val), &cmd.expanded) @@ -113,11 +115,17 @@ func (cmd *JSONCmd) Expanded() (interface{}, error) { func (cmd *JSONCmd) readReply(rd *proto.Reader) error { // nil response from JSON.(M)GET (cmd.baseCmd.err will be "redis: nil") + // This happens when the key doesn't exist if cmd.baseCmd.Err() == Nil { cmd.val = "" return Nil } + // Handle other base command errors + if cmd.baseCmd.Err() != nil { + return cmd.baseCmd.Err() + } + if readType, err := rd.PeekReplyType(); err != nil { return err } else if readType == proto.RespArray { @@ -127,6 +135,13 @@ func (cmd *JSONCmd) readReply(rd *proto.Reader) error { return err } + // Empty array means no results found for JSON path, but key exists + // This should return "[]", not an error + if size == 0 { + cmd.val = "[]" + return nil + } + expanded := make([]interface{}, size) for i := 0; i < size; i++ { @@ -141,6 +156,7 @@ func (cmd *JSONCmd) readReply(rd *proto.Reader) error { return err } else if str == "" || err == Nil { cmd.val = "" + return Nil } else { cmd.val = str } diff --git a/json_test.go b/json_test.go index 9139be3ac..f68625c2c 100644 --- a/json_test.go +++ b/json_test.go @@ -142,6 +142,10 @@ var _ = Describe("JSON Commands", Label("json"), func() { resArr, err := client.JSONArrIndex(ctx, "doc1", "$.store.book[?(@.price<10)].size", 20).Result() Expect(err).NotTo(HaveOccurred()) Expect(resArr).To(Equal([]int64{1, 2})) + + _, err = client.JSONGet(ctx, "this-key-does-not-exist", "$").Result() + Expect(err).To(HaveOccurred()) + Expect(err).To(BeIdenticalTo(redis.Nil)) }) It("should JSONArrInsert", Label("json.arrinsert", "json"), func() { @@ -402,8 +406,8 @@ var _ = Describe("JSON Commands", Label("json"), func() { Expect(cmd2.Val()).To(Equal(int64(1))) cmd3 := client.JSONGet(ctx, "del1", "$") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(HaveLen(0)) + Expect(cmd3.Err()).To(Equal(redis.Nil)) + Expect(cmd3.Val()).To(Equal("")) }) It("should JSONDel with $", Label("json.del", "json"), func() { @@ -673,6 +677,58 @@ var _ = Describe("JSON Commands", Label("json"), func() { Expect(cmd2.Val()[0]).To(Or(Equal([]interface{}{"boolean"}), Equal("boolean"))) }) }) + + Describe("JSON Nil Handling", func() { + It("should return redis.Nil for non-existent key", func() { + _, err := client.JSONGet(ctx, "non-existent-key", "$").Result() + Expect(err).To(Equal(redis.Nil)) + }) + + It("should return empty array for non-existent path in existing key", func() { + err := client.JSONSet(ctx, "test-key", "$", `{"a": 1, "b": "hello"}`).Err() + Expect(err).NotTo(HaveOccurred()) + + // Non-existent path should return empty array, not error + val, err := client.JSONGet(ctx, "test-key", "$.nonexistent").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("[]")) + }) + + It("should distinguish empty array from non-existent path", func() { + err := client.JSONSet(ctx, "test-key", "$", `{"arr": [], "obj": {}}`).Err() + Expect(err).NotTo(HaveOccurred()) + + // Empty array should return the array + val, err := client.JSONGet(ctx, "test-key", "$.arr").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("[[]]")) + + // Non-existent field should return empty array + val, err = client.JSONGet(ctx, "test-key", "$.missing").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("[]")) + }) + + It("should handle multiple paths with mixed results", func() { + err := client.JSONSet(ctx, "test-key", "$", `{"a": 1, "b": 2}`).Err() + Expect(err).NotTo(HaveOccurred()) + + // Path that exists + val, err := client.JSONGet(ctx, "test-key", "$.a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("[1]")) + + // Path that doesn't exist should return empty array + val, err = client.JSONGet(ctx, "test-key", "$.c").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("[]")) + }) + + AfterEach(func() { + // Clean up test keys + client.Del(ctx, "test-key", "non-existent-key") + }) + }) } })