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)

Commits related to this issue

Most upvoted comments

Smaller repro for the last case:

type T int

func (*T) UnmarshalJSON(b []byte) error {
        return nil
}

func main() {
        data := []byte(`{"foo":"bar"}`)
        var m map[T]string
        fmt.Println(json.Unmarshal(data, &m))
}

Not having carefully read the encoding/json docs, it’s understandable if a user sees the error json: cannot unmarshal number foo into Go value of type main.T and thinks “that doesn’t make any sense, I implement UnmarshalJSON”.