skip to content

JSON Marshaling

encoding/json in both directions: tags that rename or drop fields, Decoder and Encoder streaming, custom MarshalJSON, RawMessage. It is the standard-library package Go interviews probe most.

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

explore

questions

page 1 of 2

What concrete Go types does json.Unmarshal produce when the target is an `any`?

level: juniorimportance: must knowfreq 72%

answer

  1. six shapes, and no more
  2. objects and arrays become open containers
  3. every number gets one and the same type
  4. null keeps no type at all
  5. float64, even for an integer id

basics

~10 s

Six concrete types, and no others: map[string]any for a JSON object, []any for an array, string for a string, float64 for every number, bool for true and false, and an untyped nil for null.

solid answer

~50 s

When the destination of `json.Unmarshal` is an interface value with no methods, the decoder has no Go type to guide it, so it uses a fixed table: a JSON object becomes `map[string]any`, an array becomes `[]any`, a string becomes `string`, **every** number becomes `float64` regardless of whether it looked like an integer, `true`/`false` become `bool`, and `null` leaves the interface holding nothing at all — a nil interface. Nesting composes, so a JSON object of arrays of objects decodes into `map[string]any` holding `[]any` holding more `map[string]any`. Because the result is a map, object key order is gone. The practical consequences are that you must reach values through type assertions, that `x.(int)` never succeeds on a decoded number, and that a JSON `null` and a missing key look almost identical unless you use the comma-ok form on the map lookup.

code

go · 7 lines
go
var v any
if err := json.Unmarshal([]byte(`{"name":"ada","age":36,"tags":["x"],"boss":null}`), &v); err != nil {
	return err
}
m := v.(map[string]any)
fmt.Printf("%T %T %T %T\n", m["name"], m["age"], m["tags"], m["boss"])
// string float64 []interface {} <nil>

go deeper

for a junior

Be ready to recite the six results — object, array, string, number, boolean, null — and to say out loud that the number case gives float64. Also remember the destination must be passed as a pointer.

for a middle

Explain why the decoder has no better choice than float64 when no Go type steers it, and show the comma-ok assertion and the type switch you would use to walk such a document safely.

for a senior

Show judgment about when a schemaless decode is appropriate at all, and describe how you would catch a payload whose shape drifted before a type assertion panics inside business logic.

for a principal

Frame it as a boundary policy: which services in the estate are allowed to carry untyped documents through, and what you require of a team that decodes into an untyped map instead of declaring the contract.

