skip to content

Custom Marshalers and RawMessage

MarshalJSON and UnmarshalJSON let a type own its wire form, and the receiver you put them on decides whether they run at all. RawMessage defers a decode you cannot do yet.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

In Go, what does implementing MarshalJSON on a type change about how encoding/json encodes it?

level: juniorimportance: must knowfreq 62%

answer

  1. one method takes over the encoder
  2. an interface encoding/json checks first
  3. it returns bytes, not a string
  4. signature ends in ([]byte, error)

basics

~10 s

Implementing MarshalJSON() ([]byte, error) makes a type satisfy json.Marshaler, so encoding/json calls that method instead of walking the type's fields, and splices the JSON bytes it returns into the output.

solid answer

~50 s

`encoding/json` normally encodes a value reflectively, field by field, honouring struct tags. If the type implements `json.Marshaler` — a single method, `MarshalJSON() ([]byte, error)` — the encoder calls that method and uses whatever it returns in place of the reflective walk, so the type fully controls its own wire representation and its struct tags stop mattering. The bytes returned must be one valid JSON value: the encoder compacts and validates them, and invalid output fails the whole `json.Marshal` call with an error naming the type. The mirror image is `json.Unmarshaler`, `UnmarshalJSON([]byte) error`, which is handed the raw bytes of that value and must populate the receiver, so it has to be declared on a pointer receiver. Typical uses are an enum written as a lowercase string, a domain type flattened to one scalar, or a struct whose wire shape differs from its Go shape.

code

go · 15 lines
go
type Status int

const (
	Pending Status = iota
	Active
)

var statusNames = [...]string{"pending", "active"}

func (s Status) MarshalJSON() ([]byte, error) {
	if int(s) >= len(statusNames) {
		return nil, fmt.Errorf("unknown status %d", int(s))
	}
	return json.Marshal(statusNames[s])
}

go deeper

for a junior

Be ready to state the exact signature, MarshalJSON() ([]byte, error), and to say plainly that encoding/json then uses your bytes instead of the struct's fields. Name UnmarshalJSON([]byte) error as the decoding partner.

for a middle

Explain that the encoder checks for json.Marshaler before its reflective walk, that the bytes you return are compacted and validated, and that struct tags on that type stop being consulted entirely.

for a senior

Show judgment about when a custom marshaler beats a separate wire struct, and how you pin the behaviour down: a round-trip test plus a golden JSON fixture, since no compiler checks a wire format.

for a principal

Own the blast radius. A MarshalJSON added to a widely imported type changes every payload every service produces from it, with no call site mentioning the change. Decide whether that representation belongs on the domain type or on a wire type you can version independently.

