go 1.25's new json encoding package
⚠️ Important: The JSON v2 packages are experimental and their APIs may change in future releases. Use GOEXPERIMENT=jsonv2
to enable these features for testing.
Go 1.25 arrived on 12th August 2025. This release included no language changes compared to recent versions. The most notable addition however was the inroduction of an experimental JSON implementation that improves encoding and decoding performance whilst offering more flexible JSON data handling.
Why JSON v2 matters #
JSON processing creates significant overhead in many Go applications. The new experimental package addresses this directly. You can enable it with GOEXPERIMENT=jsonv2
at build time.
The experimental JSON v2 package addresses several long-standing issues with the current encoding/json
implementation. These include performance bottlenecks in both marshalling and unmarshalling operations, inconsistent behaviour around edge cases like duplicate keys and case sensitivity, limited flexibility in controlling JSON processing behaviour, and memory allocation patterns that impact garbage collection performance.
Enabling the experimental JSON v2 #
Go 1.25 includes the new experimental JSON implementation, which you enable by setting the environment variable GOEXPERIMENT=jsonv2
at build time.
# Enable the experimental JSON v2 implementation
GOEXPERIMENT=jsonv2 go build your-app.go
# Or set it for your entire build process
export GOEXPERIMENT=jsonv2
go build your-app.go
Alternatively, you can use the build tag:
# Using build tag instead of environment variable
go build -tags=goexperiment.jsonv2 your-app.go
When enabled, this experiment has two major effects. The existing encoding/json
package uses the new JSON implementation under the hood whilst maintaining full API compatibility. New packages become available: encoding/json/v2
and encoding/json/jsontext
for more advanced use cases.
Architecture: separation of concerns #
encoding/json/jsontext
provides low-level, high-performance JSON tokenizers (Decoder) and encoders (Encoder). This package focuses solely on the syntactic aspects of JSON.
encoding/json/v2
handles the semantic conversion between Go types and JSON values, built upon jsontext. This separation of syntactic and semantic analysis improves code clarity, enables better performance through specialised low-level operations, and provides greater flexibility for custom JSON processing needs.
Internal modernisation of JSON v1 #
A crucial aspect of the JSON v2 experiment is that the existing encoding/json
package is internally modernised to use the new v2 infrastructure under the hood. This means when you enable GOEXPERIMENT=jsonv2
, all your existing JSON code benefits from the new technology whilst maintaining full API compatibility.
This internal modernisation approach provides several benefits:
You can incrementally adopt v2 features whilst keeping existing code unchanged. Options allow you to configure behaviour anywhere from entirely v1-compatible to entirely v2-style, enabling smooth transitions. This means you’re not forced into an all-or-nothing migration approach.
As new features are added to v2, they automatically become available in v1. For example, v2’s new struct tag options like inline
and format
, plus the more performant MarshalJSONTo
and UnmarshalJSONFrom
interface methods, become available to existing v1 code without any changes required on your part.
Having a single underlying implementation also reduces the maintenance burden significantly. Bug fixes, performance improvements, and new functionality benefit both API versions simultaneously, so you get the benefits regardless of which API you’re using.
This approach ensures you can test the new implementation with your existing codebase simply by enabling the experiment, without changing any code.
Performance improvements: the numbers #
JSONv2 delivers significant speed improvements across different operations. The official blog reports unmarshal performance improvements of up to 10x, with marshal performance roughly at parity. For detailed benchmarks, the go-json-experiment/jsonbench repository provides comprehensive analysis showing performance gains that vary by operation type:
Unmarshalling Performance:
- Concrete types: 2.7x to 10.2x faster than JSONv1
- Interface types (
any
,map[string]any
,[]any
): 2.3x to 5.7x faster than JSONv1 - Raw JSON values: 10.2x to 21.1x faster than JSONv1
Marshalling Performance:
- Interface types: 1.6x to 3.6x faster than JSONv1
- Raw JSON values: 5.6x to 12.0x faster than JSONv1
- Concrete types: 1.4x faster to 1.2x slower than JSONv1
Note: These specific benchmark figures are from the external jsonbench repository. Individual results may vary based on your specific use cases and data structures.
The performance improvements stem from the architectural separation between syntax parsing and semantic marshalling. The low-level jsontext
package handles tokenisation efficiently, whilst the higher-level package focuses purely on type conversion. This eliminates redundant work that occurred in the monolithic v1 implementation.
Memory allocation patterns also improve dramatically. The new implementation reduces heap allocations during unmarshalling, which decreases garbage collection frequency. This leads to more predictable performance characteristics across different workload types.
Enhanced streaming performance #
You can achieve additional performance benefits by switching from regular MarshalJSON and UnmarshalJSON to their streaming alternatives (MarshalJSONTo and UnmarshalJSONFrom). The Go team reports this converts certain O(n²) runtime scenarios into O(n). Switching from UnmarshalJSON to UnmarshalJSONFrom in the k8s OpenAPI spec improved performance by orders of magnitude.
The streaming API works by processing JSON data directly through encoders and decoders rather than creating intermediate byte slices. This eliminates copying overhead and reduces memory pressure for large documents. Custom types can implement streaming methods to take advantage of this approach.
Behavioural changes and new options #
JSON v2 introduces several important behavioural changes that improve consistency and security.
Case sensitivity #
JSONv1 matches struct fields case-insensitively when unmarshalling. A JSON field called “firstname” would successfully map to a Go struct field named “FirstName”. JSONv2 changes this behaviour to case-sensitive matching by default, requiring exact case matches between JSON keys and struct field names or json tags.
This change prevents potential security issues where different case variations might be processed unexpectedly. You can restore the original case-insensitive behaviour using the MatchCaseInsensitiveNames(true)
option if needed for backward compatibility.
Duplicate key handling #
JSONv1 silently allows duplicate names in JSON objects, keeping the value from the last occurrence. This behaviour can create security vulnerabilities where malicious JSON might include duplicate keys to override expected values.
JSONv2 rejects duplicate object names with an error by default. This stricter parsing helps prevent security issues but may break code that previously relied on duplicate key handling. The AllowDuplicateNames(true)
option restores the original permissive behaviour when necessary.
Nil slice and map handling #
The marshalling behaviour for nil slices and maps changes between versions. JSONv1 marshals nil slices and maps as JSON null
values. JSONv2 marshals nil slices as empty arrays ([]
) and nil maps as empty objects ({}
).
This change creates more consistent JSON output where the structure remains predictable regardless of whether collections are nil or empty. You can restore the original null-marshalling behaviour using FormatNilSliceAsNull(true)
and FormatNilMapAsNull(true)
options.
Flexible options system #
JSON v2 provides a comprehensive options system for controlling marshalling and unmarshalling behaviour. The options system allows fine-grained control over parsing strictness, formatting preferences, and compatibility modes. You can combine multiple options using JoinOptions
to create custom configurations for different use cases.
Available options include multiline formatting for pretty-printed output, legacy semantics compatibility for smooth transitions from v1, HTML escaping control, and various parsing strictness settings. This flexibility ensures you can configure JSONv2 to match your specific requirements whilst taking advantage of the performance improvements.
Caller-specified customisation #
Beyond the options system, JSON v2 introduces a powerful feature allowing callers to specify custom JSON representations for any type, where caller-specified functions take precedence over type-defined methods or default behaviour.
package main
import (
"encoding/json/v2"
"encoding/json/jsontext"
"fmt"
"time"
)
func main() {
// Custom marshaler for time.Time that formats as Unix timestamp
marshalers := json.MarshalFunc(func(t time.Time) ([]byte, error) {
return []byte(fmt.Sprintf("%d", t.Unix())), nil
})
data := struct {
Name string `json:"name"`
Time time.Time `json:"timestamp"`
}{
Name: "example",
Time: time.Now(),
}
// Use custom marshaler
result, err := json.Marshal(data, json.WithMarshalers(marshalers))
if err != nil {
panic(err)
}
fmt.Println(string(result))
// Output: {"name":"example","timestamp":1725897600}
}
This feature is particularly powerful when you need to define JSON behaviour for types you don’t control, such as protocol buffer messages or external library types. It’s also useful when different parts of your application need different JSON representations for the same types based on context. You can even override the default JSON representation of any type, including built-in types, without modifying the type definition itself.
The streaming variants (MarshalToFunc
and UnmarshalFromFunc
) provide even better performance by working directly with encoders and decoders. In practice, this becomes especially valuable when integrating with third-party APIs that expect specific JSON formats for common types like timestamps or UUIDs.
Real-world impact #
For applications processing significant amounts of JSON data, these improvements deliver measurable benefits. Reduced latency in API responses becomes particularly noticeable under load. Lower memory pressure from reduced allocations means better resource utilisation in constrained environments. Improved security through stricter parsing defaults helps prevent issues with malformed or malicious JSON inputs.
Looking forward #
JSON v2 is expected to potentially become the default in Go 1.26, though this depends on feedback from the current experiment. As the official blog states, the outcome may result in “anything from abandonment of the effort, to adoption as stable packages of Go 1.26.” This timeline provides opportunity to test and provide feedback whilst the API continues to evolve.
The design of encoding/json/v2
will continue evolving based on community feedback. Developers should try the new API and provide input on the proposal issue.
Conclusion #
Go 1.25’s experimental JSON v2 package delivers substantial performance improvements alongside enhanced security through stricter defaults. The architectural separation between syntactic parsing and semantic marshalling provides a solid foundation for future enhancements.
For developers working with APIs and data-intensive applications, testing with GOEXPERIMENT=jsonv2
provides a straightforward way to evaluate these improvements. As the experiment progresses towards potential adoption in Go 1.26, now is the time to explore these capabilities and provide feedback that will shape the future of JSON processing in Go.