## The question behind the question Go's `encoding/json` normally decodes into a type you supply: you declare a struct, call `json.Unmarshal(data, &cfg)`, and the decoder uses the struct's fields to decide what each JSON value should become. But sometimes you do not know the shape ahead of time — a webhook whose body varies, a payload you are exploring at a terminal, a passthrough that only touches one field. In that case you decode into `any` (the alias for `interface{}`), and the decoder falls back on a fixed mapping. ## The mapping The decoder produces exactly six things: | JSON | Go | |---|---| | object | `map[string]any` | | array | `[]any` | | string | `string` | | number | `float64` | | `true` / `false` | `bool` | | `null` | nil interface (no type, no value) | That is the whole table. There is no `int`, no `int64`, no `time.Time`, no `map[string]string` — those require a typed destination for the decoder to aim at. ## Why every number is a float64 JSON has one numeric type. The grammar does not distinguish `7` from `7.0` from `7e0`; they are the same JSON value. With no Go type to steer it, the decoder cannot know whether you meant a counter, a price, or an id, so it picks the type that can represent the widest range of JSON numbers: `float64`. This is the single most surprising part of the table for newcomers, and it has a real cost — a `float64` carries 53 bits of mantissa, so an id above roughly 9 quadrillion silently rounds. If a decoded number must survive exactly, either decode into a typed field (an `int64` field is parsed as an integer directly, with no floating-point detour) or switch the decoder into a mode that keeps the original literal. ## Why null is not a zero value A JSON `null` stored into an `any` does not become `0`, `""`, or an empty map — it leaves the interface with no dynamic type at all, so `v == nil` is true and `fmt.Printf("%T", v)` prints `<nil>`. That means a key present with a `null` value and a key that never appeared both produce a nil-ish result at the use site. You can still tell them apart, but only at the map level, with the comma-ok form: `raw, present := m["boss"]` gives `present == true` and `raw == nil` for an explicit `null`, and `present == false` when the key was absent. ## Getting values back out Everything in the tree is an `any`, so every read is a type assertion, and every assertion must use the comma-ok form unless you are certain of the shape: ```go m, ok := v.(map[string]any) if !ok { /* payload was not an object */ } name, ok := m["name"].(string) ``` The single-result form `m["name"].(string)` panics when the payload disagrees, and payloads disagree in production far more often than in the sample you developed against. A `switch t := v.(type)` with cases for the six types above is the exhaustive way to walk an unknown document. ## What the decoder still checks Decoding into `any` is not decoding without validation. The input must be syntactically valid JSON; a malformed body still returns a `*json.SyntaxError`. And the destination must be a non-nil pointer — `json.Unmarshal(data, v)` where `v` is a plain `any` value returns a `*json.InvalidUnmarshalError`, because the argument is copied into the parameter and the decoder has nowhere to write. Idiomatic use is `var v any; err := json.Unmarshal(data, &v)`. ## Order is not preserved Because a JSON object becomes a Go map, the order in which keys appeared in the document is lost, and iterating that map yields keys in a deliberately randomised order. Re-encoding a `map[string]any` writes keys sorted, not in the original order. If key order matters to a consumer — it rarely should, since JSON objects are unordered by definition — a `map[string]any` round trip is the wrong tool. ## When to use it Decoding into `any` earns its place for genuinely schemaless data, for a quick exploration of an unfamiliar payload, and for code that must forward a document it does not understand. For anything you actually read fields out of, a struct is shorter, faster, self-documenting, and turns a runtime panic into a decode error you can log.

  • Why must the second argument to json.Unmarshal be a pointer, and what happens if it is not?
    The argument is copied into an `any` parameter, so without a pointer the decoder would only fill a copy that nobody can see. Rather than doing that silently it returns a `*json.InvalidUnmarshalError`, whose message reads like `json: Unmarshal(non-pointer main.Config)`. Passing a nil pointer gives the same error type. The idiomatic call is `json.Unmarshal(data, &v)`.
  • How do you tell an explicit JSON null apart from a key that was never sent, once the payload is a map[string]any?
    Use the comma-ok form on the map lookup rather than looking at the value. `raw, present := m["boss"]` gives `present == true` with `raw == nil` for an explicit `null`, and `present == false` when the key was absent. Checking only `m["boss"] == nil` collapses the two cases, because a missing key yields the zero value of the map's element type, which for `any` is nil.
  • Does a map[string]any keep the order the keys appeared in the JSON document?
    No. A Go map has no order, and ranging over one yields keys in a deliberately randomised order. Re-encoding a `map[string]any` writes the keys sorted, so a decode-then-encode round trip generally reorders the document. JSON objects are unordered by specification, so a consumer that depends on key order is already relying on something the format does not promise.

It is like reading a form filled in with no field labels: you can see text, digits and blanks, but nothing tells you which digits were meant as a whole count and which as a measurement, so everything numeric is filed under one heading.

saying these in an interview costs you the question

  • Says a whole JSON number decodes into an int
  • Expects a JSON object to become map[string]string
  • Thinks JSON null decodes to an empty string or zero
  • Uses single-result type assertions on decoded values
  • Claims a map[string]any preserves key order
  • Passes the destination by value instead of by pointer
open as a page

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

level: juniorimportance: must knowfreq 62%

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.

open as a page

