What do encoding/json/v2 and encoding/json/jsontext each provide, and does importing encoding/json still behave the same?
answer
- Two new packages, one old one
- Syntax below, Go types above
- One layer never sees a struct
- The old import kept its promises
- Options passed in, not set on an encoder
basics
~10 sencoding/json/jsontext handles JSON syntax: tokens, values, escaping. encoding/json/v2 layers the Go-to-JSON mapping on top of it, with stricter defaults. The original encoding/json keeps its old behaviour, so upgrading the toolchain alone changes nothing.
solid answer
~50 sGo 1.27 shipped two new packages that split a job the old one did in a single layer. `encoding/json/jsontext` is the syntactic layer: it reads and writes JSON tokens and raw values, handles escaping and indentation, and knows nothing about Go types. `encoding/json/v2` is the semantic layer built on it: it maps Go values to JSON, reads struct tags, and its `Marshal` and `Unmarshal` take variadic options instead of a mutable encoder object. The v2 defaults are deliberately stricter than v1 -- duplicate object names and invalid UTF-8 are errors, and member names match case-sensitively. The original `encoding/json` is still there and still documented to behave as it always did, so a rebuild on a newer toolchain does not silently start rejecting traffic; you move a call site by changing its import, and `GOEXPERIMENT=nojsonv2` exists as a build-level escape hatch if a v1 difference bites.
code
go · 11 linesimport (
"encoding/json/jsontext"
"encoding/json/v2" // package name is json
)
func encode(v any) (jsontext.Value, error) {
// semantic layer: a Go value in, JSON out
b, err := json.Marshal(v)
// syntactic layer: a raw chunk of JSON text, no Go types involved
return jsontext.Value(b), err
}go deeper
Be ready to name the three packages and say which is which in one sentence: jsontext for JSON text, encoding/json/v2 for Go values, encoding/json unchanged. Knowing that the old import still behaves as before is the answer interviewers actually want.
Explain the layering as a design decision: syntax options belong to jsontext, binding options to v2, and configuration moved from encoder setter methods to values passed per call. Say why a v2 was needed rather than a patch to v1.
An interviewer expects you to connect the split to operations: because adoption is an import change, it stages per package and per service, and the loosening options are your per-route rollback lever rather than a rebuild.
Own the position that a standard-library v2 is a contract change with everyone who sends you documents, and that the layering is what makes it affordable -- you can buy strictness at the edges without paying for it everywhere.
## The two packages, and why there are two Go 1.27 shipped `encoding/json/v2` and `encoding/json/jsontext` alongside the original `encoding/json`. They had been available for experimentation since Go 1.25 behind `GOEXPERIMENT=jsonv2`. The split matters more than the version number, because it tells you where any given behaviour lives. **`encoding/json/jsontext` is the syntactic layer.** It deals in JSON *text*: tokens (`{`, a string, a number, `]`), raw values, escaping, indentation, whitespace. It has an `Encoder` and a `Decoder`, and a `jsontext.Value` type for a raw chunk of JSON. What it does not have is any notion of a Go struct, a struct tag, or a field name. Anything that is a property of the document rather than of your types -- "are duplicate object names allowed?", "is invalid UTF-8 allowed?", "how is this indented?" -- is a jsontext concern. **`encoding/json/v2` is the semantic layer.** It sits on top of jsontext and does the part programmers actually think of as "JSON in Go": binding a document to Go values. It reads `json:"..."` struct tags, decides which member fills which field, calls custom marshalers, and reports type mismatches. Its top-level functions look like the old ones but end in options: ```go func Marshal(in any, opts ...Options) ([]byte, error) func Unmarshal(in []byte, out any, opts ...Options) error ``` That variadic `Options` parameter is the second structural change. In v1, anything you wanted to configure required a `json.Decoder` or `json.Encoder` object with setter methods, which meant configuration was only reachable on the streaming path. In v2 every knob is a value you pass to any call, and the jsontext options and the v2 options are the same kind of value, so `json.Unmarshal(b, &v, jsontext.AllowDuplicateNames(true))` is a legal single call. ## What happened to the old package The headline worry -- "does upgrading Go start rejecting payloads my service accepts today?" -- has a reassuring answer. `encoding/json` remains, and remains documented to behave as it always has: duplicate object names still resolve last-one-wins, member names still match case-insensitively when there is no exact match, invalid UTF-8 in a Go string is still replaced with the Unicode replacement character U+FFFD rather than reported. Internally it is reimplemented in terms of the new packages -- that is why the two shipped together -- but the observable contract is the old one, and `GOEXPERIMENT=nojsonv2` exists as a build-level opt-out if a program hits an unintended difference. So adoption is per call site. You get the stricter behaviour by changing an import from `encoding/json` to `encoding/json/v2`, one package at a time, and you can leave the rest of a large program alone. There is no global switch that flips a fleet, which is exactly what makes a staged rollout possible. ## Why the strictness changed at all The v1 defaults were chosen in 2011 and several of them are now considered mistakes: silently accepting duplicate object names is an interoperability hazard (two parsers can disagree about which value wins, which is a classic way to smuggle a value past a validator), silently mangling invalid UTF-8 hides data corruption, and case-insensitive matching means a field named `userid` quietly absorbs a member named `USERID`. None of these could be fixed in v1 without breaking working programs, which is the entire reason for a v2 in the standard library rather than a patch. ## Naming and imports The package name of `encoding/json/v2` is `json`, so you cannot import it and the old `encoding/json` into the same file without aliasing one of them -- which is a useful nudge, because most code should be on one or the other in a given package. A file that needs both the semantic and syntactic layers imports both paths: ```go import ( "encoding/json/jsontext" "encoding/json/v2" ) ``` ## What to remember Syntax below, Go types above; strict defaults in the new packages, unchanged defaults in the old one; configuration by value instead of by setter; and migration as an import change rather than a toolchain flag.
- If encoding/json keeps its old behaviour, what is GOEXPERIMENT=nojsonv2 for?It is a build-level opt-out from the new implementation. The old package's semantics are meant to be preserved on top of the new code, but a reimplementation that large can differ in a corner nobody documented. Setting `GOEXPERIMENT=nojsonv2` lets a team unblock a build while the difference is investigated. It is an escape hatch, not a migration strategy: it flips the whole binary and tells you nothing about which package was affected.
- Why do the duplicate-name and invalid-UTF-8 options live in jsontext rather than in encoding/json/v2?Because both are properties of the JSON text, not of the Go binding. Whether `{"a":1,"a":2}` is a legal document, and whether a string may contain invalid UTF-8, can be decided without knowing what Go type is on the other end. Options that need the Go side -- how member names are matched against struct fields, whether unknown members are an error -- live in `encoding/json/v2`. The two option kinds are the same value type, so one call can take both.
- When was encoding/json/v2 first usable, and how did that differ from Go 1.27?Go 1.25 made it available behind `GOEXPERIMENT=jsonv2`, so you could build against it but not rely on it: the API was still open to change and the packages were not part of the compatibility promise. Go 1.27 shipped both `encoding/json/v2` and `encoding/json/jsontext` as ordinary standard-library packages, with `GOEXPERIMENT=nojsonv2` as the opt-out in the other direction.
Think of a compiler's lexer and parser versus its type checker: jsontext tokenises and spells JSON correctly, and v2 is the layer that decides what those tokens mean for your declared types.
saying these in an interview costs you the question
- Says upgrading to Go 1.27 makes encoding/json reject duplicate keys
- Thinks encoding/json is deprecated and must be migrated now
- Describes jsontext as a drop-in replacement for Marshal and Unmarshal
- Cannot say which of the two layers knows about struct tags
- Believes a GOEXPERIMENT flag is the normal way to adopt v2