skip to content

Validating Parsed Structs

A successful Decode only proves the bytes were well-formed: a missing required field arrives as a zero value and no range was checked, so the struct still needs its own validation pass.

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

questions

4

Why does json.Unmarshal succeed when a required field is missing from the JSON input?

level: juniorimportance: must knowfreq 74%

answer

  1. decoding fills only what it finds
  2. an absent key assigns nothing
  3. the field keeps its zero value
  4. no tag option means required
  5. nil error, and zero means nothing

basics

~20 s

encoding/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 s

Decoding 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 lines
go
type 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": 0

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

Should a request type validate inside its UnmarshalJSON method or in a separate Validate method?

level: middleimportance: should knowfreq 48%

basics

~20 s

Prefer a separate Validate method. Validation inside UnmarshalJSON runs only when the value arrives as JSON, blurs malformed syntax with invalid content, and can leave the receiver half-populated. A Validate method covers every construction path and is trivial to test.

open as a page

In Go, a request struct decoded from JSON carries an empty TenantID three layers deep. How do you keep invalid decoded values out of the rest of the service?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Convert at the edge. The handler decodes a wire struct, then builds a separate domain value through a constructor that returns an error, and only that value travels inward. Deeper layers then have no way to receive an unchecked request.

open as a page

How would you write a Go fuzz target for a function that decodes and validates a request?

level: seniorimportance: nice to knowfreq 27%

basics

~20 s

Add a FuzzXxx function taking *testing.F, seed it with f.Add on real payloads, and inside f.Fuzz assert properties: parsing never panics, and a nil error always yields a value satisfying the invariants callers rely on.

open as a page