Go JSON v2: migrate the contract before the import
Go JSON v2 changes what clients receive and which inputs servers accept. Plan migration around those contracts, with compatibility choices for each boundary.
A Go service can compile after a JSON migration and still break a customer's integration. The import is easy to review. The harder question is whether a response still means the same thing to the software receiving it.
The Go team's November 4 explanation describes encoding/json/v2 as stable and available in Go 1.27. That post is a fresh explanation, not the release date of the compiler. For an existing service, the useful response is to inventory its JSON boundaries before adopting different defaults. A library improvement becomes a product improvement only when the clients can live with the change.
Two kinds of compatibility
Language compatibility asks whether the program builds. Integration compatibility asks whether its consumers still work. The compiler can help with the first; it cannot tell you what a customer's spreadsheet importer expects.
The official migration guide explicitly says migration is optional: encoding/json remains covered by Go's compatibility promise. It also describes compatibility options for using the new API with earlier behavior. There is time to make a deliberate choice, rather than treating continued use of the original import as an incident.
That distinction matters when estimating the work. Replacing an import may take minutes. Finding the owner of an old integration may take longer than changing the service. Those are different tasks and should have separate acceptance criteria.
Start with a list of places JSON crosses a boundary: incoming requests, outgoing responses and stored documents that another version will later read. Give each one an owner. An internal message changed together with both consumers has a different migration cost from a public endpoint used by unknown clients.
An empty collection can carry a different promise
The Go 1.27.0 package reference documents the new collection defaults. For an ordinary slice other than the special byte-slice representation, a nil value is emitted as an empty array; a nil map becomes an empty object. The original defaults emit null.
Consider an illustrative response containing "items": null, compared with one containing "items": []. These are examples of data shapes, not output from a program executed for this article. A hypothetical consumer might treat the first as “not loaded” and the second as “loaded, nothing found.” Another consumer might reject null because it expects an array. Either interpretation is possible in application code.
The new default may be a better starting point for the second consumer. That does not settle the contract for the first. Agree on what the field means before replacing its representation. If “not loaded” is meaningful, model that state deliberately instead of depending on an incidental Go allocation detail.
Keep comparisons close to the consumer. A server-side assertion that two responses decode into the same Go structure can miss the difference that matters to a mobile application. Use the actual client parser where practical, with fixtures stripped of customer data.
Input changes deserve a separate decision
The versioned v2 reference also documents rejection of duplicate object names and invalid UTF-8 by default. These are useful stricter rules. They can also change which requests an established endpoint accepts.
An illustrative object with two role members demonstrates the problem: a security check and a downstream reader could disagree about the value. The migration decision should specify the accepted representation at the boundary and keep the components interpreting it consistent. A compatibility setting is not evidence that ambiguous input is harmless.
For an existing partner integration, first determine whether it actually sends such input. If it does, decide how to reject it clearly and how the partner will correct it. Avoid silently preserving every old behavior just because a global compatibility switch exists. Equally, do not describe a stricter parser as a complete security review of the endpoint.
Change one observable behavior at a time
The Go 1.27.0 v1 reference provides DefaultOptionsV1 in encoding/json. This is the compatibility baseline described in the migration guide for calls through the v2 API. It gives a team a way to separate API adoption from changes in behavior. Exact options should come from the version being deployed, rather than an example written for the earlier experiment.
A practical sequence is to preserve the baseline, select one boundary, then agree which behavior will change there. Record the expected response and error handling. Exercise the consumers, deploy with a way to restore the prior behavior, and inspect failures attributable to that change. Merely counting successful HTTP responses will miss clients that receive a response but cannot use it.
Define rollback in terms of behavior too. Returning to the previous import is one possibility; restoring a boundary's compatibility configuration is another. Decide before a release which evidence would trigger that decision and who owns it.
Sometimes a small migration should stay small
There is a strong case against elaborate migration machinery for a new service or a small application whose clients ship together. If the consumers already expect the new representations, a direct move with focused tests can be reasonable. Building a permanent compatibility framework for an endpoint used by one controlled client may create more maintenance than it saves.
The recommendation is proportional: spend effort where ownership is dispersed or failure is costly. Keep deliberate compatibility at the old boundary while using clearer defaults for new work. AL+'s custom software development offer is relevant when that boundary work is part of a larger integration project.
Finish the migration when the agreed consumers accept the agreed behavior. A changed import alone cannot establish that.
Paul