Library / SDK
go-json-experiment/json avatar
go-json-experiment/json

json v2 in Go: the experimental encoding/json rewrite and how to adopt it

Experimental implementation of a proposed v2 encoding/json package

543 stars31 forksGoBSD-3-Clause

At a glance

What is it?
This module holds a mirror of encoding/json/v2, the rewrite of Go's JSON package that separates syntax from semantics, was adopted as an experiment in Go 1.25 and became a stable API in Go 1.27. The prototype that is now in the standard library lives here, and the module still hosts a mirror of it.
Who is it for?
Adopt encoding/json/v2 through the goexperiment.jsonv2 build tag if you are on Go 1.27 and want the new API, since the standard library version is the same implementation this module mirrored. Do not import this module directly for new production code, because it is a mirror of a package the standard library now owns.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 63 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What this module is now, and what it used to be

This module currently hosts a mirror of encoding/json/v2. Importing this package with the goexperiment.jsonv2 build tag, which is enabled by default on Go 1.27, will alias the implementation in the Go standard library.

That single sentence describes the current state of the project accurately, and it also explains why the repository looks the way it does. This module contained the historical prototype of encoding/json/v2 before it was formally adopted into the Go standard library as an experiment in Go 1.25 and then later as a stable API in Go 1.27.

So there are three eras here. First, the prototype: a standalone module where a proposed v2 encoding/json package was explored. Second, the experiment: the same work moved inside the standard library as encoding/json/v2 behind the GOEXPERIMENT mechanism in Go 1.25. Third, the stable API in Go 1.27, at which point the module became a mirror so that people working on the API before it landed could keep building against the same code.

The practical consequence is that the interesting design decisions are now upstream in the Go project, and this repository is where you can read them, read their history and see the shape of the API as it was proposed.

The repository layout supports that reading. There is alias.go and alias_gen.go, which is the mechanism that ties the module to the standard library when the build tag is present. There is arshal.go and its variants arshal_any.go, arshal_default.go, arshal_embedded.go, arshal_funcs.go, arshal_methods.go and arshal_time.go, which is the implementation of the marshalling behaviour. Then fields.go for struct field handling, fold.go for case folding, intern.go for interning, options.go and the options_format files for the option types, errors.go for error reporting, and a v1/ directory.

The licence is BSD-3-Clause, the default branch is master, the module path is github.com/go-json-experiment/json and the go directive in go.mod is 1.26.

The primary language is Go, and the project has no GitHub releases, which fits a module that is versioned by the Go toolchain and the standard library rather than by its own tags.

The syntax and semantics split, and the two-layer API

The design goal is stated in one sentence: this package aims to provide a clean separation between syntax and semantics.

Syntax deals with the structural representation of JSON, as specified in RFC 4627, RFC 7159, RFC 7493, RFC 8259 and RFC 8785. Semantics deals with the meaning of syntactic data as usable application data.

The API follows that split into two packages. The bottom half of the API diagram contains functionality that is only concerned with syntax, implemented by the jsontext package. The upper half assigns semantic meaning to syntactic data handled by the bottom half, as implemented by the v2 json package.

Within the syntax layer, the Encoder and Decoder types are streaming tokenizers concerned with the packing or parsing of JSON data. They operate on Token and Value types which represent the common data structures that are representable in JSON. The README is emphatic about their role: Encoder and Decoder do not aim to provide any interpretation of the data.

That is the key difference from v1. In encoding/json v1, the Encoder and the struct machinery are one thing, so a streaming parse and a semantic unmarshal are tangled together. Here, the tokenizer is a standalone layer you can use on its own if you want to inspect or rewrite JSON syntax without committing to a Go type model.

The semantic layer is where the familiar functions live, and the list is broader than v1. Functions like Marshal, MarshalWrite, MarshalEncode, Unmarshal, UnmarshalRead and UnmarshalDecode provide semantic meaning by correlating any arbitrary Go type with some JSON representation of that type, as stored in data types like []byte, io.Writer, io.Reader, Encoder or Decoder.

The Marshal family in particular has three shapes rather than one. Marshal returns a []byte, MarshalWrite writes to an io.Writer, and MarshalEncode produces a Token stream through an Encoder. The same split exists on the other side with Unmarshal taking []byte, UnmarshalRead taking an io.Reader, and UnmarshalDecode working with a Decoder.

