skip to content

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

level: middleimportance: should knowfreq 46%

answer

  1. the compiler never reads these strings
  2. a space in the wrong place is fatal
  3. the fallback is the Go identifier
  4. an unknown option is dropped without complaint
  5. one vet analyzer catches most of it

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.

solid answer

~50 s

Tags are inert strings; nothing checks them at compile time. The convention `encoding/json` relies on is space-separated `key:"value"` pairs with **no space after the colon**, so `json: "user_id"` does not parse and the field is encoded as `UserID`. The same class of typo hides inside a tag that does parse: `json:"name, omitempty"` yields the option `" omitempty"`, which matches nothing, so the field is named correctly but is never omitted. A miscased key like `Json:"name"` also misses, since the lookup is case-sensitive. All of these compile, ship, and change your wire format without a single diagnostic. The net is `go vet ./...` — its structtag analyzer reports tags that do not conform, duplicate json names within a struct, and json tags on unexported fields — plus a round-trip test that marshals a populated struct and asserts the exact JSON.

code

go · 8 lines
go
type Event struct {
	// space after the key: the pair does not parse, key becomes "ID"
	ID string `json: "id"`
	// option is " omitempty", which matches nothing: never omitted
	Name string `json:"name, omitempty"`
	// key lookup is case-sensitive: key becomes "Kind"
	Kind string `Json:"kind"`
}

go deeper

for a junior

Know that a tag is just a string the compiler ignores, and that the wire name silently reverts to the Go field name when it is malformed. Learn the no-space-after-the-colon rule by heart.

for a middle

Distinguish a tag that fails to parse from one that parses into an option nobody recognises, and explain why both end in silence. Name go vet's structtag check as the guard.

for a senior

Show how you would prevent it rather than find it: vet in CI, a golden test asserting exact JSON bytes, and extra scrutiny on generated tags where one format string affects every struct.

for a principal

Treat the wire format as a contract with its own tests. Decide where that verification lives — generator templates, CI gates, consumer contract tests — so no single typo can change the payload unnoticed.

## Tags are strings, and nothing checks them A struct tag is a string literal that sits after a field declaration. The language specification says almost nothing about its contents: it is metadata carried alongside the field, and the compiler's only interest is that it is a valid string literal. Anything syntactically valid compiles, including complete nonsense. The *convention* — which the standard library and essentially every third-party encoder follow — is a sequence of `key:"value"` pairs separated by spaces: ```go Name string `json:"name" xml:"name"` ``` The parser that reads that convention is strict, and when it fails it does not report failure; it returns "no value for this key". Every consumer then behaves as though the tag were absent. ## The four ways a tag goes quiet **1. A space after the colon.** ```go ID string `json: "id"` ``` The key must be immediately followed by a colon and then the opening quote. With a space in between, the pair does not parse, the whole tag effectively yields nothing for `json`, and the field is encoded as `ID`. **2. A space inside the value.** ```go Name string `json:"name, omitempty"` ``` This one parses fine, which makes it nastier. The value is split on commas, so the option is the six-plus-one-character string `" omitempty"`, which is not `omitempty`. The field is correctly named `name` and is never omitted. Nothing complains — unrecognised options are ignored by design. **3. A miscased or misspelled key.** ```go Kind string `Json:"kind"` ``` Key lookup is case-sensitive. `encoding/json` asks for `json`, finds nothing, uses `Kind`. **4. A misspelled option.** `json:"name,omitempy"` names the field and silently discards the unknown option. To that list add the case from the visibility rule: a perfectly formed tag on an **unexported** field, which is inert because the field itself is invisible. ## Why the failure mode is always the same Every one of these degrades to "the tag said nothing", and `encoding/json` has a well-defined fallback for that: use the Go field name. That is a good default for a struct with no tags at all, and a silent contract change for a struct someone tagged intentionally. The wire key flips from `user_id` to `UserID`, consumers stop finding the field, and the Go code that produced it looks correct at a glance. ## Where this bites hardest Hand-written tags are usually caught in review because the reviewer is reading the tag anyway. The expensive version is **generated** code. A schema-to-struct generator emits tags by string formatting, and a format string with one stray space — `json: "%s"` — disables the naming on every field of every generated type at once. The generator's own tests may well pass, because they compare Go structs rather than JSON bytes, and the break only surfaces at the boundary where some other service reads the payload. The same applies to a template that emits `json:"%s, omitempty"` for readability. ## The tools that make it loud **`go vet ./...`** — the structtag analyzer checks that struct tags conform to the conventional format, reports duplicate json or xml names declared in the same struct, and reports json or xml tags on unexported fields. It is the single highest-value guard here, it runs in a second, and it belongs in CI rather than in someone's habits. **A round-trip or golden test.** Marshal a fully populated instance and assert the exact JSON bytes, or unmarshal a captured real payload and assert every field is non-zero. Any tag that has gone quiet fails one of those immediately. For generated types, generate the golden test alongside the struct so a change to the generator's format string cannot pass. **Review the generator, not the output.** For code generation the leverage is in the template. One assertion over one generated file catches a class of defect that would otherwise be spread across hundreds of structs. ## The diagnosis, when it has already happened Symptom: a consumer reports a missing field, or a payload suddenly carries Go-style capitalised keys. Do not start with the network. Marshal the struct in a test and print the bytes; the key that comes out will name the tag that failed. Then look at the tag character by character — colon, quote, name, comma, option, quote — and run `go vet` on the package, which will usually have found it before you do.

  • Which of these mistakes does go vet catch, and which does it not?
    The structtag analyzer reports tags that do not conform to the conventional format, duplicate json or xml names in one struct, and json or xml tags on unexported fields. It does not know your intended wire names, so a correctly formed tag naming the wrong field is invisible to it — that needs a test against real JSON.
  • A code generator emits tags and every generated struct lost its wire names at once. Where do you look?
    At the generator's format string, not at the generated files. One stray space in `json: "%s"` disables naming on every field it emits. Fix it there, then add a golden test over one generated file asserting exact JSON bytes so the template can never regress silently again.
  • Why does a wrong tag change behaviour rather than fail the build?
    Because tags are ordinary string literals with no compile-time meaning; the convention is enforced only by whichever library reads them at run time, and that lookup reports absence rather than error. `encoding/json` then applies its documented fallback of using the Go field name.

saying these in an interview costs you the question

  • Expects the compiler to reject a malformed tag
  • Thinks json.Marshal errors on an unparseable tag
  • Writes json:"name, omitempty" and expects omission
  • Assumes the tag key lookup is case-insensitive
  • Relies on review alone instead of vet in CI