A generated struct embeds two types that each tag a field json:"id". What does encoding/json emit?
answer
- embedding flattens, it does not nest
- two claims on one wire name
- shallower beats deeper every time
- a tie is resolved by dropping both
- no error is ever reported for it
basics
~20 sNeither field appears. Embedded structs are flattened into the outer JSON object, but when two promoted fields sit at the same shallowest depth and claim the same name, encoding/json drops both of them and reports no error at all.
solid answer
~50 sAn embedded (anonymous) struct field is not nested — its exported fields are promoted and written into the outer JSON object as if declared there. When two names collide, `encoding/json` amends Go's own visibility rules: the field at the **shallowest depth** wins, so an outer field always beats a promoted one. Among fields tied at that shallowest depth, tagged fields beat untagged ones; if more than one survives, **all of them are dropped**, silently. Two embedded types each tagging `json:"id"` is exactly that case, so the key vanishes from the output and is ignored on input, leaving both fields zero. Nothing errors, and `go vet` will not see it either, because the conflict only exists after promotion. The fixes are to rename one tag, to give one embedded field a tag name so it nests as a sub-object, or to stop embedding and use a named field.
code
go · 11 linestype Base struct {
ID string `json:"id"`
}
type Item struct {
Base
Name string `json:"name"`
}
// json.Marshal(Item{Base{"a"}, "b"})
// {"id":"a","name":"b"}go deeper
Know that embedding flattens an inner struct's exported fields into the same JSON object rather than nesting them, and that tagging the embedded field is what makes it nest.
State the resolution order: shallowest depth first, then tagged over untagged, and a remaining tie means every candidate is dropped with no error. Explain that decoding is silent in the same way.
Diagnose the missing key without reading the network: draw the promoted field set, then reach for a round-trip test rather than vet, which cannot see a conflict created by promotion.
Decide the shape of generated types so the collision cannot arise: whether a shared base is embedded at all, whether its wire names are namespaced, and what test the generator emits alongside every struct.
## Promotion first Embedding a struct type without a field name promotes its fields into the outer type. `encoding/json` follows that promotion literally: the inner type's exported fields are encoded **into the outer JSON object**, not into a nested one. ```go type Base struct { ID string `json:"id"` } type Item struct { Base Name string `json:"name"` } // json.Marshal(Item{Base{"a"}, "b"}) -> {"id":"a","name":"b"} ``` Three details of promotion are worth holding on to: - An embedded field of **pointer-to-struct** type is flattened the same way (a nil one contributes nothing). - An embedded field of **non-struct** type — say `type Weight int` embedded — is treated as an ordinary field whose name is the type name. - Give the embedded field a **JSON tag name** and it stops being flattened: ``Base `json:"base"` `` produces `{"base":{"id":"a"},"name":"b"}`. That is the deliberate way to nest. ## The conflict rules Flattening means two fields can end up claiming the same JSON name, and Go's ordinary promotion rules are not enough (they would make the program ambiguous only when you actually *reference* the field). `encoding/json` therefore defines its own resolution over the flattened set of candidates for a name: 1. **Depth wins.** The field at the shallowest depth is selected. A field declared directly on the outer struct is at depth 0 and always beats anything promoted from depth 1. 2. **At the shallowest depth, tags win.** If any of the tied fields carry a JSON tag name, only those are considered — untagged ones drop out, even if there are several of them. 3. **If exactly one candidate remains, it is used. Otherwise all of them are ignored, and no error occurs.** Two embedded types each declaring `json:"id"` land squarely in rule 3: both are at depth 1, both are tagged, neither dominates, so the name is dropped from the type's field list altogether. ## What that looks like in practice ```go type Meta struct { ID string `json:"id"` } type Audit struct { ID string `json:"id"` } type Record struct { Meta Audit } // json.Marshal(Record{}) -> {} ``` The behaviour is symmetric. On decode, an incoming `"id"` matches no field in the flattened set, so it is discarded and both `Meta.ID` and `Audit.ID` stay at their zero values — a field that quietly stops round-tripping, with a nil error on both sides. ## Why this is a code-generation problem specifically By hand, embedding two types with an overlapping tagged field is unusual enough to catch in review. Generated code makes it routine. A schema-to-struct generator commonly emits a shared base type — identity, timestamps, tenant — and embeds it into every generated struct. The day a schema itself declares a field named `id`, the generator emits a second `id` at the same depth in one of the embedded types, and that resource's identifier disappears from its JSON. Every other generated type is fine, which makes it look like a data problem rather than a structural one. `go vet`'s structtag analyzer does report duplicate json names — but it compares the tags written in a single struct declaration. In `Record` above, the two fields are `Meta` and `Audit`, neither tagged, and vet sees no duplication; simulating promotion across embedded types is outside what that check does. So the guard here is not vet. It is a **round-trip test over generated types**: marshal a fully populated value, unmarshal it back, and assert equality. A cancelled field cannot survive that, and the test is itself cheap to generate. ## The fixes, in order of preference 1. **Rename one of the tags.** If the two identifiers genuinely mean different things, they should not share a wire name: `json:"audit_id"`. This is usually a modelling smell being surfaced by the encoder. 2. **Nest one of them deliberately.** Tag the embedded field: ``Audit `json:"audit"` `` gives `{"id":"...","audit":{"id":"..."}}`. Depth resolves the conflict because the outer `id` is now the only candidate at depth 0 for that name. 3. **Stop embedding.** A named field, ``Audit Audit `json:"audit"` ``, is the same thing said explicitly, and it removes the promotion machinery from the reader's head. 4. **Shadow on purpose.** Declaring ``ID string `json:"id"` `` directly on the outer struct wins by depth and silences the conflict — but it is easy to read as an accident, so document it if you use it. ## What to remember under pressure Flattening is the default and nesting is opt-in; a name collision is resolved by depth, then by tag, and a tie at the end is silence rather than an error. The moment a field that "should be there" is missing from an embedded-heavy struct, stop reading the payload and draw the promoted field set.
- How do you make an embedded struct appear as a nested JSON object instead of being flattened?Give the embedded field a JSON tag name. ``Base `json:"base"` `` stops promotion for encoding purposes and writes `{"base":{...}}` instead of merging the inner fields into the outer object. Without a tag name, an anonymous struct field is always flattened.
- The outer struct declares its own id field and an embedded type also tags one. Which wins?The outer one, on depth. A field declared directly on the struct is at depth 0 and dominates any promoted candidate at depth 1, so it is encoded and the promoted one is simply not in the field set. No error, no ambiguity — the deeper field is shadowed for JSON exactly as it is for ordinary selector access.
- Why does go vet's duplicate-json-name check not catch this?Because it examines the tags written in one struct declaration. In the outer struct the fields are the two embedded type names with no json tags, so there is nothing duplicated to see; the collision only exists after promotion, which the check does not simulate. A round-trip test over the type catches it instead.
- What is the cheapest guard for a generator that embeds a shared base type into every struct?Generate a round-trip test with the struct: populate every field with a distinct non-zero value, marshal, unmarshal into a fresh value, and assert equality. A cancelled field can never round-trip, so the test fails on the first generated type where a schema field collides with the base type.
Two departments both submit a form for the same slot on a shared cover sheet. The clerk has no rule to prefer either, so the slot is left blank and nobody is told.
saying these in an interview costs you the question
- Expects json.Marshal to error on the name collision
- Thinks an embedded struct nests by default
- Believes the first-declared embedded field wins
- Assumes go vet catches conflicts created by promotion
- Thinks a deeper field can beat one declared on the outer struct