Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
55 changes: 55 additions & 0 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 12 additions & 11 deletions internal/decoder/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 9 additions & 1 deletion internal/decoder/struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down