Repository: go
Issue: encoding/json: confusing errors when unmarshaling custom types
Labels: NeedsDecision

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):

``` go
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?
}
```