Skip to content
Merged
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
12 changes: 6 additions & 6 deletions internal/golang/encoding/json/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ type decodeState struct {
disallowUnknownFields bool

savedStrictErrors []error
seenStrictErrors map[string]struct{}
seenStrictErrors map[strictError]struct{}
strictFieldStack []string

caseSensitive bool
Expand Down Expand Up @@ -695,7 +695,7 @@ func (d *decodeState) object(v reflect.Value) error {
seenKeys = map[string]struct{}{}
}
if _, seen := seenKeys[fieldName]; seen {
d.saveStrictError(d.newFieldError("duplicate field", fieldName))
d.saveStrictError(d.newFieldError(duplicateStrictErrType, fieldName))
} else {
seenKeys[fieldName] = struct{}{}
}
Expand All @@ -711,7 +711,7 @@ func (d *decodeState) object(v reflect.Value) error {
var seenKeys uint64
checkDuplicateField = func(fieldNameIndex int, fieldName string) {
if seenKeys&(1<<fieldNameIndex) != 0 {
d.saveStrictError(d.newFieldError("duplicate field", fieldName))
d.saveStrictError(d.newFieldError(duplicateStrictErrType, fieldName))
} else {
seenKeys = seenKeys | (1 << fieldNameIndex)
}
Expand All @@ -724,7 +724,7 @@ func (d *decodeState) object(v reflect.Value) error {
seenIndexes = make([]bool, len(fields.list))
}
if seenIndexes[fieldNameIndex] {
d.saveStrictError(d.newFieldError("duplicate field", fieldName))
d.saveStrictError(d.newFieldError(duplicateStrictErrType, fieldName))
} else {
seenIndexes[fieldNameIndex] = true
}
Expand Down Expand Up @@ -836,7 +836,7 @@ func (d *decodeState) object(v reflect.Value) error {
d.errorContext.Struct = t
d.appendStrictFieldStackKey(f.name)
} else if d.disallowUnknownFields {
d.saveStrictError(d.newFieldError("unknown field", string(key)))
d.saveStrictError(d.newFieldError(unknownStrictErrType, string(key)))
}
}

Expand Down Expand Up @@ -1231,7 +1231,7 @@ func (d *decodeState) objectInterface() map[string]any {

if d.disallowDuplicateFields {
if _, exists := m[key]; exists {
d.saveStrictError(d.newFieldError("duplicate field", key))
d.saveStrictError(d.newFieldError(duplicateStrictErrType, key))
}
}

Expand Down
49 changes: 40 additions & 9 deletions internal/golang/encoding/json/kubernetes_patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package json

import (
gojson "encoding/json"
"fmt"
"strconv"
"strings"
)
Expand Down Expand Up @@ -71,32 +70,37 @@ func (d *Decoder) DisallowDuplicateFields() {
d.d.disallowDuplicateFields = true
}

func (d *decodeState) newFieldError(msg, field string) error {
func (d *decodeState) newFieldError(errType strictErrType, field string) *strictError {
if len(d.strictFieldStack) > 0 {
return fmt.Errorf("%s %q", msg, strings.Join(d.strictFieldStack, "")+"."+field)
return &strictError{
ErrType: errType,
Path: strings.Join(d.strictFieldStack, "") + "." + field,
}
} else {
return fmt.Errorf("%s %q", msg, field)
return &strictError{
ErrType: errType,
Path: field,
}
}
}

// saveStrictError saves a strict decoding error,
// for reporting at the end of the unmarshal if no other errors occurred.
func (d *decodeState) saveStrictError(err error) {
func (d *decodeState) saveStrictError(err *strictError) {
// prevent excessive numbers of accumulated errors
if len(d.savedStrictErrors) >= 100 {
return
}
// dedupe accumulated strict errors
if d.seenStrictErrors == nil {
d.seenStrictErrors = map[string]struct{}{}
d.seenStrictErrors = map[strictError]struct{}{}
}
msg := err.Error()
if _, seen := d.seenStrictErrors[msg]; seen {
if _, seen := d.seenStrictErrors[*err]; seen {
return
}

// accumulate the error
d.seenStrictErrors[msg] = struct{}{}
d.seenStrictErrors[*err] = struct{}{}
d.savedStrictErrors = append(d.savedStrictErrors, err)
}

Expand All @@ -118,6 +122,33 @@ func (d *decodeState) appendStrictFieldStackIndex(i int) {
d.strictFieldStack = append(d.strictFieldStack, "[", strconv.Itoa(i), "]")
}

type strictErrType string

const (
unknownStrictErrType strictErrType = "unknown field"
duplicateStrictErrType strictErrType = "duplicate field"
)

// strictError is a strict decoding error
// It has an ErrType (either unknown or duplicate)
// and a path to the erroneous field
type strictError struct {
ErrType strictErrType
Path string
}

func (e *strictError) Error() string {
return string(e.ErrType) + " " + strconv.Quote(e.Path)
}

func (e *strictError) FieldPath() string {
return e.Path
}

func (e *strictError) SetFieldPath(path string) {
e.Path = path
}

// UnmarshalStrictError holds errors resulting from use of strict disallow___ decoder directives.
// If this is returned from Unmarshal(), it means the decoding was successful in all other respects.
type UnmarshalStrictError struct {
Expand Down
11 changes: 11 additions & 0 deletions json.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ const (
// and a list of the strict failures (if any) are returned. If no `strictOptions` are selected,
// all supported strict checks are performed.
//
// Strict errors returned will implement the FieldError interface for the specific erroneous fields.
//
// Currently supported strict checks are:
// - DisallowDuplicateFields: ensure the data contains no duplicate fields
// - DisallowUnknownFields: ensure the data contains no unknown fields (when decoding into typed structs)
Expand Down Expand Up @@ -137,3 +139,12 @@ func SyntaxErrorOffset(err error) (isSyntaxError bool, offset int64) {
return false, 0
}
}

// FieldError is an error that provides access to the path of the erroneous field
type FieldError interface {
error
// FieldPath provides the full path of the erroneous field within the json object.
FieldPath() string
// SetFieldPath updates the path of the erroneous field output in the error message.
SetFieldPath(path string)
}
6 changes: 5 additions & 1 deletion json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,11 @@ func TestUnmarshal(t *testing.T) {
t.Fatalf("expected %d strict errors, got %v", len(tc.expectStrictErrs), strictErrors)
}
for i := range tc.expectStrictErrs {
if !strings.Contains(strictErrors[i].Error(), tc.expectStrictErrs[i]) {
strictFieldErr, ok := strictErrors[i].(FieldError)
if !ok {
t.Fatalf("strict error does not implement FieldError: %v", strictErrors[i])
}
if !strings.Contains(strictFieldErr.Error(), tc.expectStrictErrs[i]) {
t.Fatalf("expected strict errors:\n %s\ngot:\n %v", strings.Join(tc.expectStrictErrs, "\n "), strictErrors)
}
}
Expand Down