Why does json.Unmarshal succeed when a required field is missing from the JSON input?
answer
- decoding fills only what it finds
- an absent key assigns nothing
- the field keeps its zero value
- no tag option means required
- nil error, and zero means nothing
basics
~20 sencoding/json has no required-field concept. A key absent from the input is simply never assigned, so the struct field keeps its zero value and Unmarshal returns nil. Requiredness is a rule your own code must check after decoding.
solid answer
~40 sDecoding in Go is a shape operation, not a policy one. `json.Unmarshal` walks the JSON object and assigns the keys it finds to matching exported struct fields; a key that is not present is never assigned, so the field keeps the zero value it already had - `""`, `0`, `false`, a nil map or slice. No struct tag option marks a field as required; `omitempty` only affects encoding. So `{"email":"[email protected]"}` decoded into a struct with `Email` and `Age` gives `err == nil` and `Age == 0`, which is indistinguishable from a client that explicitly sent `"age": 0`. Requiredness therefore has to be a step you own - a `Validate() error` method, or a constructor that turns the decoded wire struct into a domain value - run on every decode path.
code
go · 8 linestype CreateUser struct {
Email string `json:"email"`
Age int `json:"age"`
}
var u CreateUser
err := json.Unmarshal([]byte(`{"email":"[email protected]"}`), &u)
// err is nil, u.Age is 0 - identical to a client that sent "age": 0go deeper
Be ready to say plainly that a missing key leaves the field at its zero value and that Unmarshal still returns nil. Know the zero values by heart: empty string, 0, false, nil map and nil slice.
Explain why the decoder cannot help: it maps a document onto a type and has no notion of a required field, so absent and explicitly-zero are indistinguishable afterwards. Be ready to name where the check belongs instead.
Show that you make the check unskippable rather than remembering it: one decode path per service, a validation step wired into it, and errors that name the offending field so the caller gets a useful 400 instead of a failure deeper in.
Own the convention across services: whether request validation is a shared helper everyone routes through or per-handler code, and what the team accepts as the cost of getting that wrong once in a rarely used endpoint.
## What decoding actually does `json.Unmarshal(data []byte, v any) error` and `(*json.Decoder).Decode(v any) error` do one job: take the JSON that is there and put it into the Go value you point them at. For a struct, the decoder walks the keys of the JSON object and, for each one, looks for a matching exported field. If it finds a match with a compatible type, it assigns. If the JSON object simply does not contain a key, nothing happens for the corresponding field - there is no rule to violate, so there is no error to report. That is why decoding an object with two of your five fields returns `nil`. The decoder did exactly what it promises. ## Why the zero value is the trap Go has no uninitialised memory: every field of a freshly declared struct already holds its type's zero value - `""` for `string`, `0` for numeric types, `false` for `bool`, and nil for maps, slices, functions and interfaces. After a decode, a field that the payload omitted is holding exactly the same value as a field the payload explicitly set to zero: ``` type CreateUser struct { Email string `json:"email"` Age int `json:"age"` } ``` Both `{"email":"[email protected]"}` and `{"email":"[email protected]","age":0}` leave `Age == 0`. The struct cannot tell you which of the two the client sent, because the wire fact - whether the key was present - was discarded during decoding. On a service where every handler decodes a request struct, that is how a bad request survives the edge: it decodes cleanly, the handler sees `err == nil`, and an empty tenant id or a zero quantity travels inward until something several layers down chokes on it - or worse, quietly does the wrong thing with it. ## The language will not do this for you A common wrong guess is that a struct tag can make a field mandatory. It cannot. `encoding/json` understands a name and a small set of options after it; `omitempty` suppresses a field when *encoding* and has no meaning at all on the way in. An invented option like `json:"age,required"` is not an error - it is simply not a flag the package knows, so it does nothing. There is no compile-time or decode-time mechanism that says "this field must appear". This is a deliberate design position: the decoder maps a document onto a type, and what counts as a valid request is domain knowledge that lives in your code. ## Where the check goes instead Two shapes cover almost everything: 1. A `Validate() error` method on the request type, called immediately after the decode returns. It checks the rules the domain actually has - the email is non-empty and parses, the age is at least 13, the start time is before the end time - and returns an error that names the offending field so the handler can turn it into a 400 with a useful message. 2. A constructor that consumes the decoded wire struct and returns a separate domain value plus an error. Nothing further inside the service ever sees the raw decoded struct, so no deeper layer has to re-check anything. Either way, the decisive discipline is that there is exactly one path from bytes to a usable value, so no handler can decode and forget to check. ## Related traps in the same family - **A reused struct variable does not get cleared.** `Unmarshal` and `Decode` only assign fields whose keys appear in the input; they do not zero the destination first. Decoding a second payload into the same variable leaves fields the second payload omitted holding the first payload's values. Declare a fresh variable per message. - **An unexported field is silently ignored.** `encoding/json` only touches exported fields. A field named `email` rather than `Email` stays zero forever and never produces an error, which looks exactly like a missing key. - **`err == nil` means the document parsed, not that the request makes sense.** Syntax errors, type mismatches and malformed UTF-8 are the decoder's business; "this request is usable" is yours. ## If you truly need presence, not just value Sometimes the domain genuinely needs to distinguish "the client said nothing" from "the client said zero" - a partial update, say. One way that costs nothing structurally is to decode the same bytes a second time into a `map[string]json.RawMessage` and test for the key: presence in that map is the wire fact, and each value stays unparsed until you want it. It costs a second pass, so most services instead choose a domain rule under which the zero value is simply not legal and let ordinary validation reject it.
- If you decode two payloads into the same struct variable, what happens to fields the second payload omits?They keep the values the first decode left behind. `json.Unmarshal` and `Decoder.Decode` assign only the fields whose keys appear in the input; they do not zero the destination first. Reusing one request variable across messages therefore leaks the previous message's values into the next one. Declare a fresh variable per decode, or reset it explicitly before reusing it.
- Can an unexported struct field ever be filled by encoding/json?No. `encoding/json` only reads and writes exported fields; unexported ones are ignored on both decode and encode, with no error. A field that stays at its zero value forever is often just a lower-case first letter rather than a missing key, and a required-field check at the boundary catches that mistake too.
- How would you tell "the client sent nothing" apart from "the client sent zero" without changing the struct's field types?Decode the same bytes a second time into a `map[string]json.RawMessage` and test whether the key is present; presence in that map is the wire fact you want, and each value stays unparsed until you need it. It costs a second pass over the payload, so most services instead define a domain rule under which the zero value is simply invalid.
The decoder is a mail sorter, not a receptionist: it delivers whatever letters arrived to the right pigeonholes and never asks why one pigeonhole is still empty.
saying these in an interview costs you the question
- Claims encoding/json returns an error for a missing key
- Thinks a json struct tag can mark a field required
- Says omitempty makes a field mandatory when decoding
- Treats a nil error from Unmarshal as a valid request
- Believes Unmarshal zeroes the destination struct first