Resolve json tag names through encoding/json - #128
Merged
Merged
Conversation
Go 1.27 rebuilt encoding/json on top of encoding/json/v2 and changed which json: tag names it accepts. An emoji or a control character now names a key where it used to be ignored, and a name holding a quote or a backslash is truncated there rather than discarded. stdocs carried its own copy of the old rule, so on Go 1.27 it documented Emoji for a field the wire called 🚀 — a wrong contract that still built, validated, and rendered. The rule is no longer reimplemented, because it is not stable enough to mirror: parseJSONTag now asks encoding/json which key a tag produces by marshalling a synthesized field carrying it. The answer comes from the same encoding/json that will marshal the caller's values, so the schema tracks whichever toolchain compiled them, on 1.24 through 1.27 and through whatever changes next. Answers are cached per tag. The tag is probed under two differently named fields. A tag that really names a key yields that key both times; one encoding/json ignores yields each probe its own field name, and the disagreement is what signals the fallback. A single probe would have had to reserve a name to signal it, and a real tag can always spell that name — a field tagged json:"StdocsWireProbe" would then have been read as a fallback and documented under its Go name instead. TestInvalidJSONTagNames pinned the rule that just changed, so it is replaced by a table that compares the documented keys against a live marshal instead of against a fixed expectation. It covers the tags it used to, plus embedding, a name that collides once truncated, and a tag spelling a probe's own field name. ExampleWithParams reads its numbers as json.Number: Go 1.27 aliases json.RawMessage to jsontext.Value, whose nil value prints as null rather than nothing, which moved the example's output on that release alone. Closes #126
FumingPower3925
force-pushed
the
fix-go127-tag-names
branch
from
August 26, 2026 15:41
492b8a9 to
4ba00f3
Compare
Owner
Author
|
Re-triggering CI: the earlier runs were lost to the GitHub Actions outage (major outage 15:11Z–~16:0xZ), not to this change. |
The Lint job installs a pinned golangci-lint and runs it against
whatever `stable` resolves to, which is now Go 1.27. golangci-lint
2.12.2 bundles a typechecker that predates it and rejects the 1.27
standard library outright:
could not import math/rand/v2
(rand.go:213:17: method must have no type parameters)
so the job failed with two typecheck errors in crypto/internal, none of
them in this repository. 2.13.0 added Go 1.27 support and carries the
x/tools releases that understand it; 2.13.1 is the current patch. Both
pins move together, the root module and the YAML round-trip module.
Verified with 2.13.1 against both toolchains, root and nested module:
clean on go1.26.6 and go1.27.0.
Owner
Author
|
Added a second commit: the Lint job pinned golangci-lint 2.12.2, whose bundled typechecker predates Go 1.27 and rejects the 1.27 standard library outright ( That was the last red job: Test, Coverage, Spec validation and Go 1.27.0 all passed on the previous run. (The earlier runs on this PR were lost to the GitHub Actions outage on the 26th, not to this change.) |
FumingPower3925
added a commit
that referenced
this pull request
Aug 27, 2026
Go 1.27 rebuilt encoding/json on top of encoding/json/v2 and changed which json: tag names it accepts. An emoji or a control character now names a key where it used to be ignored, and a name holding a quote or a backslash is truncated there rather than discarded. stdocs carried its own copy of the old rule, so on Go 1.27 it documented Emoji for a field the wire called 🚀 — a wrong contract that still built, validated, and rendered. The rule is no longer reimplemented, because it is not stable enough to mirror: parseJSONTag now asks encoding/json which key a tag produces by marshalling a synthesized field carrying it. The answer comes from the same encoding/json that will marshal the caller's values, so the schema tracks whichever toolchain compiled them, on 1.24 through 1.27 and through whatever changes next. Answers are cached per tag. The tag is probed under two differently named fields. A tag that really names a key yields that key both times; one encoding/json ignores yields each probe its own field name, and the disagreement is what signals the fallback. A single probe would have had to reserve a name to signal it, and a real tag can always spell that name — a field tagged json:"StdocsWireProbe" would then have been read as a fallback and documented under its Go name instead. TestInvalidJSONTagNames pinned the rule that just changed, so it is replaced by a table that compares the documented keys against a live marshal instead of against a fixed expectation. It covers the tags it used to, plus embedding, a name that collides once truncated, and a tag spelling a probe's own field name. ExampleWithParams reads its numbers as json.Number: Go 1.27 aliases json.RawMessage to jsontext.Value, whose nil value prints as null rather than nothing, which moved the example's output on that release alone. Closes #126
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #126.
mainis red on Go 1.27, which joined the version matrix the day it went stable — the matrix derives from go.dev.The break
Go 1.27 rebuilt
encoding/jsonon top ofencoding/json/v2(type RawMessage = jsontext.Value) and changed whichjson:tag names it accepts. stdocs carried a copy of the old rule inisValidTagName, so the documented key and the wire key diverged:json:"🚀"Emoji(field name)🚀Emoji❌json:"a\"b"X(field name)aX❌The behaviour follows the toolchain, not the
godirective, andGODEBUG=jsonv2=0does not restore it. Since the supported floor is Go 1.24, the same source has to produce the right key on 1.24 through 1.27.The fix
Stop mirroring the rule; ask
encoding/jsonfor the answer.parseJSONTagresolves a tag by marshalling a synthesized field that carries it, so the answer comes from the sameencoding/jsonthat will marshal the caller's values. Cached per tag.The tag is probed under two differently named fields. A tag that really names a key yields that key twice; one
encoding/jsonignores yields each probe its own field name, and the disagreement signals the fallback. This matters: a single probe has to reserve a name to signal fallback, and a real tag can always spell it — a field taggedjson:"StdocsWireProbe"would then be documented under its Go name while the wire carriedStdocsWireProbe, and an embedded field would additionally be flattened, because the empty name also drivesembeddedandtaggedininspectField. Both shapes are regression-tested.Deliberately scoped to name resolution. The BFS embedding/dominance logic from #70 still decides conflicts and flattening, and the tests confirm the composition is right: on 1.27 two tags that truncate to the same key collide,
encoding/jsondrops both, and so does stdocs.Tests
TestInvalidJSONTagNamespinned the rule that just changed, so it is replaced byTestTagNamesTrackEncodingJSON— a table that marshals each case with the realencoding/jsonand asserts the schema documents exactly the keys that appear, in both directions. It covers the old test's tags plus embedding, an outer tag shadowing a promoted one, a name that collides once truncated, and a tag spelling a probe's own field name.Because it compares against a live marshal rather than a fixed expectation, it passes on both sides of the change instead of encoding either toolchain's answer.
Verification
Everything below was run under both
GOTOOLCHAIN=go1.26.6andgo1.27.0:gofmt/vet/build, full suite,-race, and the nested YAML module — green on both;golangci-lint0 issues.An adversarial review pass over this change found the forgeable-sentinel bug above before it shipped; it also turned up two wrong-contract shapes that predate this work and reproduce on both toolchains, filed separately as #127.
Note
This unblocks #125 (the Scalar/Swagger bump), which is verified but sitting on red CI for this reason. It rebases onto this and releases as v0.9.3.