encoding/json/v2: add string option hint optimization

Starting with CL 779300, we return an error if fields with a `string`
tag do not have a supported type.

Most field validation occurs in makeStructFields. makeStructFields runs
in a per-type sync.Once, with errors reported during each
Marshal/Unmarshal call before any marshaling or unmarshaling begins.

The string validation semantics depend on the
StringifyWithLegacySemantics option, so the full validation cannot occur
inside the sync.Once. Instead each Marshal/Unmarshal call must loop over
every field to perform validation. We must use a second loop over the
fields rather than performing validation inline inside the main marshal
loop to match the other field validation semantic of reporting errors
before marshaling begins.

The loop isn't very expensive, but it does double the number of times we
need to loop at each struct field. Package benchmarks did not show any
statistically significant regressions on specific benchmarks (all
regressions I saw disappear when running just that benchmark with more
iterations). Regardless, BenchmarkUnmarshal/Struct/ManySmall spends ~2%
of CPU time on this loop.

Most structs won't have any fields with a string tag, so add a simple
hint for when at least one field has the tag, and skip the validation
loop otherwise.

For #79065.

Change-Id: I6f8b7d8c120c43e68b32dd1b19107d266a6a6964
Reviewed-on: https://go-review.googlesource.com/c/go/+/779940
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Michael Pratt <mpratt@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
This commit is contained in:
Michael Pratt 2026-05-19 12:34:46 -04:00 committed by Gopher Robot
parent 469636308b
commit 4b77d329ea
2 changed files with 10 additions and 2 deletions

View file

@ -1085,7 +1085,7 @@ func makeStructArshaler(t reflect.Type) *arshaler {
// This validation is effectively a makeStructFields error that
// occurs before any marshalling begins, but since it depends
// on the marshal options it can't be part of the sync.Once.
if !mo.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
if fields.hasString && !mo.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
for i := range fields.flattened {
f := &fields.flattened[i]
if f.string {
@ -1290,7 +1290,7 @@ func makeStructArshaler(t reflect.Type) *arshaler {
// This validation is effectively a makeStructFields error that
// occurs before any marshalling begins, but since it depends
// on the marshal options it can't be part of the sync.Once.
if !uo.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
if fields.hasString && !uo.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
for i := range fields.flattened {
f := &fields.flattened[i]
if f.string {

View file

@ -34,6 +34,7 @@ type structFields struct {
byActualName map[string]*structField
byFoldedName map[string][]*structField
inlinedFallback *structField
hasString bool // one or more fields set the string option
}
// reindex recomputes index to avoid bounds check during runtime.
@ -81,6 +82,9 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
return cmp.Or(serr, &SemanticError{GoType: t, Err: fmt.Errorf(f, a...)})
}
// Whether any field sets the string option.
var hasString bool
// Setup a queue for a breadth-first search.
var queueIndex int
type queueEntry struct {
@ -248,6 +252,9 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
f.id = len(allFields)
f.fncs = lookupArshaler(sf.Type)
allFields = append(allFields, f)
if f.string {
hasString = true
}
}
}
@ -313,6 +320,7 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
flattened: flattened,
byActualName: make(map[string]*structField, len(flattened)),
byFoldedName: make(map[string][]*structField, len(flattened)),
hasString: hasString,
}
for i, f := range fs.flattened {
foldedName := string(foldName([]byte(f.name)))