In encoding/json, what happens to the emitted JSON key when you rename a struct field?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Without a json tag, encoding/json emits the Go field name as the key, so renaming the field renames the key and old readers see nothing. A json tag pins the wire name, freeing the Go field to be renamed.

open as a page

In encoding/json, what does the omitempty struct tag option omit, and what does it leave in?

level: juniorimportance: must knowfreq 78%

basics

~20 s

In encoding/json, omitempty skips a field whose value is false, 0, an empty string, a nil pointer or interface, or an empty array, slice or map. It never skips a zero-valued struct such as time.Time{}, and it only affects encoding.

open as a page

When should you use json.NewDecoder(r).Decode(&v) instead of json.Unmarshal on a []byte?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Use json.NewDecoder when the JSON arrives as a stream, such as an HTTP body or a file, so it is decoded as bytes arrive. Use json.Unmarshal when you already hold the whole document in memory as a []byte.

open as a page

Why does encoding/json skip a struct's unexported fields, even when they carry a json tag?

level: juniorimportance: must knowfreq 74%

basics

~20 s

encoding/json can only see exported fields, meaning those whose names begin with a capital letter. Unexported fields are skipped when marshaling and left untouched when unmarshaling, so a json tag written on one has no effect at all.

open as a page

What does the struct tag json:"user_id,omitempty" change about how encoding/json handles that field?

level: juniorimportance: must knowfreq 82%

basics

~20 s

The tag renames the field to user_id in JSON, on both encoding and decoding. The omitempty option additionally drops the field from the encoder's output when its value is false, 0, nil, or an empty string, slice, array or map.

open as a page

Why does JSON decoding into a Go `any` mangle a 19-digit id, and how do you keep it exact?

level: middleimportance: must knowfreq 58%

basics

~20 s

Decoding into an any makes every JSON number a float64, whose 53-bit mantissa cannot hold a 19-digit id, so it rounds silently. Use an int64 field, or a json.Decoder with UseNumber to get a json.Number.

open as a page

What does encoding/json/v2 reject or match differently by default compared with encoding/json?

level: middleimportance: must knowfreq 50%

basics

~10 s

encoding/json/v2 rejects duplicate object names and invalid UTF-8, which encoding/json accepts (last one wins, and invalid bytes become U+FFFD). It also matches member names to struct fields case-sensitively, with no case-insensitive fallback.

open as a page

What do encoding/json/v2 and encoding/json/jsontext each provide, and does importing encoding/json still behave the same?

level: juniorimportance: should knowfreq 42%

basics

~10 s

encoding/json/jsontext handles JSON syntax: tokens, values, escaping. encoding/json/v2 layers the Go-to-JSON mapping on top of it, with stricter defaults. The original encoding/json keeps its old behaviour, so upgrading the toolchain alone changes nothing.

open as a page

Which Go types does json.Marshal encode as something other than their obvious JSON counterpart?

level: middleimportance: should knowfreq 48%

basics

~10 s

A []byte becomes a base64 string, not an array of numbers. A nil slice or map becomes null while an empty non-nil one becomes [] or {}. Integer map keys become quoted object keys.

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

Which changes to a Go struct marshaled by encoding/json break clients you cannot upgrade?

level: middleimportance: should knowfreq 58%

basics

~20 s

Adding a field under a new tag name is safe; an old decoder has no field for the extra key and skips it. Renaming or deleting a key breaks readers silently, and changing the JSON type behind a key breaks them loudly.

open as a page

When json.Unmarshal meets a JSON null, which Go field types does it set to nil and which does it leave untouched?

level: middleimportance: should knowfreq 52%

basics

~20 s

In encoding/json, a JSON null sets a pointer, interface, map or slice field to nil. For any other Go type, including numbers, strings and structs, it is a documented no-op: the field keeps its value and no error is returned.

open as a page

How does encoding/json's omitzero option differ from omitempty, and when do the two disagree?

level: middleimportance: should knowfreq 42%

basics

~20 s

