When should a fleet move from encoding/json to encoding/json/v2, and who owns the traffic it starts rejecting?
answer
- A contract change wearing an import's clothes
- Two commitments: toolchain and strictness
- Adopt by position relative to the boundary
- The partner-facing team holds the veto
- It changes what you emit, too
basics
~20 sAdopt it where strictness is the product -- inbound trust boundaries -- and leave interior and round-tripping code alone. It is an import change, so it stages per service. The team facing the sender owns the rejections, so they hold a veto and a documented per-call loosening option.
solid answer
~60 sTreat it as a contract change, not a library upgrade. The technical move is small -- change an import, raise the module's `go` directive to a version that has the package -- but the effect is that documents your partners send today start being refused, and the team that answers the partner's phone call is not the platform team that scheduled the change. So the policy I would write has three parts. First, adopt by *position*: at inbound edges where rejecting a malformed document is the whole point, and not in interior services or in code that round-trips third-party documents it did not author. Second, adopt on *evidence*: no route flips until a shadow decode over real traffic has produced a list of named senders and classes. Third, make the exception legitimate: the per-call loosening options are an approved, greppable, dated escape hatch owned by the service team, which means they can hold the line against a schedule while the platform team keeps the fleet-wide direction. What I would not do is treat `GOEXPERIMENT=nojsonv2` as fleet policy.
go deeper
Know that moving to the new package is an import change per call site rather than a global switch, and that it can make a service refuse documents it accepted yesterday.
Be able to separate the two commitments: raising the module's go directive to a toolchain that has the package, and actually changing an import. Explain why only the second one changes behaviour.
Argue adoption by position -- inbound edges first, interior code on its own schedule, round-tripping code possibly never -- and insist on shadow-decode evidence before any route flips.
Own the governance: who may set the date, who may refuse it, how an exception is recorded and when it expires, and the fact that the change alters emitted payloads as well as accepted ones.
## What is actually being decided The question sounds like a dependency upgrade and is not. `encoding/json/v2` shipped in Go 1.27, and moving a call site to it costs one import line. What it changes is the set of documents your systems accept, and those documents are produced by people who do not report to you. That makes this a decision about *whose traffic may break, and on whose schedule* -- which is why it belongs to someone who can be overruled by a service team, rather than to whoever is doing the upgrade. Two commitments are entangled and worth separating. **The toolchain commitment.** The package exists only from Go 1.27, so a module using it must declare a `go` directive of at least that version. In Go 1.27 `go test` runs the `stdversion` vet check by default, so a module still declaring an older `go` line will be told it is using a standard-library package newer than it claims to support. That is a real cost for a fleet with a conservative pinning policy, and it is separable: a team can be on the toolchain for a year before it touches an import. **The strictness commitment.** Independent of the version, this is the one with a blast radius, and it is per call site. ## Adopt by position, not by percentage The useful axis is where a piece of code sits relative to a trust boundary. *Inbound edges* -- a gateway validating payloads before forwarding them, a webhook receiver, anything parsing a document from outside -- are where strictness is the product. Rejecting a document with duplicate object names is precisely what you want at the place where two components could otherwise disagree about which value counts. Adopt here first. *Interior services* that decode documents your own code encoded gain little. The producer is under your control, so the class of defect v2 rejects mostly cannot occur, and the migration risk -- particularly the quiet one, a member that used to match case-insensitively and now does not -- is pure cost. Adopt here on the ordinary schedule of touching that code, not on a campaign. *Round-tripping code* -- a proxy that reads a third-party document, modifies one member and forwards the rest -- is the case to be most careful with, because you are not the author of what passes through and you may not be entitled to refuse it. Keeping the older package here can be the correct permanent answer, and a policy that cannot express "never, deliberately" will be routed around. ## Evidence before schedule A campaign that says "all services on v2 by Q3" without measurement converts an unknown number of partner outages into a deadline. The precondition I would put in writing is that no inbound route flips until a shadow decode over live traffic has produced a list: which sender, which class of rejection, on which route. That list is the artefact the decision actually needs, because it turns "strictness might break things" into a set of named conversations with dates. ## Who holds the veto The platform owner sets direction; the team facing the partner owns the rejection. Formalise that rather than resolving it in an incident. The mechanism is already in the API: the loosening options are per call, so a service team can adopt v2 everywhere and pass `jsontext.AllowDuplicateNames(true)` on the one route whose partner is still fixing an encoder. That exception should carry a comment naming the partner and the date it is expected to go, and it should be reviewed like any other dated debt. What should not be policy is the build-level opt-out. Rebuilding a service with `GOEXPERIMENT=nojsonv2` reverts an entire implementation for reasons nobody records, hides which package was affected, and does not even undo an import that was already changed. It is a diagnostic tool for a suspected regression in the reimplementation of the old package, not a governance instrument. ## The part that is not about decoding One more cost belongs in the decision because it is easy to miss: the redefinition of `omitempty` changes what services *emit*, not only what they accept. A service that adopts v2 for its own strictness starts putting `"verbose":false` on the wire where it previously omitted the member. That is a change to a published payload, and if consumers distinguish absent from false, the adopting team has just modified an interface it may not own. A policy that only talks about rejection has covered half the change. ## The position to defend Strictness is worth buying at the boundary and rarely worth a campaign in the interior; measurement precedes schedule; the exception mechanism belongs to the team holding the relationship; and the toolchain pin is a separate, cheaper decision that can be made first.
- How do the toolchain decision and the strictness decision differ in cost?The toolchain decision is internal and reversible: raise the module's `go` directive, satisfy the `stdversion` vet check that Go 1.27 runs by default, and nothing about behaviour changes until an import does. The strictness decision is external and not reversible on your own timetable, because it changes what other people's documents are allowed to look like. Making the first one early buys the option to make the second one per service.
- A platform campaign wants every service on encoding/json/v2 this quarter. What would you push back on?The absence of evidence and the absence of an exception path. I would ask for shadow-decode results per inbound route before any date is set, exempt code that round-trips documents it does not author, and write down that the per-call loosening options are approved for a partner-facing team to use with an expiry. A campaign without those turns partner outages into a deadline the platform team does not absorb.
- Why does adopting encoding/json/v2 also change what a service sends?Because `omitempty` is redefined against the encoded JSON rather than the Go value, so a false boolean or a zero number that used to be omitted is now emitted. If any consumer distinguishes an absent member from an explicit false, the adopting team has changed a published payload. That belongs in the same review as the decoding change, and it is the half most adoption plans forget.
saying these in an interview costs you the question
- Treats the move as a routine dependency bump
- Sets a fleet-wide deadline before any traffic has been measured
- Makes GOEXPERIMENT=nojsonv2 the standing fleet policy
- Gives the platform team the final word over partner-facing teams
- Ignores that the change also alters emitted payloads
- Has no expiry attached to a loosening option left in code