## What the encoder does by default `json.Marshal(v)` takes an `any`, inspects the dynamic type with reflection and builds an encoder for it: a struct becomes a JSON object whose member names come from each exported field's name or its `json:"..."` tag, a slice becomes an array, a map becomes an object, and the basic kinds become JSON scalars. Unexported fields are skipped because reflection cannot read them. Before it does any of that, the encoder asks one question: **does this type implement `json.Marshaler`?** ```go type Marshaler interface { MarshalJSON() ([]byte, error) } type Unmarshaler interface { UnmarshalJSON([]byte) error } ``` If the answer is yes, the reflective walk for that value is abandoned entirely. The encoder calls `MarshalJSON`, and the bytes that come back become the encoding of that value. ## What "instead of" really means This is total, not additive. Once a type has `MarshalJSON`: - its `json` struct tags are no longer consulted for that type — nothing reads them, because nothing walks the fields; - adding a field to the struct does **not** add it to the JSON, which is the most common surprise when someone else's type has a custom marshaler; - the method decides everything: names, order, nesting, whether the value is an object at all. A `Temperature float64` can encode as `"21.5C"`, and a whole struct can encode as a single string. ## The contract on the bytes you return The method must return **one complete, valid JSON value** — an object, array, string, number, boolean, or `null`. Not a fragment such as `"a":1`, and not two values. The encoder does not trust you: it compacts the bytes (interior whitespace and newlines are removed) and validates them as it copies them into the output stream. If they are not valid JSON, `json.Marshal` returns an error that names the type, of the form `json: error calling MarshalJSON for type T`. Any error you return yourself is wrapped the same way, so returning an error from the method aborts the entire encode — there is no partial output. Two small behaviours worth knowing. By default `json.Marshal` HTML-escapes `<`, `>` and `&` in the stream, and that escaping is applied to the bytes your marshaler returned too. And a `nil` pointer whose pointer type implements `Marshaler` is written as `null` without the method being called at all, so `MarshalJSON` never has to defend against a nil receiver reached that way. ## The decoding side `UnmarshalJSON([]byte) error` receives the raw bytes of the JSON value that maps to this position — the whole object, the whole array, or the scalar, exactly as it appeared in the input, including a literal `null`. Its job is to parse those bytes and set the receiver. Since it mutates, it must be declared on a pointer receiver; a value-receiver `UnmarshalJSON` compiles, is found for `*T`, and quietly writes into a copy that is thrown away. Inside either method, the usual move is to delegate to `encoding/json` for a *different* type — a string, a map, or a purpose-built struct — which is both simpler and faster than hand-writing JSON syntax. ## A worked shape An enum is the canonical example. In Go it is an integer; on the wire the team wants a stable lowercase name so the JSON stays readable and does not break when someone inserts a constant. `MarshalJSON` converts the integer to its name and marshals *that string*; `UnmarshalJSON` does the reverse and returns an error for a name it does not know, which turns an unknown enum value into a decode failure at the edge rather than a silent zero deep inside the program. ## When to reach for it A custom marshaler is the right tool when one type has a representation that no combination of struct tags can express, or when a type is used in dozens of payloads and you want that representation in exactly one place. It is the wrong tool when the difference is really between a domain model and a wire contract for one endpoint — then a separate wire struct, converted explicitly, is easier to evolve and easier to read, because a marshaler hides the wire format inside a method that nothing at the call site mentions. Whatever you choose, test it as a contract: a round-trip test (`Marshal` then `Unmarshal`, compare) plus a golden fixture asserting the exact bytes. The compiler cannot check a wire format, and a custom marshaler is precisely the code that changes it.

  • What happens if MarshalJSON returns bytes that are not valid JSON?
    The encode fails. `encoding/json` compacts and validates whatever the method returns rather than splicing it in blindly, so a fragment such as `"a":1` or a truncated object makes `json.Marshal` return an error of the form `json: error calling MarshalJSON for type T`. Nothing partial is written.
  • Does encoding/json call MarshalJSON on a nil pointer whose pointer type declares it?
    No. The encoder writes `null` for a nil pointer without invoking the method, so you never have to guard the receiver against nil for that case. It is still worth guarding against a nil map or slice *inside* the value, which the encoder cannot know about.
  • Once a type has MarshalJSON, do its json struct tags still do anything?
    Not for that type. Tags are read by the reflective field walk, and the walk no longer happens — the method's output is the encoding. This is why adding a field to a struct that has a custom marshaler does not make the field appear on the wire until the method is updated too.

saying these in an interview costs you the question

  • Says MarshalJSON should return a JSON string rather than a []byte
  • Assumes json struct tags still shape output once MarshalJSON exists
  • Believes the returned bytes are written through without validation
  • Names the method ToJSON or Marshal and expects encoding/json to find it
  • Declares UnmarshalJSON on a value receiver and expects the field to be set
open as a page

Why is a pointer-receiver MarshalJSON skipped when json.Marshal is passed the struct by value?

level: middleimportance: should knowfreq 48%

basics

~20 s

A method declared on *T is not in T's method set, so a T copied into json.Marshal's any parameter does not satisfy json.Marshaler. The encoder silently falls back to the default field-by-field output. Passing &v works.

open as a page

What problem does json.RawMessage solve when a JSON envelope's payload shape depends on a type field?

level: middleimportance: should knowfreq 46%

basics

~20 s

json.RawMessage is a []byte that implements both JSON interfaces, so a field of that type is filled with the payload's raw bytes instead of being decoded. You read the discriminator first, then unmarshal those bytes into the concrete type.

open as a page

After embedding time.Time in a struct, why does json.Marshal emit only a timestamp and drop the other fields?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Embedding promotes time.Time's MarshalJSON into the outer struct's method set, so the outer struct itself satisfies json.Marshaler. The encoder calls the promoted method, which knows only the time, and never walks the outer fields. Give the field a name.

open as a page

Why does a MarshalJSON that calls json.Marshal on its own receiver recurse forever, and what stops it?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

json.Marshal checks the value's type for a MarshalJSON method, finds the very method that is running, and calls it again, until the goroutine stack limit is hit. The fix is to convert to a locally declared type with the same fields and no methods.

open as a page