skip to content

Which Go values make json.Marshal return an error instead of producing JSON?

level: middleimportance: nice to knowfreq 33%

answer

  1. two error types, type versus value
  2. some kinds simply have no wire form
  3. a legal float with no JSON spelling
  4. one bad field sinks the whole document
  5. channels, funcs, complex, NaN, infinity, cycles

basics

~20 s

Channels, functions and complex numbers have no JSON form and give an UnsupportedTypeError. The float values NaN, positive and negative infinity give an UnsupportedValueError. So does a pointer cycle. Nothing is written: Marshal returns nil bytes plus the error.

solid answer

~50 s

Two error types cover it. `*json.UnsupportedTypeError` is returned when a *type* has no JSON representation at all — a channel, a function, a complex number, or a map whose key type is neither a string kind nor an integer kind nor something with a textual form. `*json.UnsupportedValueError` is returned when the type is fine but a particular *value* is not: `NaN`, `+Inf` and `-Inf` are all legal `float64` values with no JSON spelling, and a data structure that points back at itself would encode forever, so the encoder detects the cycle and errors instead. Two consequences matter in practice. The check reaches all the way down, so one channel field buried in a nested struct fails the entire document, not just that field. And `Marshal` is all-or-nothing: on error it returns a nil byte slice, never a partial document, so a handler that ignores the error writes an empty body.

code

go · 12 lines
go
type Job struct {
	Name string
	Done chan struct{}
}

_, err := json.Marshal(Job{Name: "import"})
var ute *json.UnsupportedTypeError
fmt.Println(errors.As(err, &ute)) // true - chan struct {} has no JSON form

_, err = json.Marshal(map[string]float64{"rate": math.NaN()})
var uve *json.UnsupportedValueError
fmt.Println(errors.As(err, &uve)) // true - NaN is a valid float64, not a valid JSON number

go deeper

for a junior

Know the short list of things that simply cannot be encoded: channels, functions, complex numbers, and the float values NaN and infinity. Remember that a nil pointer is fine and becomes null.

for a middle

Distinguish the type-level failure from the value-level one, name both error types, and explain that the check descends through the whole value so one nested field fails everything.

for a senior

Trace the failure back to its real cause — a NaN produced by an unguarded division, or internal machinery living on a wire type — and say what you change so it cannot recur.

for a principal

Argue the separation of wire types from runtime types as a standard, and weigh the cost of that duplication against outages caused by a type serving both roles.

## Two error types, two different reasons Most Go values have an obvious JSON form. A few have none, and `encoding/json` distinguishes carefully between "this type can never work" and "this particular value cannot be written". **`*json.UnsupportedTypeError`** — the type has no JSON counterpart: - a channel: JSON describes data, not a communication endpoint - a function value: likewise - a complex number: JSON has one numeric type and it is not complex - a map whose key type is neither a string kind nor an integer kind nor a type that can produce text: JSON object keys must be strings, and the encoder will not invent a spelling for, say, a struct key **`*json.UnsupportedValueError`** — the type is encodable but this value is not: - `math.NaN()`, `math.Inf(1)` and `math.Inf(-1)` are perfectly ordinary `float64` values, but the JSON number grammar has no way to write them - a cyclic structure — a node whose pointer chain leads back to itself — would recurse forever, so the encoder detects the cycle and stops with an error naming the path Both are concrete error types, so you can inspect them with `errors.As` when you want to report the offending Go type or value rather than a generic failure. ## What Marshal does not treat as an error Just as important is the list of things people expect to fail and which do not: - a nil pointer, nil slice, nil map or nil interface encodes as `null` - an unexported field is skipped silently, not rejected - an empty struct encodes as `{}` - a very large integer encodes fine as a JSON number; whether the *consumer* can read it back is a different problem So the error set is small and specific, which is why hitting one usually means a genuine modelling mistake rather than a routine input problem. ## The depth of the check The encoder walks the value graph, so the failure can originate anywhere inside it. A struct with thirty innocuous fields and one `chan struct{}` cannot be marshaled at all — not "marshaled without that field". This is how the error usually reaches people: somebody adds a field for internal coordination, such as a done channel or a callback, to a type that is also the wire type, and every response the service produces starts failing. The lesson that follows is a design one — a struct that is serialized is a wire type, and internal machinery does not belong in it — and the mechanical workaround, if the field must stay, is a tag that excludes it from encoding. The float case is the same shape: nothing rejects `NaN` when it is computed. A rate that divides by a zero count produces `NaN` quietly, it flows into a metrics struct, and the failure appears at the encoder, far from the division. When you see `json: unsupported value: NaN` in a log, the bug is upstream in the arithmetic. ## All or nothing `Marshal` returns `([]byte, error)` and on failure the byte slice is nil. There is no partial document with a hole where the bad value was. In an HTTP handler that means a caller who ignores the error writes zero bytes with whatever status was already set, and the client sees an empty body rather than a helpful failure. Checking the marshal error, or letting an encoder that writes straight to the response surface it, is the difference between a 200 with an empty body and a diagnosable 500. ## Contrast worth knowing Other languages' JSON encoders often coerce rather than refuse: a common choice elsewhere is to turn `NaN` and infinity into `null`, and to drop values that have no representation. Go refuses. That is consistent with the language's general preference for surfacing an impossible conversion rather than guessing at an intent, and it is a good thing to say out loud in an interview, because it explains the design rather than merely reciting the list.

  • A metrics struct marshals fine in tests and fails in production with an unsupported value error. What do you look for?
    A float that became NaN or infinity upstream — almost always a division where the denominator was zero for that sample, such as a success rate computed before any requests arrived. The encoder is only where it surfaces. Fix the arithmetic by guarding the divisor and deciding what an undefined rate should be on the wire, rather than trying to make the encoder tolerate it.
  • If Marshal returns an error, what is in the byte slice it returns alongside it?
    Nothing — it is nil. Encoding is all-or-nothing, so there is no partial document with a gap where the bad value was. In an HTTP handler that means ignoring the error sends an empty body, which is why the marshal error must be checked and turned into a real failure response.
  • How do you keep a field that cannot be encoded, such as a channel, on a type you also send over the wire?
    Exclude it from encoding with a struct tag, or better, stop using one type for two jobs: keep the internal type with its machinery and define a separate struct for the wire. The second option is usually right, because a type that is both a runtime object and a published document shape ends up constrained by whichever role changes first.

saying these in an interview costs you the question

  • Expects NaN to encode as null like some other languages
  • Thinks an unencodable field is skipped rather than fatal
  • Assumes Marshal returns a partial document with the error
  • Confuses a nil pointer, which encodes as null, with an error
  • Believes a cyclic structure loops forever instead of erroring