golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.59k stars 17.61k forks source link

encoding/json: include field name in unmarshal error messages when extracting time.Time #22816

Open ganelon13 opened 6 years ago

ganelon13 commented 6 years ago

allocated from #6716 .

What version of Go are you using (go version)?

go1.9.2 linux/amd64

Does this issue reproduce with the latest release?

yes

What operating system and processor architecture are you using (go env)?

GOARCH="amd64" GOHOSTARCH="amd64" GOHOSTOS="linux" GOOS="linux"

What did you do?

run https://play.golang.org/p/YnlDi-3DMP

If possible, provide a recipe for reproducing the error. A complete runnable program is good. A link on play.golang.org is best.

What did you expect to see?

Field name with error message

What did you see instead?

Error without field name

tniswong commented 6 years ago

One thing I'd like to see here is consistency in the errors returned from json.Unmarshal and it's analog's.

For example, always returning a json.UnmarshalTypeError that perhaps also holds some ref to the underlying error as well as the existing the Value, Type, Offset, Struct and Field information related to the error condition.

Doing this, rather than just bubbling up the underlying error, (in this case a time.ParseError), enables some more consistency to handling unmarshaling errors.

I had a need for this on a project recently and decided do exactly this, so I copied the encoding/json package and made the following changes in decoder.go:

Plus sign in column 0 denotes additions, Minus sign in column 0 denotes subtractions.

// An UnmarshalTypeError describes a JSON value that was
// not appropriate for a value of a specific Go type.
type UnmarshalTypeError struct {
    Value  string       // description of JSON value - "bool", "array", "number -5"
    Type   reflect.Type // type of Go value it could not be assigned to
    Offset int64        // error occurred after reading Offset bytes
    Struct string       // name of the struct type containing the field
    Field  string       // name of the field holding the Go value
+   Err    error        // error, if any which occurred during unmarshal
}
// literalStore decodes a literal stored in item into v.
//
// fromQuoted indicates whether this literal came from unwrapping a
// string from the ",string" struct tag option. this is used only to
// produce more helpful error messages.
func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) {
    // Check for unmarshaler.
    if len(item) == 0 {
        //Empty string given
        d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()))
        return
    }
    isNull := item[0] == 'n' // null
    u, ut, pv := d.indirect(v, isNull)
    if u != nil {
        err := u.UnmarshalJSON(item)
        if err != nil {
-                      d.error(err)
+           var val string
+           switch item[0] {
+           case '"':
+               val = "string"
+           case 'n':
+               val = "null"
+           case 't', 'f':
+               val = "bool"
+           default:
+               val = "number"
+           }
+           d.error(&UnmarshalTypeError{Value: val, Type: v.Type(), Offset: int64(d.off), Err: err})
        }
        return
    }

...

I only had to make a couple small changes in the tests to get them all to pass, though, to be fair, I'm not sure that I have all the possible code paths are covered.

tniswong commented 6 years ago

Here's a few lines added to the array function that adds Field context for arrays.

Plus sign in column 0 denotes additions