The API overview diagram in api.png shows this as a high-level overview of the v2 json and jsontext packages, with purple blocks for types and blue blocks for functions or methods, and arrows showing the approximate flow of data.

Options as types instead of setter methods

The difference from v1 that will affect your code first is how options are expressed.

In contrast to v1 encoding/json, options are represented as separate types rather than being setter methods on the Encoder or Decoder types.

In v1, configuring behaviour means calling methods on the encoder, such as SetIndent or SetEscapeHTML, and the settings live as mutable state on that object. In v2, an option is a value that you pass into a call, which means the configuration is visible at the call site rather than hidden in the object's state.

The README then explains why the options are split the way they are, and this is the part that makes the design coherent. Some options affect JSON serialization at the syntactic layer, while others affect it at the semantic layer. Some options only affect JSON when decoding, while others affect JSON while encoding.

So the option types are not just a stylistic change. Because the syntax layer and the semantic layer are separate, an option that only makes sense for one of them can only be accepted by the functions in that layer. An option that changes how bytes are written, such as indentation or escaping, belongs to the syntactic side and is available to the Encoder. An option that changes how a Go type maps to JSON, such as whether a field is omitted or how a named type is handled, belongs to the semantic side and is available to Marshal.

The files in the tree reflect this. options.go holds the option types, options_format.go and options_format_global.go handle formatting options, and options_format_test.go tests them. The intern.go file is the interning support, which is what lets repeated strings in a document share storage rather than being allocated per occurrence.

There is also a v1/ directory in the repository, which is the compatibility path for code being moved off v1, and a migrate.sh script at the top level, which suggests the migration is intended to be partly mechanical.

Performance claims, and the benchmark that is not in this repository

The performance section is short and has an important qualifier.

One of the goals of the v2 module is to be more performant than v1, but not at the expense of correctness. In general, v2 is at performance parity with v1 for marshaling, but dramatically faster for unmarshaling.

The asymmetry is worth noting. Marshaling is a write-only operation where the structure of the output is largely determined by the Go type, so the gains available are limited. Unmarshaling is where the work is, because v1 has to do reflection over struct tags, match field names case-insensitively and allocate as it goes. That is the operation the rewrite targeted.

The benchmarks are not in this repository. The README points to the jsonbench project at github.com/go-json-experiment/jsonbench for benchmarks comparing v2 with v1 and a number of other popular JSON implementations. So the numbers you want to check against your own payloads live in a separate repository, and the README's own claim is a general statement rather than a table.

The parenthetical about correctness is the significant part. A rewrite of a standard library package is judged on whether it produces the same bytes as the thing it replaces, and the phrase not at the expense of correctness says that the design decisions were made with compatibility in mind rather than in favour of the benchmark.

For anyone measuring this, the practical point is that your own data matters. A struct-heavy payload, a map-heavy payload and a large slice of small structs hit the reflection and allocation paths differently, and the unmarshaling win will not be uniform across them.

The repository itself carries bench_test.go, so there is a benchmark harness in the tree alongside the fuzz_test.go, which is the other notable testing artefact and the right kind of tool for a parser.

Who wrote it and how the design was reviewed

The development section names the people involved, and for a proposal that became a standard library API that list is part of the story.

This module was primarily developed by @dsnet, @mvdan and @johanbrandhorst, with feedback provided by @rogpeppe, @ChrisHines and @rsc. During the process to adopt the project into the Go standard library, API review was provided by @aclements, @neild and @prattmic.

The division of roles is instructive. The primary development happened in this repository, with external feedback, and then a separate API review stage happened during the adoption process, with different people. A design that survives that two-stage structure is likely to have been argued about in a way that a single author's proposal would not.

The semiregular discussion process is also documented. Discussion about semantics occur semi-regularly, where a record of past meetings can be found in a shared Google document linked from the README. Semantics was clearly the contested part of the design, which fits the syntax and semantics split: the syntax is largely settled by the RFCs, while the semantics is where behaviour has to be decided.

The tree backs this up with the usual governance files: AUTHORS, CONTRIBUTORS, LICENSE, and a .github/ directory for the CI configuration. The CI badge points at a ci.yml workflow on the master branch, and the GoDev badge marks the package as a reference implementation on pkg.go.dev.

The migration and adoption path is documented in the tree as well. There is a migrate.sh script at the root, a v1/ directory holding the compatibility code, and example test files showing intended usage, including example_marshaljson_test.go and example_orderedobject_test.go, the latter hinting at ordered object support as a feature of the new API.

