From 377d31cf88778ac5ec4610ef877f4beb3e7e9eaf Mon Sep 17 00:00:00 2001 From: "Chris (ChrisJr404)" <11917633+ChrisJr404@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:20:41 -0400 Subject: [PATCH] Add Decoder.DisallowDuplicateFields to reject duplicate object keys --- decode.go | 8 ++++++ decode_test.go | 55 ++++++++++++++++++++++++++++++++++++++ internal/decoder/stream.go | 23 ++++++++-------- internal/decoder/struct.go | 10 ++++++- 4 files changed, 84 insertions(+), 12 deletions(-) diff --git a/decode.go b/decode.go index 74c6ac3b..9ea4eb34 100644 --- a/decode.go +++ b/decode.go @@ -252,6 +252,14 @@ func (d *Decoder) DisallowUnknownFields() { d.s.DisallowUnknownFields = true } +// DisallowDuplicateFields causes the Decoder to return an error when the +// destination is a struct and the input object contains more than one key that +// maps to the same field. By default the last occurrence wins, matching +// encoding/json. +func (d *Decoder) DisallowDuplicateFields() { + d.s.DisallowDuplicateFields = true +} + func (d *Decoder) InputOffset() int64 { return d.s.TotalOffset() } diff --git a/decode_test.go b/decode_test.go index 8f32cedb..33d92d61 100644 --- a/decode_test.go +++ b/decode_test.go @@ -289,6 +289,61 @@ func Test_Decoder_DisallowUnknownFields(t *testing.T) { } } +func Test_Decoder_DisallowDuplicateFields(t *testing.T) { + type nested struct { + B int `json:"b"` + } + type payload struct { + A int `json:"a"` + N nested `json:"n"` + } + + t.Run("duplicate reports an error", func(t *testing.T) { + dec := json.NewDecoder(strings.NewReader(`{"a": 1, "a": 2}`)) + dec.DisallowDuplicateFields() + var v payload + err := dec.Decode(&v) + if err == nil { + t.Fatal("expected duplicate field error") + } + if err.Error() != `json: duplicate field "a"` { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("duplicate in a nested object is caught", func(t *testing.T) { + dec := json.NewDecoder(strings.NewReader(`{"n": {"b": 1, "b": 2}}`)) + dec.DisallowDuplicateFields() + var v payload + if err := dec.Decode(&v); err == nil { + t.Fatal("expected duplicate field error") + } + }) + + t.Run("distinct fields decode normally", func(t *testing.T) { + dec := json.NewDecoder(strings.NewReader(`{"a": 1, "n": {"b": 2}}`)) + dec.DisallowDuplicateFields() + var v payload + if err := dec.Decode(&v); err != nil { + t.Fatal(err) + } + if v.A != 1 || v.N.B != 2 { + t.Fatalf("unexpected value: %+v", v) + } + }) + + t.Run("default keeps last-wins behavior", func(t *testing.T) { + dec := json.NewDecoder(strings.NewReader(`{"a": 1, "a": 2}`)) + var v payload + if err := dec.Decode(&v); err != nil { + t.Fatal(err) + } + if v.A != 2 { + t.Fatalf("expected last value to win, got %d", v.A) + } + }) +} + func Test_Decoder_EmptyObjectWithSpace(t *testing.T) { dec := json.NewDecoder(strings.NewReader(`{"obj":{ }}`)) var v struct { diff --git a/internal/decoder/stream.go b/internal/decoder/stream.go index a383f725..fdc04bbe 100644 --- a/internal/decoder/stream.go +++ b/internal/decoder/stream.go @@ -15,17 +15,18 @@ const ( ) type Stream struct { - buf []byte - bufSize int64 - length int64 - r io.Reader - offset int64 - cursor int64 - filledBuffer bool - allRead bool - UseNumber bool - DisallowUnknownFields bool - Option *Option + buf []byte + bufSize int64 + length int64 + r io.Reader + offset int64 + cursor int64 + filledBuffer bool + allRead bool + UseNumber bool + DisallowUnknownFields bool + DisallowDuplicateFields bool + Option *Option } func NewStream(r io.Reader) *Stream { diff --git a/internal/decoder/struct.go b/internal/decoder/struct.go index 313da153..9565f379 100644 --- a/internal/decoder/struct.go +++ b/internal/decoder/struct.go @@ -688,7 +688,7 @@ func (d *structDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) e seenFieldNum int ) firstWin := (s.Option.Flags & FirstWinOption) != 0 - if firstWin { + if firstWin || s.DisallowDuplicateFields { seenFields = make(map[int]struct{}, d.fieldUniqueNameNum) } for { @@ -720,6 +720,14 @@ func (d *structDecoder) DecodeStream(s *Stream, depth int64, p unsafe.Pointer) e } seenFields[field.fieldIdx] = struct{}{} } + } else if s.DisallowDuplicateFields { + if _, exists := seenFields[field.fieldIdx]; exists { + return fmt.Errorf("json: duplicate field %q", key) + } + if err := field.dec.DecodeStream(s, depth, unsafe.Pointer(uintptr(p)+field.offset)); err != nil { + return err + } + seenFields[field.fieldIdx] = struct{}{} } else { if err := field.dec.DecodeStream(s, depth, unsafe.Pointer(uintptr(p)+field.offset)); err != nil { return err