Beating Go's Encoding/JSON with Schema-Guided ProtoJSON

ingve2 pts0 comments

Beating Go's encoding/json with Schema-Guided ProtoJSON

Newer|Older

2026-07-21<br>&bull; 1991 words<br>&bull; 10 minutes<br>&bull; #protobuf<br>&bull; #go<br>&bull; #performance<br>&bull; #json<br>&bull; #protojsonxBeating Go&rsquo;s encoding/json with Schema-Guided ProtoJSON

How protojsonx compares to standard protojson, JSON, and vtproto.

In my previous article, I explored the performance bottlenecks of using dynamic JSON structures like google.protobuf.Value and official protojson in Go. The benchmark results pointed at the same culprit over and over: reflection, pointer chasing, and descriptor traversal were showing up as real latency and allocation costs. The benchmark results also pointed out that protojson was consistently really slow.<br>ProtoJSON sits in an awkward place. It gives Protobuf-backed APIs a JSON representation that human operators, browsers, API gateways, and debugging tools can work with, but Go’s official protojson implementation pays heavily for these abilities. To resolve field names, enum values, presence semantics, and message structure at runtime, it has to walk message descriptors using Protobuf reflection (protoreflect) and allocate intermediate values.<br>This is the right tradeoff for correctness and compatibility. But it made me wonder: how much of this cost is fundamental to ProtoJSON, and how much comes from repeatedly resolving schema mappings at runtime?<br>So I built protojsonx, partly as an experiment and partly because I wanted to know whether ProtoJSON was inherently slow or whether Go’s implementation was just lacking optimizations. This library implements two strategies: compiling descriptors into flat offset layout tables once at startup (Runtime Table Mode ), and generating static, reflection-free parsing routines via a protoc plugin (Generated Plugin Mode ).<br>In this post, I&rsquo;ll walk through benchmark results comparing standard protojson, standard struct-based JSON (encoding/json and encoding/json/v2 currently living at github.com/go-json-experiment/json), protojsonx in both modes, and raw binary Protobuf (proto and vtproto). Moving reflection out of the hot path ends up getting ProtoJSON performance much closer to binary protobuf and easily out-performs encoding/json and encoding/json/v2.

sudorandom/protojsonx<br>An experimental faster ProtoJSON encoder and decoder for Go.<br>Warning: protojsonx is highly experimental at this stage. It does pass the official Protobuf conformance tests, which gives me confidence that it follows the expected ProtoJSON behavior, but I would still treat it as early-stage software.

What is protojsonx?<br>protojsonx is designed as a mostly API-compatible experimental replacement for Go&rsquo;s official google.golang.org/protobuf/encoding/protojson library. It supports core ProtoJSON behaviors like field names, enums, presence semantics, unknown-field handling, json_name, oneofs, maps, and standard marshaling/unmarshaling options. However, it does not yet cover every edge case or configuration option of the official library (such as dynamic Any resolving).<br>To make serialization faster, it implements two key optimization strategies:<br>Runtime Table Mode : This mode implements the startup compilation strategy. It builds flat offset tables at initialization, allowing it to marshal and unmarshal structures using fast, sequential offset arithmetic instead of per-call descriptor traversal.<br>Generated Plugin Mode : For the highest-performance path, protojsonx provides a protoc plugin (protoc-gen-go-protojsonx) that generates type-specific marshaling and unmarshaling methods directly. At that point, a lot of the runtime bookkeeping disappears. The remaining cost is less about “what field is this?” and more about the unavoidable work of reading or writing JSON. This strategy also inceases the binary size, which may or may not be appropriate.<br>The Benchmark Setup<br>I used the benchmark suite from my previous article, running on Go 1.26 on an Apple M1 Pro. The configurations are:<br>Small: A flat object with 4 fields (string ID, status boolean, age integer, score float).<br>Medium: A nested user signup event containing actor object, string tags, and metadata map.<br>Large: An array repeating the Medium object 100 times.<br>Methodology Notes<br>All tests use equivalent payload shapes and measure end-to-end marshal/unmarshal cost, including allocations. The generic JSON cases use plain Go structs with similar fields, while the protobuf cases use generated protobuf messages.<br>This is not a perfect apples-to-apples comparison. ProtoJSON has extra rules around field names, presence, enums, well-known types, and numeric encoding. The point is narrower: if you already need ProtoJSON-shaped output, how much does that cost compared with the JSON tools Go developers normally reach for? These benchmarks compare the cost of serving similar application-shaped JSON payloads, not identical semantics.<br>I also included hyperpb, a descriptor/layout-driven dynamic protobuf parser built around read-oriented offset...

json protojson protobuf encoding protojsonx bull

Related Articles