BuildNot yet confirmed elsewhere1 publisher3 min readPublished
Upgrading to Go 1.27 changes the engine under encoding/json before you touch an import
Go 1.27 now runs encoding/json on the v2 engine, so error message text can change on upgrade, according to a dev.to post. Changing an import to encoding/json/v2 flips four defaults, and some of the changes raise no error.
The Engineer · Build desk

Three of the six differences between encoding/json and encoding/json/v2 in the post's run raise no error: nil slices, omitempty and case matching. The other three, duplicate names, invalid UTF-8 and time.Duration, produce an error in v2.
- Differences with no error in v2, claim 19 3 of 6 differences 50% computed
Differences with no error in v2 The rest of the 6
| Share | Part | Whole | Percent | Claim |
|---|---|---|---|---|
| Differences with no error in v2 | 3 differences | 6 differences | 50% (computed) | 19 |
What happened
- The v1 API stays supported, and nobody is required to migrate to encoding/json/v2.
- If an upgrade breaks something, setting GOEXPERIMENT=nojsonv2 brings back the original code, though the release notes say a future release is expected to drop that opt-out.
- In the post's own run on a four-field struct, six behaviours differ between encoding/json and encoding/json/v2.
- The encoding/json documentation has a "Migrating to v2" section that lists every behaviour difference and the option that controls it.
Why it matters
- constraint Any test that compares err.Error() to a fixed string can fail on upgrade with no behaviour change, and the fix is to assert on error types.
- exposure Services that accept inconsistently cased keys from clients get empty fields and a nil error under v2, so the failure appears downstream as missing data.
- decision Each package moved to v2 forces a per-behaviour choice: take the new default, or pass the option that restores the old one, such as FormatNilSliceAsNull.
The upgrade and the import change are different operations. The post says that since Go 1.27 the old package runs on the v2 engine with compatibility options switched on [2]. The release notes, as the post quotes them, say encoding/json "is now backed by the v2 implementation. Marshaling and unmarshaling behavior is preserved, but the exact text of error messages may differ." [3]
The import change is where output moves. The post's test program pushes the same values through both packages using a four-field User struct [1]. One line of its output shows two changes at once. v1 wrote {"name":"ana","tags":null}. v2 wrote {"name":"ana","tags":[],"admin":false,"retries":0} [7].
The second change comes from omitempty. In v1 it skips fields holding false, 0, or a nil pointer or interface. v2 skips a field only when its encoding would be null, "", {} or [] [9]. The package documentation, per the post, says existing uses on a bool, number, pointer or interface "should migrate to specifying omitzero instead" [10].
Count the differences in that run and you get six [18]. Three of them fail loudly: duplicate names, invalid UTF-8 and time.Duration [19]. On invalid UTF-8, v1 returned "caf�" with a nil error, and v2 returns an error naming the path "/name" and offset 12 [15]. On time.Duration, v2 fails with "no default representation" [16].
The other three change data and say nothing: nil slices, omitempty and case matching [19]. For case, v2 leaves the field empty and returns a nil error. The post says you will learn about it from missing data, not from logs [11]. No grep finds a missing field.
Duplicate keys sit in between. v1 let the last "name" win, so {"name":"alice","name":"mallory"} became mallory. v2 rejects the input unless you pass jsontext.AllowDuplicateNames(true) [12]. The post points to the security section of the v2 documentation, which explains why this matters when two services parse the same request differently [14]. After the v2 error, the destination already held "alice" [13]. We would not reuse that struct.
One caveat on the numbers. This is one struct on one run. It shows which behaviours exist, not how many of your payloads hit them. Whether six differences matter to you depends on whether your clients send mixed-case keys, whether downstream code checks === null on a field your Go service leaves nil, and whether anything marshals a time.Duration. The post does not show the v1 error strings before and after the upgrade, so the size of the error-text drift is unmeasured.
In our view the order is: upgrade, move tests to error types, then migrate packages to v2 one at a time. That is the right order for services that take payloads from clients you do not control. The upgrade has one known seam, and the import change has six [18].
What to watch
- The release that removes the GOEXPERIMENT=nojsonv2 opt-out, which ends the one-line rollback for the engine swap.
- A published before-and-after list of v1 error strings under Go 1.27, which would size the drift that the post does not measure.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence60
- Adoption
- Insufficient
- Hype gap−5
- Incentives
- Insufficient
- Confidence62
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
The post's program marshals and unmarshals the same values with encoding/json and encoding/json/v2 using a User struct with four fields: Name, Tags, Admin and Retries.
ReportedSupportedSource: dev.to post by ImportStatic2 sources— create a free account to open themView cited source - [2]
Since Go 1.27, the old encoding/json package is implemented on top of the v2 engine with a set of compatibility options switched on.
- [3]
The Go 1.27 release notes, as quoted in the post, say encoding/json "is now backed by the v2 implementation. Marshaling and unmarshaling behavior is preserved, but the exact text of error messages may differ."
- [4]
The v1 API stays supported and nobody is required to migrate to encoding/json/v2.
- [5]
Building with GOEXPERIMENT=nojsonv2 restores the original encoding/json code, but the release notes say that opt-out "is expected to be removed in a future release."
ReportedSupportedSource: dev.to post by ImportStatic, quoting the Go 1.27 release notesView cited source - [6]
Tests that compare err.Error() against a fixed string can fail after the upgrade without any behaviour change; the post's fix is to assert on error types such as *json.SyntaxError or *json.UnmarshalTypeError instead.
- [7]
In the post's run, v1 marshaled the User struct as {"name":"ana","tags":null} and v2 marshaled it as {"name":"ana","tags":[],"admin":false,"retries":0}.
- [8]
json.FormatNilSliceAsNull(true) and json.FormatNilMapAsNull(true) bring the old null output for nil slices and maps back; v2 otherwise writes [] and {}.
- [9]
In v1, omitempty drops false, 0, nil pointers and nil interfaces. In v2 it drops a field only if it would encode as JSON null, "", {} or [].
- [10]
The documentation says existing omitempty uses on a bool, number, pointer or interface "should migrate to specifying omitzero instead", which works the same in both versions.
ReportedSupportedSource: encoding/json documentation, as described in the dev.to postView cited source - [11]
v1 put {"NAME":"bob"} into the Name field; v2 matches names exactly, so the field stays empty and no error is returned. The post says users would find out from missing data, not from logs. json.MatchCaseInsensitiveNames(true) restores loose matching for a call, and the case:ignore tag option does it per field.
- [12]
v1 let the last "name" win, so {"name":"alice","name":"mallory"} became mallory; v2 rejects duplicate names unless jsontext.AllowDuplicateNames(true) is passed.
- [13]
After the v2 duplicate-name error, the destination struct already held "alice".
- [14]
The v2 documentation's security section explains why duplicate keys matter when two services parse the same request differently.
ReportedSupportedSource: dev.to post by ImportStatic, describing the v2 documentationView cited source - [15]
On invalid UTF-8, v1 returned "caf�" with a nil error, while v2 returned the error: jsontext: invalid UTF-8 within "/name" after offset 12.
- [16]
Marshaling a time.Duration under v2 fails with: json: unable to marshal from Go time.Duration within "/timeout": no default representation.
- [17]
The package documentation for encoding/json has a "Migrating to v2" section listing every behaviour difference and the option that controls it.
- [18]
The post's run shows six distinct v1/v2 differences: nil slices, omitempty on bool/int, case matching, duplicate names, invalid UTF-8, and time.Duration.
- [19]
Three of the six differences produce an error in v2 (duplicate names, invalid UTF-8, time.Duration); the other three (nil slices, omitempty, case matching) change output or input handling with no error shown in the post's run.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toGo 1.27's JSON v2 is already under your encoding/json
1 article · October 11, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.