Where the v2 approach is the wrong choice

There are three limits, and the first is about who owns the decision.

The first is that the API is still moving and this is not where you should be making your production dependency. The go.mod file carries an explicit warning: this package is an experimental implementation in order to explore possible API designs for a future proposal of a v2 encoding/json package, and this package will regularly experience breaking changes. Even though the API has now been adopted into the standard library as a stable API in Go 1.27, the module's own documentation still carries that warning, and the version of the code in this repository is the experimental one rather than the standard library one.

If you want the new API, the route the README gives is the build tag, not the module. Importing this package with the goexperiment.jsonv2 build tag, enabled by default on Go 1.27, will alias the implementation in the Go standard library. That means you are using the standard library's copy, with the standard library's stability guarantees, and the module is just the alias.

The second is that v2 is a different API, not a drop-in. Options became types rather than setter methods, the Encoder and Decoder no longer interpret data, and the semantic functions split into Marshal, MarshalWrite and MarshalEncode variants. A codebase that depends on v1 behaviours, particularly around unknown fields, case-insensitive field matching, HTML escaping and the treatment of nil pointers, will need work. The v1/ directory and migrate.sh exist because that work is real.

The third is that the semantics of edge cases will differ from v1 in ways that matter even if the API compiles. The point of the rewrite was to resolve long-standing complaints about v1's behaviour, and a resolution that is more principled is still a behaviour change. If you have persisted JSON in a database, or a service boundary where one side uses v1 and the other v2, then the bytes on the wire may differ even where the types are the same.

The honest alternative for most teams is to stay on encoding/json v1 and treat the rewrite as a future migration rather than a present change, particularly if your Go version is older than 1.25 and the experiment is not even available to you. If you are on 1.27 and want the new API, using the standard library package with the build tag is the right path rather than depending on this module. And if your real problem is that JSON is slow in your service, the alternatives are usually more effective than the encoder swap, such as reducing the number of decode operations, avoiding interface-typed fields that force reflection, or changing the wire format to something with a schema.

Editorial conclusion

Adopt encoding/json/v2 through the goexperiment.jsonv2 build tag if you are on Go 1.27 and want the new API, since the standard library version is the same implementation this module mirrored. Do not import this module directly for new production code, because it is a mirror of a package the standard library now owns. Before you migrate, read the API overview diagram and check the v1/ directory and migrate.sh in this repository for the compatibility path.

Frequently asked questions

What is the go-json-experiment/json module?

It is a Go module that currently hosts a mirror of encoding/json/v2. Importing it with the goexperiment.jsonv2 build tag, which is enabled by default on Go 1.27, aliases the implementation in the Go standard library. It previously held the historical prototype of encoding/json/v2 before that was adopted into the standard library as an experiment in Go 1.25 and a stable API in Go 1.27.

How do I use the new json v2 API in Go?

Use encoding/json/v2 from the standard library on Go 1.27, where the goexperiment.jsonv2 build tag is enabled by default. The module in this repository is a mirror, and its alias.go and alias_gen.go files provide the aliasing mechanism when the build tag is present.

What is the syntax and semantics split in json v2?

Syntax deals with the structural representation of JSON as specified in the relevant RFCs, and semantics deals with the meaning of syntactic data as usable application data. The Encoder and Decoder types are streaming tokenizers concerned only with syntax and do not provide any interpretation, while Marshal, MarshalWrite, MarshalEncode, Unmarshal, UnmarshalRead and UnmarshalDecode assign semantic meaning by correlating a Go type with a JSON representation.

How does json v2 differ from v1 in how options are set?

Options are represented as separate types rather than as setter methods on the Encoder or Decoder. The split follows the package's layers, so some options affect serialization at the syntactic layer and others at the semantic layer, and some apply only when decoding while others apply when encoding.

Is json v2 faster than encoding/json v1?

The README states that v2 is at performance parity with v1 for marshaling but dramatically faster for unmarshaling, and that the goal is to be more performant than v1 but not at the expense of correctness. The benchmarks themselves live in a separate repository, the jsonbench project, which compares v2 with v1 and other JSON implementations.

Official sources

  1. go-json-experiment/json on GitHub
  2. Issues
  3. License: BSD-3-Clause
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/go-json-experiment-json.svg)](https://hysenlabs.com/projects/go-json-experiment-json)

Was this useful?