ErrLookup › Background articles › json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it
json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it
"json marshal failed", "failed to marshal request body", or "unsupported type" errors from Go's encoding/json mean a value could not be serialized to JSON — typically because the data contains a channel, func, cyclic reference, NaN/Inf float, or a custom MarshalJSON that returned an error. This family gathers that failure mode across Go libraries and shows how to find the offending field and fix it.
Distilled from 108 documented records across 22 repositories.
Background
Every record in this family comes from the same place: a call to json.Marshal, json.MarshalIndent, or json.NewEncoder(...).Encode in a Go library, wrapped and re-raised with library-specific context like "failed to marshal GraphQL request: %w", "encoding value: %w", or "marshaling request object as JSON: %w". The error exists because encoding/json is a total serializer only for JSON-shaped data: strings, numbers, booleans, nil, slices, maps with string-like keys, and structs of those. Values outside that set — channels, funcs, complex numbers, unsafe pointers, NaN and ±Inf floats, and cyclic or self-referential structures — make the marshal call fail at runtime rather than at compile time, and the library has to decide what to do with that failure.
From the caller's side the error is almost always unexpected, and the records show why: in the overwhelming majority of cases the thing being marshaled is a plain struct or map of strings and numbers, and the library authors themselves flag the branch as "practically unreachable" or "defensive." Tailscale's operator marshals a Records struct of strings and string slices and calls the error effectively unreachable; charmbracelet/crush wraps a marshal of a struct with two string fields; beads wraps json.Marshal on a single Go string and notes it "cannot fail in current Go." When such an error does fire, it signals one of a small set of conditions: a non-serializable value was injected somewhere upstream, a custom MarshalJSON method returned an error, a float became NaN/Inf (for example via division by zero), or the binary was built from modified code or mismatched versions.
The family varies mainly in how much context the wrapper gives you and what it means operationally. Some wrappers name the offending object — cilium prints the patch it could not serialize ("json.Marshal(%v) failed"), logtail identifies the buffered entry by sequence number ("logtail: encoding entry %d"), and beads names the issue ID ("failed to marshal issue %s"). Others are generic. The consequence also varies: in pulumi's REST client the HTTP request is never sent; in navidrome's inspect endpoint and vitess's /twopcz handler the raw error text is written as an HTTP 500 body (which itself leaks implementation detail); in tailscale's logtail the entire log upload batch is aborted. In most other cases persistence, export, or a debug endpoint simply fails.
Because the underlying cause lives in the data rather than the call site, the reliable diagnosis path is the same everywhere: unwrap the %w error to see the concrete json.UnsupportedTypeError (unsupported type) or json.UnsupportedValueError (unsupported value such as NaN or a cycle), then inspect the named object for fields holding channels, funcs, cyclic references, non-finite floats, or a custom Marshaler with an error path. Several libraries add extra wrinkle: siyuan and gofr marshal through helper wrappers (gulu.JSON.MarshalIndentJSON, a bulk encoder) that preserve the same semantics, and waveterm's tool-input parsing deliberately round-trips loosely-typed input through marshal/unmarshal, so caller-supplied data — not library-internal state — is the usual culprit there.
Common causes
- Channel, func, or complex value in the payload. encoding/json cannot represent chan, func, or complex types and fails with an unsupported-type error. This is the trigger named most often across the records — usually after a struct or map gained an exotic field, or a generic any/map[string]any field collected a Go value that should never have been there.
- Cyclic or self-referential data. Self-referencing pointers or maps produce an unsupported-value error at encode time. Records for navidrome, pulumi, and waveterm call this out explicitly for rules, payloads, and tool input maps.
- NaN or ±Inf float values. JSON has no representation for non-finite floats. Tailscale's logtail, pentagi's CompletionRequest (Temperature/TopP parsed from config), cilium's perfData, and pulumi's config structs all name NaN/Inf — often produced upstream by division by zero or misparsed configuration — as the offending value.
- Custom MarshalJSON / MarshalJSONV2 returning an error. Any nested type with a custom marshaler can inject a failure into an otherwise-plain payload. go-micro's AP2 mandate signing, beads, and cilium all advise auditing custom Marshaler implementations for error paths on valid data.
- Bad or non-JSON-shaped input stored in generic containers. Libraries that accept map[string]any or any inputs (gofr Bulk operations, pulumi ESC overrides, waveterm tool callbacks) marshal whatever callers put in. Binary or invalid-UTF-8 strings passed in by an upstream system or LLM tool call can also surface as a marshal failure (crush's Sourcegraph query).
- Version skew, forks, or local modifications. Several records note that the marshal branch is unreachable on stock code, so hitting it suggests a fork or patched build changed a struct to hold unencodable fields, or binary and library versions were mixed (lima, tailscale operator forks, oh-my-posh, SST bootstrap steps).
- In-memory data anomaly or corruption. When runtime-maintained state is marshaled (vitess's in-flight transaction report, beads issue backups), a corrupted or wedged value in memory can fail to serialize. A restart or re-read resolves it when the anomaly is transient.
What usually fixes it
- [object Object]
- [object Object]
- [object Object]
- [object Object]
- [object Object]
- [object Object]
Go deeper
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Documented occurrences
- invalid criteria expression: %w (navidrome/navidrome)
- failed to marshal GraphQL request: %w (charmbracelet/crush)
- error marshalling DNS records: %w (tailscale/tailscale)
- error encoding operation (gofr-dev/gofr)
- ap2: marshal mandate: %w (micro/go-micro)
- failed to marshal tailscaled config: %w (tailscale/tailscale)
- failed to marshal instance %#q: %w (lima-vm/lima)
- failed to marshal patch for node %s: %w (cilium/cilium)
- encoding value: %w (pulumi/pulumi)
- error formatting metadata: %v (wavetermdev/waveterm)
- failed to marshal input: %w (wavetermdev/waveterm)
- marshal request: %w (charmbracelet/crush)
- marshaling environment overrides: %w (pulumi/pulumi)
- logtail: encoding entry %d: %w (tailscale/tailscale)
- json.Marshal(%v) failed: %w (cilium/cilium)
- failed to marshal policy for bucket %s: %w (anomalyco/sst)
- json marshal error (err.Error()) (navidrome/navidrome)
- marshaling Linear milestone metadata: %w (gastownhall/beads)
- marshalling request object as JSON: %w (pulumi/pulumi)
- failed to marshal template cache: %w (JanDeDobbeleer/oh-my-posh)
…and 88 more across the corpus — use search.
Honest provenance: generated on 2026-09-01 from AI-assisted analysis of the linked records. See how records are made.