Skip to content

Commit

Permalink
store err type and path separately in strict errors
Browse files Browse the repository at this point in the history
  • Loading branch information
kevindelgado committed Jul 12, 2022
1 parent 227cbc7 commit fc711c9
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 14 deletions.
12 changes: 6 additions & 6 deletions internal/golang/encoding/json/decode.go
Expand Up @@ -232,7 +232,7 @@ type decodeState struct {
useNumber bool
disallowUnknownFields bool

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

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]interface{} {

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

Expand Down
57 changes: 51 additions & 6 deletions internal/golang/encoding/json/kubernetes_patch.go
Expand Up @@ -18,7 +18,7 @@ package json

import (
gojson "encoding/json"
"fmt"
"errors"
"strconv"
"strings"
)
Expand Down Expand Up @@ -71,17 +71,23 @@ 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
Expand Down Expand Up @@ -118,10 +124,29 @@ func (d *decodeState) appendStrictFieldStackIndex(i int) {
d.strictFieldStack = append(d.strictFieldStack, "[", strconv.Itoa(i), "]")
}

type strictErrType string

const (
unknownStrictErrType strictErrType = "unknown field"
duplicateStrictErrType = "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) + ` "` + e.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 {
Errors []error
Errors []StrictError
}

func (e *UnmarshalStrictError) Error() string {
Expand All @@ -135,3 +160,23 @@ func (e *UnmarshalStrictError) Error() string {
}
return b.String()
}

// FullErrors returns all the errors with each containing the prefix
// indicating what type of strict error it is.
func (e *UnmarshalStrictError) FullErrors() []error {
errs := make([]error, len(e.Errors))
for i, err := range e.Errors {
errs[i] = errors.New(err.Error())
}
return errs
}

// PathOnlyErrors returns the errors as only the path to the
// erroneous field, leaving off the error type prefix.
func (e *UnmarshalStrictError) PathOnlyErrors() []error {
errs := make([]error, len(e.Errors))
for i, err := range e.Errors {
errs[i] = errors.New(err.Path)
}
return errs
}
18 changes: 16 additions & 2 deletions json.go
Expand Up @@ -75,6 +75,11 @@ const (

// DisallowUnknownFields returns strict errors if data contains unknown fields when decoding into typed structs
DisallowUnknownFields StrictOption = 2

// ReturnPathsOnly ensure that any strict errors are returned as just the
// path without the error type prefix. This is used during the validation of
// embedded objects to allow strict decoding error paths to be chained together.
ReturnPathsOnly StrictOption = 3
)

// UnmarshalStrict parses the JSON-encoded data and stores the result in the value pointed to by v.
Expand All @@ -94,7 +99,11 @@ const (
// For example, if duplicate fields are present, they will be parsed and stored in v,
// and errors about the duplicate fields will be returned in the strict error list.
func UnmarshalStrict(data []byte, v interface{}, strictOptions ...StrictOption) (strictErrors []error, err error) {
if len(strictOptions) == 0 {
var returnPathsOnly bool
if len(strictOptions) == 1 && strictOptions[0] == ReturnPathsOnly {
returnPathsOnly = true
}
if len(strictOptions) == 0 || returnPathsOnly == true {
err = internaljson.Unmarshal(data, v,
// options matching UnmarshalCaseSensitivePreserveInts
internaljson.CaseSensitive,
Expand All @@ -113,6 +122,8 @@ func UnmarshalStrict(data []byte, v interface{}, strictOptions ...StrictOption)
opts = append(opts, internaljson.DisallowDuplicateFields)
case DisallowUnknownFields:
opts = append(opts, internaljson.DisallowUnknownFields)
case ReturnPathsOnly:
returnPathsOnly = true
default:
return nil, fmt.Errorf("unknown strict option %d", strictOpt)
}
Expand All @@ -121,7 +132,10 @@ func UnmarshalStrict(data []byte, v interface{}, strictOptions ...StrictOption)
}

if strictErr, ok := err.(*internaljson.UnmarshalStrictError); ok {
return strictErr.Errors, nil
if returnPathsOnly {
return strictErr.PathOnlyErrors(), nil
}
return strictErr.FullErrors(), nil
}
return nil, err
}
Expand Down

0 comments on commit fc711c9

Please sign in to comment.