Go

You implemented MarshalJSON but it isn't called. Why? Explain receiver rules and the recursion trap.

Question 453HardGo 1.22 to 1.25

If MarshalJSON has a pointer receiver, only *T implements json.Marshaler. Marshaling a non-addressable T value (e.g. json.Marshal(t), a value in a map, or a value field of a value passed in) skips your method. Use a value receiver for MarshalJSON; use a pointer receiver for UnmarshalJSON (it must mutate).

Recursion trap: calling json.Marshal(t) inside T.MarshalJSON recurses infinitely (stack overflow). Use a local type alias that has the same fields but no methods:

type Money struct {
	Cents    int64
	Currency string
}

func (m Money) MarshalJSON() ([]byte, error) {
	type plain Money // same fields, no methods
	return json.Marshal(struct {
		plain
		Display string `json:"display"`
	}{plain(m), fmt.Sprintf("%.2f %s", float64(m.Cents)/100, m.Currency)})
}

func (m *Money) UnmarshalJSON(b []byte) error {
	type plain Money
	var p plain
	if err := json.Unmarshal(b, &p); err != nil {
		return err
	}
	if p.Currency == "" {
		return errors.New("currency required")
	}
	*m = Money(p)
	return nil
}

Also note encoding.TextMarshaler is used for map keys and as a fallback, and a nil pointer with a value-receiver MarshalJSON encodes as null without calling the method.

More on Standard Library, HTTP & Systems Design in Go

All 35 Standard Library, HTTP & Systems Design in Go questions