Changing the JSON package in an existing Go application should not be based only on API compatibility. Performance should also be measured before deciding whether the change is worthwhile.

JSON processing can become a noticeable part of application cost when a service handles large request bodies, high request volumes, nested objects, event streams, or repeated serialization and deserialization.

The right way to evaluate encoding/json/v2 is to benchmark the workload that the application actually processes.

A small benchmark using a three-field struct may show a difference, but it may not represent the behavior of a production API that processes hundreds of fields, nested arrays, optional properties, and custom data types.

This article explains how to create useful JSON benchmarks in Go, compare serialization and deserialization workloads, measure memory usage, and interpret the results before changing an existing application.

Why JSON Performance Should Be Measured Before Migration

A JSON package is only one part of an application's request-processing pipeline.

For an HTTP API, the complete operation may look like this:

Code
HTTP Request
     |
     v
Request Body
     |
     v
JSON Decode
     |
     v
Validation
     |
     v
Business Logic
     |
     v
Database
     |
     v
JSON Encode
     |
     v
HTTP Response

Even if JSON decoding becomes faster, the overall request may not become noticeably faster if database access or another operation dominates the response time.

The opposite is also possible.

If an application spends a significant amount of CPU time encoding and decoding JSON, improvements in the JSON layer can matter more.

Therefore, benchmark the complete workload before drawing conclusions.

What Should Be Measured?

At minimum, compare these areas:

  • JSON marshaling.
  • JSON unmarshaling.
  • Allocation count.
  • Allocated memory.
  • Large payload processing.
  • Small payload processing.
  • Nested object processing.
  • Array processing.
  • Custom JSON types.
  • Streaming workloads when applicable.

For HTTP services, it can also be useful to measure the complete request path separately.

Create a Representative Data Model

Start with a model that resembles the application's actual data.

For example:

Go
type User struct {
	ID        int64    `json:"id"`
	Name      string   `json:"name"`
	Email     string   `json:"email"`
	Active    bool     `json:"active"`
	Roles     []string `json:"roles"`
	Address   Address  `json:"address"`
	CreatedAt string   `json:"created_at"`
}

type Address struct {
	Street  string `json:"street"`
	City    string `json:"city"`
	Country string `json:"country"`
	ZipCode string `json:"zip_code"`
}

Create representative test data:

Go
var testUser = User{
	ID:        1001,
	Name:      "Rahul Kumar",
	Email:     "[email protected]",
	Active:    true,
	Roles:     []string{"developer", "admin"},
	Address: Address{
		Street:  "Main Street",
		City:    "Patna",
		Country: "India",
		ZipCode: "800001",
	},
	CreatedAt: "2026-11-05T10:30:00Z",
}

The goal is not to create a perfect copy of production data.

The goal is to create benchmark data with similar structure, size, nesting, and value characteristics.

Benchmark JSON Marshaling

A Go benchmark uses the testing package.

For the v2 implementation:

Go
func BenchmarkJSONV2Marshal(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_, err := json.Marshal(testUser)
		if err != nil {
			b.Fatal(err)
		}
	}
}

A corresponding benchmark can be created for the existing implementation:

Go
func BenchmarkJSONV1Marshal(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_, err := json.Marshal(testUser)
		if err != nil {
			b.Fatal(err)
		}
	}
}

Because both packages expose similar basic APIs, the benchmark structure can remain simple.

However, the two benchmarks must use the same input data.

Otherwise, the comparison is not meaningful.

Benchmark JSON Unmarshaling

Serialization is only half of the workload.

For API servers and message consumers, deserialization can be just as important.

First create a JSON payload:

Go
var testJSON []byte

func init() {
	var err error

	testJSON, err = json.Marshal(testUser)
	if err != nil {
		panic(err)
	}
}

Then benchmark decoding:

Go
func BenchmarkJSONV2Unmarshal(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var user User

		if err := json.Unmarshal(testJSON, &user); err != nil {
			b.Fatal(err)
		}
	}
}

The v1 benchmark should use the same testJSON:

Go
func BenchmarkJSONV1Unmarshal(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var user User

		if err := json.Unmarshal(testJSON, &user); err != nil {
			b.Fatal(err)
		}
	}
}

Using identical input ensures that the comparison focuses on the JSON implementation rather than differences in the data.

Measure Allocations and Memory

Execution time alone is not enough.