In encoding/json, omitzero (Go 1.24) omits a field holding its type's zero value, using an IsZero() bool method when there is one. omitempty omits a fixed list of empty values instead, so the two disagree on zero structs and on non-nil empty slices.

open as a page

How does the output of json.NewEncoder(w).Encode(v) differ from json.Marshal(v)?

level: middleimportance: should knowfreq 48%

basics

~20 s

Encode writes the value to the io.Writer and appends a trailing newline, which json.Marshal never does. The encoder also carries settings json.Marshal has no access to: SetIndent for formatting and SetEscapeHTML(false) to stop < > & being escaped.

open as a page

Why can a json struct tag be silently ignored, leaving encoding/json using the Go field name?

level: middleimportance: should knowfreq 46%

basics

~20 s

A struct tag is an ordinary string literal the Go compiler never validates. If it breaks the conventional key:"value" form, the json lookup finds nothing and encoding/json falls back to encoding the field under its Go name.

open as a page

How does omitempty in encoding/json/v2 differ from omitempty in encoding/json?

level: middleimportance: should knowfreq 44%

basics

~20 s

encoding/json omits a member when the Go value is empty: false, 0, "", nil, or a zero-length map, slice or array. encoding/json/v2 omits it only when the value encodes to an empty JSON value: null, "", {} or []. So false and 0 are now emitted.

open as a page

Your service decodes every payload into map[string]any and type-asserts at use sites. Why does that panic in production?

level: seniorimportance: should knowfreq 44%

basics

~10 s

Decoding into map[string]any succeeds for any valid JSON, so nothing checks shape, and each single-result type assertion is an unchecked bet. Drift panics deep in business logic instead of failing at the decode.

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 keep a separate JSON wire struct rather than putting json tags on the domain type?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Tagging the domain type welds the published payload to your internal model, so every refactor becomes a wire change. A small tagged struct plus a conversion function keeps the contract in one reviewable file and lets the two change at different rates.

open as a page

In a Go patch API, how do you tell an omitted JSON field from an explicit null when decoding?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A *T field cannot do it, because encoding/json leaves it nil for an absent key and also sets it to nil for an explicit null. Three states need a value-typed wrapper with an UnmarshalJSON method, a json.RawMessage field, or a first pass into map[string]json.RawMessage.

open as a page

A report-export handler shares one json.Encoder across goroutines and the download is corrupt. Why?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A json.Encoder has no internal locking, and neither does an http.ResponseWriter. Concurrent Encode calls race on shared state and their writes interleave, splicing records together. Give one goroutine the encoder and feed it rows over a channel.

open as a page

A generated struct embeds two types that each tag a field json:"id". What does encoding/json emit?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Neither field appears. Embedded structs are flattened into the outer JSON object, but when two promoted fields sit at the same shallowest depth and claim the same name, encoding/json drops both of them and reports no error at all.

open as a page

An API gateway switching to encoding/json/v2 starts rejecting payloads it used to forward. How do you find them before the switch?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Shadow-decode live traffic: run every payload through both encoding/json and encoding/json/v2, keep the v1 result, and record only the disagreements, classified as duplicate name, invalid UTF-8 or an unmatched member. Then attribute each class to a sender before flipping any route.

open as a page

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

level: middleimportance: nice to knowfreq 33%

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.

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

Why does a struct with a sql.NullString field marshal to a JSON object instead of a string or null?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

sql.NullString is an ordinary two-field struct and implements neither json.Marshaler nor encoding.TextMarshaler, so encoding/json applies its default struct rules and writes {"String":"","Valid":false}. It also fails to decode a bare JSON string. Use a *string in the wire type instead.

open as a page

Using json.Decoder.Token and More, how do you decode a huge JSON array one element at a time?

level: middleimportance: nice to knowfreq 40%

basics

~20 s

Call Token once to consume the opening bracket, then loop while More reports another element, calling Decode into one element value each time, and finally call Token again to consume the closing bracket. Only one element is ever in memory.

open as a page

showing 1–30 of 33