// array consumes an array from d.data[d.off-1:], decoding into the value v.
// the first byte of the array ('[') has been read already.
func (d *decodeState) array(v reflect.Value) {
+   arrayField := d.errorContext.Field
    // Check for unmarshaler.
    u, ut, pv := d.indirect(v, false)
    if u != nil {
        d.off--
        err := u.UnmarshalJSON(d.next())
        if err != nil {
            d.error(err)
        }
        return
    }
    if ut != nil {
        d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)})
        d.off--
        d.next()
        return
    }

    v = pv

    // Check type of target.
    switch v.Kind() {
    case reflect.Interface:
        if v.NumMethod() == 0 {
            // Decoding into nil interface?  Switch to non-reflect code.
            v.Set(reflect.ValueOf(d.arrayInterface()))
            return
        }
        // Otherwise it's invalid.
        fallthrough
    default:
        d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)})
        d.off--
        d.next()
        return
    case reflect.Array:
    case reflect.Slice:
        break
    }

    i := 0
    for {
+       d.errorContext.Field = fmt.Sprintf("%s[%v]", arrayField, i)
        // Look ahead for ] - can only happen on first iteration.
        op := d.scanWhile(scanSkipSpace)
        if op == scanEndArray {
            break
        }

        // Back up so d.value can have the byte we just read.
        d.off--
        d.scan.undo(op)

        // Get element of array, growing if necessary.
        if v.Kind() == reflect.Slice {
            // Grow slice if necessary
            if i >= v.Cap() {
                newcap := v.Cap() + v.Cap()/2
                if newcap < 4 {
                    newcap = 4
                }
                newv := reflect.MakeSlice(v.Type(), v.Len(), newcap)
                reflect.Copy(newv, v)
                v.Set(newv)
            }
            if i >= v.Len() {
                v.SetLen(i + 1)
            }
        }

        if i < v.Len() {
            // Decode into element.
            d.value(v.Index(i))
        } else {
            // Ran out of fixed array: skip.
            d.value(reflect.Value{})
        }
        i++

        // Next token must be , or ].
        op = d.scanWhile(scanSkipSpace)
        if op == scanEndArray {
            break
        }
        if op != scanArrayValue {
            d.error(errPhase)
        }
    }

    if i < v.Len() {
        if v.Kind() == reflect.Array {
            // Array. Zero the rest.
            z := reflect.Zero(v.Type().Elem())
            for ; i < v.Len(); i++ {
                v.Index(i).Set(z)
            }
        } else {
            v.SetLen(i)
        }
    }
    if i == 0 && v.Kind() == reflect.Slice {
        v.Set(reflect.MakeSlice(v.Type(), 0, 0))
    }

+   d.errorContext.Field = arrayField

}
almas1992 commented 3 years ago

Here's a few lines added to the array function that adds Field context for arrays.

Plus sign in column 0 denotes additions

// array consumes an array from d.data[d.off-1:], decoding into the value v.
// the first byte of the array ('[') has been read already.
func (d *decodeState) array(v reflect.Value) {
+ arrayField := d.errorContext.Field
  // Check for unmarshaler.
  u, ut, pv := d.indirect(v, false)
  if u != nil {
      d.off--
      err := u.UnmarshalJSON(d.next())
      if err != nil {
          d.error(err)
      }
      return
  }
  if ut != nil {
      d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)})
      d.off--
      d.next()
      return
  }

  v = pv

  // Check type of target.
  switch v.Kind() {
  case reflect.Interface:
      if v.NumMethod() == 0 {
          // Decoding into nil interface?  Switch to non-reflect code.
          v.Set(reflect.ValueOf(d.arrayInterface()))
          return
      }
      // Otherwise it's invalid.
      fallthrough
  default:
      d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)})
      d.off--
      d.next()
      return
  case reflect.Array:
  case reflect.Slice:
      break
  }

  i := 0
  for {
+     d.errorContext.Field = fmt.Sprintf("%s[%v]", arrayField, i)
      // Look ahead for ] - can only happen on first iteration.
      op := d.scanWhile(scanSkipSpace)
      if op == scanEndArray {
          break
      }

      // Back up so d.value can have the byte we just read.
      d.off--
      d.scan.undo(op)

      // Get element of array, growing if necessary.
      if v.Kind() == reflect.Slice {
          // Grow slice if necessary
          if i >= v.Cap() {
              newcap := v.Cap() + v.Cap()/2
              if newcap < 4 {
                  newcap = 4
              }
              newv := reflect.MakeSlice(v.Type(), v.Len(), newcap)
              reflect.Copy(newv, v)
              v.Set(newv)
          }
          if i >= v.Len() {
              v.SetLen(i + 1)
          }
      }

      if i < v.Len() {
          // Decode into element.
          d.value(v.Index(i))
      } else {
          // Ran out of fixed array: skip.
          d.value(reflect.Value{})
      }
      i++

      // Next token must be , or ].
      op = d.scanWhile(scanSkipSpace)
      if op == scanEndArray {
          break
      }
      if op != scanArrayValue {
          d.error(errPhase)
      }
  }

  if i < v.Len() {
      if v.Kind() == reflect.Array {
          // Array. Zero the rest.
          z := reflect.Zero(v.Type().Elem())
          for ; i < v.Len(); i++ {
              v.Index(i).Set(z)
          }
      } else {
          v.SetLen(i)
      }
  }
  if i == 0 && v.Kind() == reflect.Slice {
      v.Set(reflect.MakeSlice(v.Type(), 0, 0))
  }

+ d.errorContext.Field = arrayField

}

Have you tried to submit PR for this change?

fatelei commented 2 years ago

i can submit a pr for this issue