What does encoding/json/v2 reject or match differently by default compared with encoding/json?
answer
- Three defaults, three restore options
- Two of them are about the text
- Repeat a name, get an error
- Bad bytes stop becoming U+FFFD quietly
- Exact match only; the fallback is gone
basics
~10 sencoding/json/v2 rejects duplicate object names and invalid UTF-8, which encoding/json accepts (last one wins, and invalid bytes become U+FFFD). It also matches member names to struct fields case-sensitively, with no case-insensitive fallback.
solid answer
~40 sThree defaults changed. Duplicate object names are an error in `encoding/json/v2`, where `encoding/json` silently lets the last occurrence win. Invalid UTF-8 is an error, where v1 substitutes the replacement character U+FFFD on the way out and tolerates bad bytes on the way in. And member names match struct fields case-sensitively: v1 preferred an exact match but fell back to a case-insensitive one, so a document member `USERID` would fill a field tagged `userid`; in v2 it simply does not match, and the member is treated as unknown. Each is restorable per call, and where the option lives tells you which layer owns the behaviour: `jsontext.AllowDuplicateNames(true)` and `jsontext.AllowInvalidUTF8(true)` are syntactic, `json.MatchCaseInsensitiveNames(true)` is about the Go binding. That per-call granularity is what makes a route-by-route rollout possible.
code
go · 11 linestype Order struct {
ID string `json:"id"`
Qty int `json:"qty"`
}
data := []byte(`{"id":"a1","qty":1,"qty":2}`)
var o Order
err := json.Unmarshal(data, &o)
// encoding/json: err == nil, o.Qty == 2 (last one wins)
// encoding/json/v2: err != nil, reporting the duplicate name "qty"go deeper
Be able to list the three: duplicate object names rejected, invalid UTF-8 rejected, names matched case-sensitively. Knowing the direction of each change is enough at this level.
Explain the mechanics and where each restore option comes from, and why two live in jsontext and one in encoding/json/v2. Interviewers here want the reason duplicates are dangerous, not just the fact.
Show which of the three fails loudly and which fails silently, and design the migration test accordingly: assert decoded field values, not merely that Unmarshal returned nil.
Frame the defaults as a contract with senders you do not control. Decide where strictness is worth rejecting real traffic for -- typically inbound edges -- and where a documented, expiring loosening option is the cheaper answer.
## The three changed defaults `encoding/json/v2` is not a rewrite for its own sake; several of its defaults are the ones the original package would have had if it were written today. Three of them change what a program accepts. ### 1. Duplicate object names are an error Given `{"qty":1,"qty":2}`, `encoding/json` decodes without complaint and the last occurrence wins, so the field ends up 2. `encoding/json/v2` returns an error identifying the duplicated name. This is an interoperability and security fix, not pedantry. RFC 8259 says object names *should* be unique but does not require a parser to reject repeats, and implementations disagree about which one wins -- some take the first, some the last, some concatenate. Any system where one component validates a document and another consumes it can be split by that disagreement: the validator reads the harmless first `role`, the consumer reads the second. Rejecting the document removes the question. Restore the old tolerance with `jsontext.AllowDuplicateNames(true)`. ### 2. Invalid UTF-8 is an error Go strings are arbitrary bytes, not guaranteed UTF-8, while JSON strings are text. `encoding/json` resolves that mismatch by silently replacing each invalid byte with U+FFFD, the Unicode replacement character, when marshaling, and is similarly permissive when decoding. The corruption is irreversible and invisible: nothing in the API tells you it happened. `encoding/json/v2` reports an error instead, so a byte slice that is not valid UTF-8 stops being quietly laundered into replacement characters. Restore the old behaviour with `jsontext.AllowInvalidUTF8(true)`. ### 3. Member names match case-sensitively When v1 binds a document member to a struct field, it first looks for an exact match against the tag name (or field name), and if there is none it falls back to a case-insensitive comparison. That fallback is why `{"USERID": 7}` fills a field tagged `json:"userid"`, and why two fields differing only in case are a coin flip. v2 drops the fallback: names match exactly or not at all. The practical consequence is not an error but a *silence* -- with the default handling of unrecognised members, a case-mismatched name is simply not matched, so the field keeps its zero value. That makes this the most dangerous of the three during a migration, because the other two announce themselves and this one does not. Restore the fallback with `json.MatchCaseInsensitiveNames(true)`. ## Where an option lives tells you what it is Notice the package split. Duplicate names and invalid UTF-8 are properties of the JSON *text*, decidable without knowing the destination type, so their options come from `encoding/json/jsontext`. Case-insensitive matching only means something once there is a Go struct to match against, so its option comes from `encoding/json/v2`. Both are the same kind of value and can be passed to one call: ```go err := json.Unmarshal(data, &v, jsontext.AllowDuplicateNames(true), json.MatchCaseInsensitiveNames(true), ) ``` ## Why per-call options matter more than they look In v1, configuration lived on a `Decoder` or `Encoder` object, so it was only reachable if you were already on the streaming path and it applied to everything that object touched. v2's options are values passed to any call. That is what turns a strictness upgrade from a binary decision into a schedule: one route that talks to a partner known to send duplicate names can pass `jsontext.AllowDuplicateNames(true)` while every other route in the same binary stays strict, and the option sits in the code as a visible, greppable, removable piece of debt rather than a build flag. ## What has *not* changed Unrecognised members are still ignored by default in v2, just as in v1 -- strictness about names is not the same as strictness about *unknown* names, and the two are separate decisions. The old `encoding/json` package also keeps every one of the three behaviours above; you opt into strictness by importing v2 at a call site, not by upgrading the toolchain. ## The summary that answers the interview question Duplicates rejected, invalid UTF-8 rejected, names matched exactly. Two of the three shout when they change behaviour; the third goes quiet, and that is the one to test for.
- Why is silently accepting duplicate object names considered a security problem, not just untidiness?Because parsers disagree about which occurrence wins. If a validator and the service behind it use different libraries, a document can carry two values for the same name and be judged on one while acting on the other -- the classic parser-differential smuggle. Rejecting the document makes the disagreement impossible rather than merely unlikely, which is why v2 made it the default.
- Of the three changed defaults, which is most likely to break a service quietly, and why?Case-sensitive matching. A duplicate name or invalid UTF-8 produces an error you will see in logs and metrics. A member whose case no longer matches a struct tag simply goes unmatched, the field keeps its zero value, and the request succeeds with missing data. That is why a migration test should assert decoded values, not just the absence of an error.
- Does encoding/json/v2 also reject members that match no struct field?No. Unrecognised members are ignored by default, exactly as in the original package; rejecting them is a separate opt-in decision. Strictness about how names are matched and strictness about names you do not know are independent choices, and v2 only changed the first one's default.
saying these in an interview costs you the question
- Says v2 rejects unknown members by default
- Thinks invalid UTF-8 was already an error in encoding/json
- Claims the duplicate-name rule is configurable only at build time
- Says a case-mismatched member produces an error in v2
- Believes the first duplicate wins in encoding/json