Repository navigation
encoding/json: potential compatibility issues with "inline" tag on existing types #79985
Description
Go 1.27 adds a new JSON "inline" struct tag as part of #71497. This tag specifies that the JSON object representation of this field is to be promoted as if it were specified in the parent struct. It is the JSON equivalent of Go struct embedding. A Go embedded field is implicitly inlined unless an explicit JSON name is specified.
In Go 1.27, this tag is supported by both the encoding/json and encoding/json/v2 APIs. Support in encoding/json is intentional to ensure that packages using new tags on their types don't implicitly require all of their dependents switch to the encoding/json/v2 API.
Even though this tag doesn't exist in 1.26, it turns out that it has widespread use throughout public Go code, particularly within the Kubernetes community. https://grep.app/search?q=%60json%3A%22%2Cinline%22%60 shows ~29k results for uses on the "inline" tag.
I do not know the original source of this tag in Kubernetes. It seems to have originated at some point and then got copied around. In fact, @liggitt has been removing uses of this tag since it didn't actually do anything (kubernetes/kubernetes#138260).
Regardless, Kubernetes uses encoding/json and many of these tags still exist, so there is a risk of behavior change when the 1.27 toolchain upgrade occurs and encoding/json gains support for this tag.
29k uses sounds dire, but the vast majority of uses apply the tag to an embedded field. Embedded fields are already inlined, so the "inline" tag has no impact to these fields.
I analyzed modules in the Go module proxy with at 3 transitive importers for uses of the "inline" tag on named fields. The full set of 2639 findings are here.
While I have not looked at every finding, some things I've found so far:
- A large proportion of uses come from
github.com/openai/openai-goor copies of this code. This module uses a custom json package (github.com/openai/openai-go/internal/apijson) that supports an "inline" tag with semantics similar to Go 1.27. It does not appear that these types are ever used withencoding/json. Rudimentary filtering of the findings to exclude these packages reduces the list to 694 findings. The full filtered list is here. - https://go-mod-viewer.appspot.com/knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/apis/volatile_time.go#L34 applies an "inline" tag, but also implements a MarshalJSON method that manually implements the inline behavior. I've seen this pattern on several different types. This should be fine in 1.27 since MarshalJSON will render "inline" useless.
- https://go-mod-viewer.appspot.com/github.com/chainreactors/fingers@v1.2.1/fingers/fingers.go#L18 is affected as far as I can tell. This module has a
LoadFingersfunction that unmarshals this type withencoding/json, which 1.27 will do differently. Ironically I can't find any tests in this module that actually care about the inlined fields, so I don't have confirmation that it is broken. - The most frequent case I see are cases like https://go-mod-viewer.appspot.com/github.com/rancher/opni-monitoring@v0.4.0/pkg/sdk/api/v1beta1/gateway_types.go#L36, where marshaling of this type is definitely affected by 1.27, but the code is too abstract for me to easily determine whether the type is ever used with
encoding/json.
All told, I have not yet found any cases that are definitively broken by a 1.27 behavior change, but I do not have high confidence that there are none.
I'm creating this issue to discuss what we should do about this. Currently I see four options:
- Simply do nothing because we don't believe that this will cause ecosystem disruption.
- Rename the "inline" tag to something with no existing use in the ecosystem to avoid potential conflicts.
- Drop the "inline" tag entirely, and reconsider it for 1.28.
- Add a compatibility GODEBUG that gates the "inline" tag behind the language version. See the full description of this below.
Activity
prattmic commentedon Jun 12, 2026
My proposal for a compatibility GODEBUG is:
We define a new json127tags GODEBUG option. This option defaults to 0 if language version is 1.26 or lower, and 1 if the language version is 1.27 or higher.
If GODEBUG=json127tags=0, then encoding/json completely ignores the new struct tags (case:ignore, case:strict, inline), as in 1.26 today. The non-”inline” new tags are included for consistency.
If GODEBUG=json127tags=1, then encoding/json supports these tags, as implemented at tip today.
For encoding/json/v2, if GODEBUG=json127tags=0, the presence of one of these tags results in a run time error which notes that these tags require language version 1.27.
If GODEBUG=json127tags=1, then encoding/json/v2 supports these tags, as implemented at tip today.
The basic idea here is to emulate as if these tags were actual language features. We can't fail the build on their use, but we can have v2 report an error on use.
When upgrading to a 1.27 toolchain, bad “inline” tags can’t cause compatibility issues. Any issues are deferred until the main module chooses to upgrade to the 1.27 language version.
This actually has a nice property beyond the “inline” compatibility concern. Today I may upgrade to a 1.27 toolchain and start using the new tags that I find useful. I am not required to increase the language version, so my module remains at language version 1.26. Users of my module may be using a 1.26 toolchain, and thus not support the new tags. My types will thus not marshal as expected. If the new tags are guarded behind language version 1.27, then I must upgrade my module’s language version, which will in turn require my users to do the same.
I think this makes a solid argument for putting any new tags behind the language version. This is a new idea, we did not do this for “omitzero”, even though the same argument applies there. With typed struct tags (#74472) this problem goes away as the new toolchain will be required to resolve the tag’s symbol.
One downside of this GODEBUG idea is that compatibility GODEBUGs change behavior across the entire program, not on a per-module basis. So while bad “inline” tags won’t break things when upgrading the toolchain, they will break things when upgrading the language version. The bad tag may be deep in a transitive dependency with a lower language version, so it could be difficult to find the problematic type.
The bisect tool supports a
-godebugflag to add a bisect string to the GODEBUG. encoding/json could use the bisect string to decide which types enable “inline” support so the bisect tool can pinpoint the problematic type. This would help, but it is still a pretty annoying and niche option.dims commentedon Jun 12, 2026
It started here i think:
it was a mirror of the functional
yaml:",inline"prattmic commentedon Jun 12, 2026
Thanks @dims, that's helpful (also an impressively early Kubernetes PR!). If I understand that PR correctly, the idea is that the types can be given as JSON, since
gopkg.in/v1/yamlcan unmarshal JSON (JSON is valid YAML). But the tag looks like a misunderstanding, asgopkg.in/v1/yamlappears to always use theyamltag even if the input happens to be JSON.neild commentedon Jun 12, 2026
I don't really like the idea of a version-gated GODEBUG, but I'm not adamantly opposed. A GODEBUG has a few problems that worry me:
I think my preferences are to change the tag name to "embed" (which hopefully doesn't have any conflicts) or disable it for 1.27 and hopefully add it back in as a structured tag in 1.28. This assumes we manage to get structured tags in 1.28, of course, but signs are hopeful for that at this time.
prattmic commentedon Jun 12, 2026
The name "embed" does have a lot going for it, as "inline" already describes itself with "It is the JSON equivalent of Go struct embedding. A Go embedded field is implicitly inlined unless an explicit JSON name is specified."
My quick search attempt did not find any uses of the name. It did find a use of inline with a comment saying embed. 😃 https://grep.app/search?regexp=true&q=%60json%3A%22.*%2C.*embed
ChrisHines commentedon Jun 12, 2026
I have no problem with switching to "embed". FWIW, during the design discussions around this feature I often forgot what the semantics of the "inline" tag were and had to be reminded by others. Aligning the tag with the Go embedding feature makes it easier for a Gopher like me to remember. :)
gabyhelp commentedon Jun 13, 2026
Related Issues
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
dsnet commentedon Jun 13, 2026
Some thoughts:
inlinefor Go 1.27.embedsounds fine to me. In general, I'd prefer to use JSON terminology for features, but there is nothing in the RFCs that's remotely like this. Given thatembedoperates most closely to Go's embed feature and arguably is semantically closely tied with it, naming it after Go terminology makes sense. The terminlinelacks any shared heritage with both Go and JSON.Given that https://go.dev/cl/788864 makes it such that invalid usages of
inlinestill operate like normal fields, do we still anticipate more compatibility issues?prattmic commentedon Jun 15, 2026
The cases I'm worried about are ones like https://go-mod-viewer.appspot.com/github.com/rancher/opni-monitoring@v0.4.0/pkg/sdk/api/v1beta1/gateway_types.go#L36, where the use of "inline" is valid, but was placed there by mistake and never mattered because 1.26 ignores the tag.
liggitt commentedon Jun 16, 2026
Exactly ... the only good outcomes require someone to notice the tag wasn't doing what they intended and fixing it (either by dropping the
inlinetag to keep compat with their existing serializations, or by modifying their Go struct to actually embed to get the serialization behavior they wanted). That particular example did end up embedding the field later if you trace through to the current repos for that type.Silently changing serialization behavior of a widely-used-but-inert-tag when switching a module from building with Go 1.26 to Go 1.27 will almost certainly break someone.
For my own projects (e.g. Kubernetes), we added linters to flag use of
inlinewith non-embedded struct fields, so I'm not personally invested either way, but going with a tag likeembedwe think has ~zero existing use would definitely be safer for the ecosystem.dsnet commentedon Jun 16, 2026
Thanks @prattmic and @liggitt for the explanation. I propose that we switch to using name
embedfor the feature.I scanned the latest version of all modules known by the Go module proxy:
jsonfield tagsinlinefield tagembedfield tagFor some history, when I ran this analysis back in December of 2021:
jsonfield tagsinlinefield taggopherbot commentedon Jun 16, 2026
Change https://go.dev/cl/791440 mentions this issue:
encoding/json/v2: renameinlinetag option asembed``prattmic commentedon Jun 17, 2026
I'm fine with renaming to "embed". I initially leaned against it to avoid making a last minute API change, but if we're going to make a change, now is the time to do it.
encoding/json/v2: rename `inline` tag option as `embed`
SA5008: add support for `embed` tag option
SA5008: add support for `embed` tag option
SA5008: add support for `embed` tag option