go: encoding/json: confusing errors when unmarshaling custom types
In go1.11
There is a minor issue with custom type unmarshaling using “encoding/json”. As far as I understand the documentation (and also looking through the internal code), the requirements are:
- when processing map keys, the custom type needs to support UnmarshalText
- when processing values, the custom type needs to support UnmarshalJSON
When both of these interfaces are supported, everything is fine. Otherwise, the produced error messages are a bit cryptic and in some cases really confusing. I believe, adding a test for Implements(jsonUnmarshallerType)
inside encoding/json/decode.go: func (d *decodeState) object(v reflect.Value)
will make things more consistent.
Here is the code (try commenting out UnmarshalText
and/or/xor UnmarshalJSON
methods):
package main
import (
"encoding/json"
"fmt"
)
type Enum int
const (
Enum1 = Enum(iota + 1)
Enum2
)
func (enum Enum) String() string {
switch enum {
case Enum1: return "Enum1"
case Enum2: return "Enum2"
default: return "<INVALID ENUM>"
}
}
func (enum *Enum) unmarshal(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil { return err }
switch s {
case "ONE": *enum = Enum1
case "TWO": *enum = Enum2
default: return fmt.Errorf("Invalid Enum value '%s'", s)
}
return nil
}
func (enum *Enum) UnmarshalText(b []byte) error {
return enum.unmarshal(b)
}
func (enum *Enum) UnmarshalJSON(b []byte) error {
return enum.unmarshal(b)
}
func main() {
data := []byte(`{"ONE":"ONE", "TWO":"TWO"}`)
var ss map[string]string
err := json.Unmarshal(data, &ss)
if err != nil { fmt.Println("ss failure:", err) } else { fmt.Println("ss success:", ss) }
var se map[string]Enum
err = json.Unmarshal(data, &se)
if err != nil { fmt.Println("se failure:", err) } else { fmt.Println("se success:", se) }
var es map[Enum]string
err = json.Unmarshal(data, &es)
if err != nil { fmt.Println("es failure:", err) } else { fmt.Println("es success:", es) }
var ee map[Enum]Enum
err = json.Unmarshal(data, &ee)
if err != nil { fmt.Println("ee failure:", err) } else { fmt.Println("ee success:", ee) }
// Output when both UnmarshalText and UnmarshalJSON are defined:
// ss success: map[ONE:ONE TWO:TWO]
// se success: map[ONE:Enum1 TWO:Enum2]
// es success: map[Enum1:ONE Enum2:TWO]
// ee success: map[Enum1:Enum1 Enum2:Enum2]
// Output when UnmarshalJSON is commented out:
// ss success: map[ONE:ONE TWO:TWO]
// se failure: invalid character 'T' looking for beginning of value
// es failure: invalid character 'O' looking for beginning of value
// ee failure: invalid character 'T' looking for beginning of value
// Output when UnmarshalText is commented out:
// ss success: map[ONE:ONE TWO:TWO]
// se success: map[ONE:Enum1 TWO:Enum2]
// es failure: json: cannot unmarshal number ONE into Go value of type main.Enum
// ee failure: json: cannot unmarshal number ONE into Go value of type main.Enum
// In more complex cases, having UnmarshalText undefined also produced this
// error message: JSON decoder out of sync - data changing underfoot?
}
About this issue
- Original URL
- State: open
- Created 6 years ago
- Comments: 18 (11 by maintainers)
Smaller repro for the last case:
Not having carefully read the
encoding/json
docs, it’s understandable if a user sees the errorjson: cannot unmarshal number foo into Go value of type main.T
and thinks “that doesn’t make any sense, I implementUnmarshalJSON
”.