What concrete Go types does json.Unmarshal produce when the target is an `any`?
answer
- six shapes, and no more
- objects and arrays become open containers
- every number gets one and the same type
- null keeps no type at all
- float64, even for an integer id
basics
~10 sSix 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 sWhen 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 linesvar 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
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.
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.
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.
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