When a type implements both json.Marshaler and encoding.TextMarshaler, which does encoding/json use?
answer
- an ordered chain, resolved once per type
- the JSON-shaped method is checked first
- one hook returns text, the other returns a value
- only one of them can emit a JSON number
- the encoder does the quoting for the text hook
basics
~10 sjson.Marshaler wins: encoding/json calls MarshalJSON and writes its bytes, which must form a complete valid JSON value. MarshalText is only the fallback, and its output is always quoted and escaped into a JSON string.
solid answer
~40 s`encoding/json` decides once per type, in a fixed order: `json.Marshaler` first, `encoding.TextMarshaler` second, structural encoding last. If `MarshalJSON` exists it is used and `MarshalText` is never called for that type. The two hooks are not equivalent in power: `MarshalJSON` returns a complete JSON *value* — it may be an object, an array, a number, a bare `null` — and the encoder validates and compacts it, failing the whole marshal with a `json.MarshalerError` if the bytes are not valid JSON. `MarshalText` returns plain text, and the encoder is the one that quotes and escapes it, so a text marshaler can only ever produce a JSON string. Neither method is visible to `fmt`, which looks for `fmt.Formatter`, `error` and `fmt.Stringer` and nothing else.
code
go · 8 linestype Tag string
func (t Tag) String() string { return "tag:" + string(t) }
func (t Tag) MarshalText() ([]byte, error) { return []byte("T-" + string(t)), nil }
// fmt.Println(Tag("blue")) prints: tag:blue
// json.Marshal(Tag("blue")) gives: "T-blue"go deeper
Know that encoding/json looks for its own marshal methods and checks them in a fixed order, and that neither of them is the String method used for printing.
Explain the chain and the difference in power: one hook returns a complete JSON value that the encoder validates and compacts, the other returns text that the encoder quotes and escapes into a JSON string.
Show you would pick the smallest sufficient hook, and that you test custom marshalling against quotes, backslashes and non-ASCII input, because invalid bytes fail the entire marshal at run time.
Own the API consequence: the JSON shape a type emits is a contract other services parse, so decide deliberately whether a value is a string or a structured value, and keep that decision reviewable.
## The order the encoder resolves The first time `encoding/json` encounters a type, it works out how to encode it and caches that decision. The order is: 1. Does the type implement `json.Marshaler` — `MarshalJSON() ([]byte, error)`? Use it. 2. Otherwise, does it implement `encoding.TextMarshaler` — `MarshalText() ([]byte, error)`? Use it. 3. Otherwise, encode structurally by kind: struct to object, map to object, slice/array to array, string to JSON string, numbers to JSON numbers, and so on. Because it is an ordered chain and not a merge, a type carrying both methods only ever exercises the first. That matters when a type gains `MarshalText` for some other reason and someone expects it to affect JSON — it will not, as long as `MarshalJSON` is present. ## The two hooks differ in what they are allowed to produce This is the part candidates most often miss. `MarshalJSON` returns raw bytes that are spliced in as the encoded value. The encoder validates and compacts them; if they are not a syntactically valid JSON value, `json.Marshal` fails, returning a `*json.MarshalerError` that wraps the underlying syntax error and names the offending type. Because the bytes are a whole JSON value, the method can produce any shape at all: an object, an array, a number, a string, `true`, or `null`. `MarshalText` cannot. Its return value is text, and the encoder writes it through the same path as any Go string: surrounded by double quotes, with quotes, backslashes and control characters escaped. So the *only* JSON shape a text marshaler can yield is a string. Returning `[]byte("42")` from `MarshalText` produces `"42"`, not `42`. Returning bytes containing a quote character produces a correctly escaped string, not broken JSON — the encoder owns the escaping, so a text marshaler cannot corrupt the document, whereas a `MarshalJSON` that emits garbage will fail the marshal. ## Where `fmt` sits in all this Nowhere. `fmt` has its own, shorter chain: `fmt.Formatter` if present (for every verb), then, for `%v`, `%s`, `%q`, `%x` and `%X`, `error` and then `fmt.Stringer`. It does not know `MarshalText` or `MarshalJSON` exist. So the three interfaces divide cleanly by consumer: | Interface | Method | Consulted by | Produces | |---|---|---|---| | `fmt.Stringer` | `String() string` | `fmt` (and anything formatting through it) | human-readable text | | `encoding.TextMarshaler` | `MarshalText() ([]byte, error)` | `encoding/json` and other encoders | a JSON string (quoting done by the encoder) | | `json.Marshaler` | `MarshalJSON() ([]byte, error)` | `encoding/json` only | any JSON value | A type may implement all three, with three different renderings, and each consumer picks the one addressed to it. ## Choosing between them, as the author `MarshalText` is the smaller promise and usually the better default for a scalar-shaped type such as a string-backed enum or an identifier: it is one short method, the encoder handles quoting and escaping for you, and the same method is reused by other encoders that understand the `encoding` interfaces, so one implementation covers more than JSON. Reach for `MarshalJSON` when the JSON shape genuinely is not a string — when a value must appear as a number, an array, or an object with fields that do not match the Go struct. The cost of `MarshalJSON` is that you are now responsible for emitting valid JSON, including escaping, and a mistake surfaces as a marshalling failure at run time rather than a compile error. That is a real review point: any hand-built `MarshalJSON` should have a test that marshals a value containing a quote, a backslash and a non-ASCII rune. ## Diagnosing a surprise If a value serialises in an unexpected shape, walk the chain in order rather than guessing. Ask whether the type — or an embedded type it promotes methods from — has a `MarshalJSON`; if it does, that is the answer and nothing else is consulted. If not, ask about `MarshalText`. Only then look at struct tags and field structure. Working the chain top-down turns "why is my field a string?" into a two-minute answer instead of a debugging session.
- What happens if MarshalJSON returns bytes that are not valid JSON?The encoder validates and compacts the returned bytes, so the whole `json.Marshal` call fails with a `*json.MarshalerError` wrapping the syntax error and naming the type. Nothing partial is written for that value. This is why a hand-built `MarshalJSON` needs a test with a quote, a backslash and a multi-byte rune in the data.
- Can a type's MarshalText make its JSON a number rather than a string?No. The encoder writes a text marshaler's output through its string path — quoted and escaped — so the result is always a JSON string. `[]byte("42")` becomes `"42"`. Producing a bare JSON number requires the JSON-specific hook, which returns a complete JSON value.
- Does fmt ever call MarshalText or MarshalJSON?No. `fmt` knows only `fmt.Formatter`, `error` and `fmt.Stringer`. A type with `MarshalText` but no `String` method prints with default formatting, so a value can look raw in a log line and still be nicely encoded in a payload.
saying these in an interview costs you the question
- Says the text hook wins because it is more specific
- Thinks MarshalText output is inserted unquoted
- Believes MarshalText can emit a JSON number or object
- Assumes both methods are called and merged
- Claims fmt falls back to MarshalText when String is absent