A benchmark can also report allocation information.

Run benchmarks with memory statistics enabled:

Bash
go test -bench=. -benchmem

The benchmark output includes information such as:

Code
ns/op
B/op
allocs/op

These values provide three useful perspectives:

  • ns/op indicates approximate time per operation.
  • B/op indicates bytes allocated per operation.
  • allocs/op indicates the number of allocations per operation.

Lower values are generally better, but the most important metric depends on the application.

For a high-throughput API, allocation pressure can be important because excessive allocations can increase garbage-collection work.

Use the Same Benchmark Environment

Benchmark comparisons are only useful when the environment is reasonably consistent.

Avoid comparing results from:

  • Different machines.
  • Different Go versions.
  • Different compiler settings.
  • Different operating-system environments.
  • Different benchmark data.
  • Different background workloads.

Run both implementations under the same conditions.

A practical benchmark process is:

Bash
go test -bench=JSON -benchmem

Then repeat the benchmark several times if the results vary significantly.

For more consistent comparison, Go's benchmark tooling can also be combined with repeated benchmark runs and statistical comparison tools.

Benchmark Small and Large JSON Payloads

One benchmark is rarely enough.

An API may process both small and large payloads.

For example, create a small payload:

Go
var smallUser = User{
	ID:     1,
	Name:   "Rahul",
	Email:  "[email protected]",
	Active: true,
}

Then create a larger workload:

Go
type UserList struct {
	Users []User `json:"users"`
}

Generate multiple records:

Go
func createUsers(count int) UserList {
	users := make([]User, 0, count)

	for i := 0; i < count; i++ {
		users = append(users, User{
			ID:     int64(i + 1),
			Name:   "Developer",
			Email:  "[email protected]",
			Active: true,
			Roles:  []string{"developer"},
			Address: Address{
				Street:  "Main Street",
				City:    "Patna",
				Country: "India",
				ZipCode: "800001",
			},
			CreatedAt: "2026-11-05T10:30:00Z",
		})
	}

	return UserList{
		Users: users,
	}
}

This lets you compare behavior as the JSON document grows.

Large payloads can expose allocation or processing differences that are not visible with small objects.

Benchmark Nested JSON Structures

Real application data is rarely flat.

For example:

JSON
{
	"id": 1001,
	"name": "Rahul",
	"address": {
		"city": "Patna",
		"country": "India"
	},
	"roles": [
		"developer",
		"admin"
	]
}

Nested structures should therefore be included in benchmark data.

This matters because JSON processing involves more than simply reading primitive values.

Nested objects, arrays, optional properties, and repeated structures can change the workload significantly.

Benchmark Arrays Separately

Applications that return collections should benchmark collections.

For example:

Go
func BenchmarkJSONV2LargeArray(b *testing.B) {
	users := createUsers(1000)

	for i := 0; i < b.N; i++ {
		_, err := json.Marshal(users)
		if err != nil {
			b.Fatal(err)
		}
	}
}

A benchmark like this is more useful for an API returning large lists than a benchmark containing a single object.

If an API supports pagination, benchmark realistic page sizes as well.

Measure End-to-End API Performance

JSON benchmarks are useful, but they should not replace application-level performance testing.

Suppose an API has this handler:

