goccy/go-json: a drop-in replacement for encoding/json that trades reflection for type pointers
Fast JSON encoder/decoder compatible with encoding/json for Go
At a glance
- What is it?
- goccy/go-json keeps the encoding/json API and swaps the reflection path for a type-pointer dispatch table, plus a generic MarshalOf that avoids the interface boxing copy. It is for Go services where JSON encoding shows up in profiles, and it is not for anyone who needs a v1.0 API promise.
- Who is it for?
- Adopt goccy/go-json if your service spends measurable time in encoding/json and you want the change to be one import line, with the option of MarshalOf later for hot paths. Do not adopt it if you need a frozen v1.0 API surface, or if your JSON decoding depends on streaming Token behaviour that you have not checked.
- Can I use it commercially?
- Yes. MIT 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 received new commits within the last day.
- 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 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem goccy/go-json solves, and who actually has it
encoding/json is the reference implementation. It is also reflection-based, and reflection costs. Every Marshal call walks struct fields through the reflect package, and every value passed as interface{} gets boxed onto the heap first. For a service that serialises a few small payloads per request this is invisible. For a service that encodes large responses, batches, or event payloads at high request rates, the encoder becomes a line item in the CPU profile.
The usual escape routes have a cost of their own. Code generation (easyjson and similar) buys speed by adding a build step and a generated file per type. Dedicated interfaces buy speed by giving up the standard API. goccy/go-json takes the third position: keep the encoding/json surface exactly, and recover the performance inside the library. The README states the goal directly, saying the project "dares to stick to compatibility with encoding/json and is the simple interface" while aiming to be the fastest.
That framing defines the audience. This is for teams with an existing Go codebase already using encoding/json, where the migration cost of a code generator is not justified. It is not aimed at new projects choosing a serialisation format from scratch, and it is not aimed at anyone who needs a stable v1.0 contract today.
How the encoder avoids reflection: type pointers and a dispatch map
The README's "How it works" section describes two mechanisms. The first is buffer reuse through sync.Pool: since the only value json.Marshal must return is a []byte, the library reuses a pooled buffer for intermediate encoding and allocates the final result once. That is a standard technique and the README says as much, listing it among the methods used by most comparable libraries.
The second is the interesting one. The address at which a Go binary stores type information is fixed for that binary, and the project calls this address the typeptr. Given an interface{} value, the library reads the type pointer out of the interface header and looks it up in a map from typeptr to a pre-built encoder function. If the entry exists, the value is encoded by that function with no reflect calls at all.
The README shows the shape of this, using an emptyInterface struct with typ and ptr fields and a typeToEncoder map keyed by uintptr. It also flags the obvious hazard in a footnote: in reality typeToEncoder can be referenced by multiple goroutines, so exclusive control is required. That is the trade-off in one line. The speed comes from unsafe pointer reads and a shared map, and correctness then depends on the synchronisation around that map. The repository layout reflects the care this needs: there are separate test files for race builds, encode_value_lifetime_test.go, encode_stack_copy_test.go, and a dedicated go-json-fuzz repository the README points to for fuzzing.
MarshalOf and the interface boxing copy
The README's unique-technique section opens with a specific cost that the standard API imposes. json.Marshal takes an interface{}. A non-pointer value converted to an interface{} is copied to the heap, so json.Marshal(v) allocates a copy of v on every call, on top of the result buffer.
json.MarshalOf[T] takes the value by its concrete type instead. According to the README, it copies the value into a heap value that is reused, so the per-call boxing allocation goes away. The repository contains encode_of.go and encode_of_test.go, which is consistent with the README's description of the API.
This is the part of the library with the sharpest edge, and it is worth being explicit about why. MarshalOf is generic, so it needs a Go version that supports generics. The module declares go 1.21 in go.mod, so anything below that will not build the module as declared. If you are on an older toolchain, the drop-in import may still be the only option available to you, and the allocation win from MarshalOf is off the table.
The README also lists other features beyond raw speed: options for customisation, colouring of encoded output, propagating context.Context into MarshalJSON and UnmarshalJSON, and type-safe dynamic field filtering. Those are extensions layered on top of the compatible core, and they are the reason the project describes itself as more than a faster encoder.
Installing goccy/go-json and swapping the import
Installation is one command. The README gives it as go get, and the module path is the repository path.
go get github.com/goccy/go-jsonThe migration itself is an import replacement, which the README shows as a diff. Every call site keeps its current form: json.Marshal, json.Unmarshal, json.NewEncoder, json.NewDecoder, and the struct tags all stay as they are.
-import "encoding/json"
+import "github.com/goccy/go-json"After that swap, a normal marshal call works unchanged. The README's usage section is exactly this one-line change, and the library's own tests import it the same way. The repository's docker-compose.yml runs the suite in a golang:1.25 container with a 620M memory limit, working from /go/src/go-json, which is one way to see the tests run without installing anything locally.
services:
go-json:
image: golang:1.25
volumes:
- '.:/go/src/go-json'
deploy:
resources:
limits:
memory: 620M
working_dir: /go/src/go-jsonThe Makefile exposes cover, cover-html, lint, generate and bench-check targets. The bench-check target runs go run ./internal/cmd/benchcheck and fails if go-json's benchmarks in ./benchmarks degrade against master beyond a tolerance, either on the mean or on a single benchmark; it never measures the other libraries.
make bench-check BENCH_CHECK_FLAGS="-bench GoJson -no-cache"If you want the allocation win on a hot path, MarshalOf is the next step, but confirm your toolchain satisfies the go 1.21 directive in go.mod first.
Where compatibility claims get thin
The README's comparison table puts goccy/go-json in a small group: encoder yes, decoder yes, compatible with encoding/json yes. Only encoding/json itself shares that row. json-iterator/go and segmentio/encoding/json are marked partial. easyjson, gojay, jettison and simdjson-go are marked no.
That table is the project's own assessment, and the README's supporting notes are candid about the others rather than about itself. On segmentio/encoding/json it notes that encoders are well supported but some decoder APIs such as Token for streaming decode are not. On json-iterator/go it points to a compatibility issue and adds that it has not been supported for a long time.
What the README does not do is enumerate the cases where its own compatibility is imperfect. A drop-in replacement claim is a strong claim, and the honest reading is that "compatible" here means the API and the observable behaviour on the project's test corpus, not a formal proof against every edge of encoding/json. The repository's test file names suggest the maintainers know where the edges are: encode_cycle_detection_test.go, encode_embedded_omitempty_test.go, encode_unordered_map_test.go, encode_long_key_test.go, encode_map_key_test.go. Each of those names corresponds to a behaviour that is easy to get subtly wrong.
There is also no v1.0. The roadmap in the README places v1.0.0 after v0.9.0, with the intervening work described as adding convenient APIs while maintaining compatibility. The latest release listed is v0.10.6 from 2026-03-12, so the project is still in the v0.x line. The last push to the repository was on 2026-09-23. If your policy is to depend only on libraries with a stable major version, this one does not qualify yet, and that is a legitimate reason to wait.
Choosing between goccy/go-json, sonic and a code generator
The README's Makefile is unusually informative about the intended comparison. Its bench-compare-encode target prints the encode benchmarks of go-json and bytedance/sonic side by side, and the comments explain the configuration: SonicStd is sonic configured to do what encoding/json and go-json do, meaning escape HTML and sort map keys, while GoJsonLikeSonic is go-json configured to do what sonic does by default. SONIC_MAX_INLINE_DEPTH controls how deep sonic inlines nested structs.
That comment is the whole comparison in miniature. Sonic reaches its numbers partly by changing defaults: not escaping HTML, not sorting map keys. go-json's defaults match encoding/json, and matching those defaults costs something. If you control both ends of the wire and do not need HTML escaping or deterministic map key order, sonic's defaults are a legitimate choice and may be faster. If you are swapping a library inside an existing system where downstream consumers rely on encoding/json's output byte for byte, go-json's defaults are the reason to pick it over sonic.
The code-generation route is the other real alternative. easyjson and similar tools generate a marshaller per type at build time. That removes reflection entirely and can beat any runtime-dispatch approach, but it adds generated files to your repository and a step to your build, and the README's table marks those libraries as not compatible with encoding/json. go-json's position is that you get most of the speed without either cost. Whether "most" is enough depends on your profile, and the only way to know is to run the benchmarks in the benchmarks directory against your own types.
Maintenance, licence and upgrade cost
The repository is not archived, and the last push was on 2026-09-23. Release cadence is uneven: v0.10.4 on 2024-12-12, v0.10.5 on 2025-01-28, then v0.10.6 on 2026-03-12, a gap of more than a year between the last two releases. Commits and releases are not the same thing, so a quiet release list does not mean a quiet repository, but it does mean you should not expect frequent tagged versions.
The practical upgrade cost is low by design. Because the API is encoding/json's API, upgrading the library does not change your call sites. What can change is the type-pointer dispatch behaviour, since the internal encoder selection is exactly the machinery that gets optimised between versions. The Makefile's bench-check target exists for this reason: it fails if go-json's benchmarks in ./benchmarks degrade against master beyond a tolerance, either on the mean or on a single benchmark, and it never measures the other libraries. That is a guard for the maintainers, not for you, but it tells you the project treats encode performance as a regression surface.
The licence is MIT, and the LICENSE file is at the repository root. MIT is permissive: it allows use in closed-source products and requires that the copyright notice and permission notice be preserved. That is a summary of the licence text, not legal advice, and if your organisation has a licence review process, the MIT text is what it will look at.
Editorial conclusion
Adopt goccy/go-json if your service spends measurable time in encoding/json and you want the change to be one import line, with the option of MarshalOf later for hot paths. Do not adopt it if you need a frozen v1.0 API surface, or if your JSON decoding depends on streaming Token behaviour that you have not checked. Before switching, run your own test suite against the swapped import, confirm the Go version in go.mod (1.21) is at or below your toolchain, and pin the version you tested rather than tracking master.
Frequently asked questions
How do I decode JSON in Go with goccy/go-json?
The same way you do with encoding/json: import github.com/goccy/go-json and call json.Unmarshal, or use json.NewDecoder for a stream. The README's migration is a single import replacement, so existing decode call sites keep their current form.
Is goccy/go-json compatible with encoding/json?
The README's comparison table marks goccy/go-json as compatible with encoding/json for both encoder and decoder, a row it shares only with encoding/json itself. The project describes itself as a drop-in replacement, so the intended migration is changing the import and nothing else.
How does goccy/go-json compare with sonic?
The Makefile's bench-compare-encode target prints their encode benchmarks side by side, and its comments note that sonic's defaults do not escape HTML or sort map keys, while go-json matches encoding/json on both. If downstream consumers depend on encoding/json's exact output, that difference in defaults matters more than the benchmark numbers.
How does goccy/go-json compare with jsoniter?
The README's table marks json-iterator/go as only partially compatible with encoding/json, and a note points to a compatibility issue while observing that it has not been supported for a long time. go-json is marked fully compatible and is a drop-in import replacement.
How does goccy/go-json compare with easyjson?
easyjson generates a marshaller per type at build time and the README's table marks it as not compatible with encoding/json. go-json avoids code generation and keeps the standard API, aiming to recover speed inside the library instead.
Official sources
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.
[](https://hysenlabs.com/projects/goccy-go-json)