gin/codec/json/json_benchmark_test.go
Pete Steyert-Woods 091584830b feat(codec/json): add opt-in encoding/json/v2 codec behind jsonv2 tag
As an experiment, with a thought for how Gin might eventually migrate to
and use features from jsonv2, I have put together a new json/codec for
json/v2.

---

Selected with -tags jsonv2, and constrained to go1.27 &&
goexperiment.jsonv2 since encoding/json/v2 is still gated by the
experiment in 1.27 and GOEXPERIMENT=nojsonv2 makes it unimportable.

json.go negates that entire expression, mirroring how it already
negates sonic's OS constraint, so -tags jsonv2 on Go 1.25/1.26 falls
back to encoding/json rather than leaving API nil.

Uses json.DefaultOptionsV1() to keep behaviour identical to the v1
codec: v2's native defaults would render nil slices/maps as []/{} and
match member names case-sensitively, silently zeroing fields that
previously bound.

Fills three v1 gaps v2's API doesn't cover directly: UseNumber has no
exported option, so it's reproduced with a custom *any unmarshaler;
MarshalWrite omits Encoder.Encode's trailing newline; MarshalIndent
becomes jsontext.WithIndentPrefix + WithIndent.
2026-09-01 14:41:31 +01:00

118 lines
2.9 KiB
Go

// Copyright 2026 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package json_test
import (
"io"
"strings"
"testing"
"github.com/gin-gonic/gin/codec/json"
)
// These benchmarks compare codecs, and on go1.27 also compare the engine behind
// encoding/json: the jsonv2 GOEXPERIMENT is on by default there, so v1 is itself
// implemented over encoding/json/v2. To see a codec or engine difference, run
// the same benchmark under each configuration and compare with benchstat, e.g.
//
// go test -bench . -count 10 ./codec/json/ > new.txt
// GOEXPERIMENT=nojsonv2 go test -bench . -count 10 ./codec/json/ > old.txt
// benchstat old.txt new.txt
//
// Marshal and Unmarshal do not necessarily move in the same direction, so read
// them separately rather than as one number.
type benchPayload struct {
ID int `json:"id"`
Name string `json:"name"`
Tags []string `json:"tags"`
Meta map[string]string `json:"meta"`
Nested []benchPayload `json:"nested,omitempty"`
}
var nested = benchPayload{
ID: 1,
Name: "gin <framework>",
Tags: []string{"a", "b", "c"},
Meta: map[string]string{"k1": "v1", "k2": "v2"},
Nested: []benchPayload{
{ID: 2, Name: "x", Tags: []string{"z"}},
{ID: 3, Name: "y", Meta: map[string]string{"q": "r"}},
},
}
// flat carries no map, so it isolates struct and slice encoding from the key
// sorting that marshaling a map requires.
type benchFlat struct {
ID int `json:"id"`
Name string `json:"name"`
Tags []string `json:"tags"`
N float64 `json:"n"`
OK bool `json:"ok"`
}
var flat = []benchFlat{
{1, "alpha <a>", []string{"x", "y"}, 1.5, true},
{2, "beta", []string{"z"}, 2.25, false},
{3, "gamma", nil, 3.75, true},
}
// Safe at package scope: this is an external test package, so codec/json is
// fully initialised and API is installed before these run.
var nestedJSON, _ = json.API.Marshal(nested)
func BenchmarkMarshal(b *testing.B) {
for b.Loop() {
if _, err := json.API.Marshal(nested); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkMarshalFlat(b *testing.B) {
for b.Loop() {
if _, err := json.API.Marshal(flat); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkMarshalIndent(b *testing.B) {
for b.Loop() {
if _, err := json.API.MarshalIndent(nested, "", " "); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkUnmarshal(b *testing.B) {
for b.Loop() {
var out benchPayload
if err := json.API.Unmarshal(nestedJSON, &out); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkEncoder(b *testing.B) {
for b.Loop() {
if err := json.API.NewEncoder(io.Discard).Encode(nested); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkDecoder(b *testing.B) {
r := strings.NewReader("")
for b.Loop() {
r.Reset(string(nestedJSON))
var out benchPayload
if err := json.API.NewDecoder(r).Decode(&out); err != nil {
b.Fatal(err)
}
}
}