Go
func GetUser(w http.ResponseWriter, r *http.Request) {
	user := loadUser()

	data, err := json.Marshal(user)
	if err != nil {
		http.Error(w, "serialization failed", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write(data)
}

The JSON operation is only part of the request.

The application may spend more time in:

Code
Database query
Validation
Business logic
Network communication
JSON serialization

If JSON takes a small percentage of the total request time, replacing the JSON implementation may have limited effect on end-to-end latency.

This is why application profiling should accompany JSON benchmarking.

Compare the Results Carefully

A simple comparison can be organized like this:

Metric

encoding/json

encoding/json/v2

What It Tells You

ns/op

Measure

Measure

Processing time

B/op

Measure

Measure

Memory allocated

allocs/op

Measure

Measure

Allocation pressure

Small payload

Measure

Measure

Basic workload

Large payload

Measure

Measure

Scaling behavior

Nested data

Measure

Measure

Complex object handling

Arrays

Measure

Measure

Collection workload

End-to-end API

Measure

Measure

Real application impact

Do not treat one benchmark number as proof that one implementation is always better.

The workload determines the result.

Benchmark Custom JSON Types

Custom marshaling can change performance significantly.

For example:

Go
type Product struct {
	ID    int     `json:"id"`
	Price float64 `json:"price"`
}

func (p Product) MarshalJSON() ([]byte, error) {
	type ProductAlias Product

	return json.Marshal(ProductAlias(p))
}

If the application contains many custom JSON implementations, include them in the benchmark.

Otherwise, the benchmark may measure only the simplest serialization path and fail to represent the application's real workload.

Benchmarking Streaming Workloads

Applications that process JSON from readers should benchmark that path separately.

For example:

Go
func BenchmarkJSONStream(b *testing.B) {
	payload := bytes.NewReader(testJSON)

	for i := 0; i < b.N; i++ {
		payload.Reset(testJSON)

		var user User

		if err := json.UnmarshalRead(payload, &user); err != nil {
			b.Fatal(err)
		}
	}
}

Streaming workloads should be measured independently because reading from a stream introduces behavior that is different from processing an already allocated byte slice.

Common Benchmarking Mistakes

Using Tiny Test Data

A three-field object may not represent a real API payload.

Use representative data.

Benchmarking Only Marshaling

If the application is primarily an API consumer, unmarshaling may be more important.

Measure both directions.

Ignoring Memory

A faster operation that creates significantly more allocations may not be beneficial for a high-throughput service.

Measure B/op and allocs/op.

Running One Benchmark Once

Benchmark results can vary.

Repeat measurements and investigate large variations.

Benchmarking the Wrong Workload

If production processes large arrays but the benchmark uses one object, the benchmark answers the wrong question.

Mixing JSON and Business Logic

Do not use a benchmark to measure database queries and JSON serialization together when the goal is specifically to compare JSON implementations.

Keep focused benchmarks separate from end-to-end benchmarks.

Best Practices for JSON Performance Testing

  1. Use production-shaped JSON structures.
  2. Benchmark both marshaling and unmarshaling.
  3. Include small, medium, and large payloads.
  4. Include nested objects and arrays.
  5. Measure allocations with -benchmem.
  6. Run both implementations in the same environment.
  7. Keep benchmark data identical between implementations.
  8. Include custom JSON types when they are important to the application.
  9. Benchmark streaming workloads separately.
  10. Validate improvements with end-to-end application measurements.
  11. Repeat benchmarks when results are close.
  12. Profile the application before making a migration decision based on JSON performance alone.

When Should You Switch to json/v2?

A performance improvement by itself should not be the only reason to migrate.

A migration decision should consider several factors:

Code
Performance
    +
Compatibility
    +
Correctness
    +
Maintainability
    +
Operational Risk

If the new implementation performs well but breaks important external payloads, the migration may not be appropriate yet.

If performance is similar but the new API provides behavior or configuration that solves a real application problem, migration may still make sense.

The decision should be based on the application's requirements rather than a single benchmark result.

Troubleshooting Unexpected Benchmark Results

If the benchmark shows inconsistent results, first check the benchmark itself.

Look for:

  • Different input data.
  • Shared mutable state.
  • Excessive setup work inside the benchmark loop.
  • Garbage-collection effects.
  • Background processes.
  • Different Go versions.
  • CPU frequency changes.
  • Network or disk operations accidentally included in the benchmark.
  • Data generation happening during every iteration.

The benchmark loop should contain the operation being measured, not unrelated preparation work.

For example, avoid this pattern:

Go
for i := 0; i < b.N; i++ {
	data := createLargePayload()
	json.Marshal(data)
}

If the goal is to measure JSON marshaling, creating the payload inside the benchmark can distort the result.

Prepare the data before the benchmark loop when appropriate.

Summary

Measuring JSON performance before switching from encoding/json to encoding/json/v2 helps developers make migration decisions based on real application behavior.

A useful benchmark should measure more than execution time. Allocation count, memory usage, payload size, nesting, arrays, custom JSON types, and streaming workloads can all affect the result.

The most important rule is to benchmark representative data. A small synthetic object can be useful for basic testing, but it should not be treated as a representation of an entire production workload.

Use focused JSON benchmarks to understand the serialization layer, then use application-level profiling and end-to-end testing to determine whether the difference actually matters to users and infrastructure.

The result should be a measured migration decision rather than an assumption that a newer JSON implementation will automatically make the entire application faster.