Why does json.Unmarshal leave a struct field at zero when a key in the JSON is misspelled, and return no error?
answer
- the decoder is tolerant by default
- no field matched, so nothing happened
- a successful decode with an untouched field
- zero value, nil error, no log line
- strictness is opt-in on the Decoder
basics
~20 sencoding/json skips any JSON object key that matches no field of the destination struct. A misspelled key is discarded, the field keeps its zero value, and Unmarshal returns nil. Silence is the documented default, not a bug.
solid answer
~50 sBy default `encoding/json` treats a JSON object as a bag of keys to try against the destination struct: for each key it looks for a matching field, and if there is none it skips that key's value and moves on. There is no error, no warning and no record that the key existed, so `json.Unmarshal` returns `nil` and the field it was meant to fill keeps its zero value — `0`, `""`, `false`, or a nil map or slice. This is why a daemon can load a config file in which an operator wrote `"maxConnnections"` instead of `"maxConns"` and start perfectly happily with the default connection limit. The behaviour is deliberate: it lets a reader tolerate keys it does not know about. If you want the typo reported, you have to ask for it explicitly by decoding through a `json.Decoder` with `DisallowUnknownFields` set.
code
go · 8 linestype Config struct {
MaxConns int
Timeout string
}
var c Config
err := json.Unmarshal([]byte(`{"maxConnnections": 50}`), &c)
// err is nil; c.MaxConns is still 0go deeper
Be ready to say plainly that encoding/json ignores keys it cannot match, leaves the field at its zero value, and returns no error. Know that strict behaviour exists and lives on the Decoder.
Explain the mechanics: the decoder skips the value of an unmatched key and continues, so a valid document always decodes; contrast that with a type mismatch, which does produce an error.
Show how you would stop this reaching production: strict decoding at startup, or a second strict pass purely to log the offending key, plus logging the effective configuration the process actually holds.
Own the posture. Decide where in the pipeline an unknown key is fatal versus merely reported, and who is allowed to relax it when a rollout is blocked by a key an older binary does not know.
## What actually happens during the decode When you call `json.Unmarshal(data, &c)` with a struct destination, the decoder walks the JSON object one key at a time. For each key it builds the list of candidate fields on the destination struct and looks for one whose name matches. If it finds a field, it decodes the value into it. **If it finds no field, it does not fail — it parses the value just far enough to skip past it and continues with the next key.** So three separate things are true at once after a decode of `{"maxConnnections": 50}` into a struct whose field is `MaxConns int`: - The input was valid JSON, so there is no syntax error. - No field matched the key, so nothing was assigned. - Nothing failed, so `Unmarshal` returns `nil` and the field is still `0`. The caller sees a successful decode. The only evidence that something was wrong is a value that happens to equal the zero value — which is indistinguishable from "the operator did not set it". ## Why the standard library chose silence The default is tolerant on purpose. A JSON document is often written by something you do not control and evolves faster than your reader does: a payload gains a field, a config template gains a setting for a newer build, an API adds metadata. If an unrecognised key were fatal, every reader would break the moment a writer added anything. Skipping unknown keys lets a program decode the part of a document it understands and ignore the rest. That tolerance is exactly the property that turns a typo into a silent misconfiguration, so the library is asymmetric by design: **lenient by default, strict only when you ask.** ## The two shapes this takes in practice **The typo.** Someone hand-edits a config file or a request body and writes `"reties"`, `"time_out"`, or `"maxConnnections"`. Nothing matches, the setting is dropped, and the process runs on defaults. This is the case the operator experiences as "my change had no effect". **The drift.** A field is renamed in the Go struct — or a writer starts emitting a new key name — and the two sides stop agreeing. Every document still decodes cleanly; the affected field just quietly stops being populated. Because nothing errors, this can survive a deploy, a rollback, and several weeks of production. ## What a zero value does and does not tell you A zero value after a decode has at least three causes that the decoder does not distinguish: the key was absent, the key was present but unmatched (the case here), or the key was present with an explicit zero — `0`, `""`, `false`. Decoding JSON `null` into most Go types is also a no-op that leaves the field as it was. Any code that reasons about "did the user set this?" from the value alone is guessing. ## How to make it visible The direct tool is a `json.Decoder` with `DisallowUnknownFields()` called before `Decode`, which turns the first unmatched key into an error whose text names it: `json: unknown field "maxConnnections"`. `json.Unmarshal` itself has no such option, so when you only have bytes you wrap them: `json.NewDecoder(bytes.NewReader(b))`. Even when you do not want strict decoding to be fatal in production, you can get the report cheaply: decode leniently into your struct as usual, then decode the same bytes a second time with the strict decoder purely to log what it complains about. The service still starts, but the offending key is now named in a log line instead of being invisible. ## The rules worth memorising - An unmatched JSON key is skipped; the decode still succeeds. - An unmatched key leaves the corresponding field at its Go zero value. - Matching is not exact-only: a key that differs from the field name only in letter case does match, so not every "typo" is dropped — some land on the field anyway. - `json.Unmarshal` cannot report unknown keys; only a `json.Decoder` configured with `DisallowUnknownFields` can. - The struct populated by a failed strict decode may already hold the fields that did match, so treat that error as fatal and throw the value away.
- How would you make that misspelled key an error instead?Decode through a `json.Decoder` and call `DisallowUnknownFields()` on it before `Decode`. The first key that matches no field then produces an error whose text names it, such as `json: unknown field "maxConnnections"`. `json.Unmarshal` has no equivalent switch, so wrap the bytes with `json.NewDecoder(bytes.NewReader(b))` when that is all you have.
- After a successful Unmarshal, can you tell whether a field was absent from the JSON or explicitly set to zero?Not from the value. Absent, unmatched, and explicitly `0`/`""`/`false` all leave the same result, and JSON `null` typically leaves the field untouched too. Distinguishing them requires the decode itself to carry that information — for example by decoding into a pointer field, which stays nil when no key filled it.
- Does the same silence apply when the key matches but the type does not?No. A type mismatch is reported: decoding `{"maxConns": "fifty"}` into an `int` field returns a `*json.UnmarshalTypeError`. Unmatched keys are skipped silently; matched keys with the wrong JSON type are errors. That asymmetry is why a wrong value is usually noticed and a wrong key name usually is not.
It is like posting a form into a slot labelled with the fields the reader knows: anything written next to a label it does not recognise is not rejected, it is simply never transcribed, and the receipt still says accepted.
saying these in an interview costs you the question
- Claims Unmarshal errors on keys the struct does not have
- Says the decoder does fuzzy or nearest-name matching on keys
- Thinks a nil error proves every key in the input was used
- Assumes an unexported field can receive a JSON key
- Believes json.Valid would have caught the misspelling