Sonic JSON provider

Replacement for the standard library encoding/json backed by bytedance/sonic. It JIT-compiles per-type encoders and decoders for hot paths.

Overview

Sonic compiles per-type encoders and decoders to machine code on first use, then reuses those compiled paths for later calls. Long-running services marshal the same struct shapes over and over. Request and response DTOs, server-action payloads, and frontend hydration are the common case, so the JIT cost amortises fast.

The provider is one small Provider implementation behind piko's JSON facade. It adds no API surface of its own. It reuses the same Marshal, Unmarshal, streaming, and Config port that the rest of piko already uses.

Activation

WithJSONProvider calls provider.Activate() once during bootstrap. Activate rebinds all of piko's package-level JSON functions to sonic, including the pikojson.Marshal, Unmarshal, encoder, decoder, and config variables. After that point, every JSON call across the binary goes through sonic. Activation is process-wide. The binary holds one JSON implementation at a time, and there is no per-call opt-out.

Frozen configs resolve lazily. pikojson.Freeze returns a proxy that defers to the active provider on its first call. A config frozen in init() therefore picks up sonic once it activates later in bootstrap, so the order of freezing against activation does not matter.

Generated action registries pre-register their DTO types in piko's pretouch buffer. Activate drains that buffer and precompiles each buffered type, so first-request latency on action payloads matches steady-state latency with no manual Pretouch calls.

What changes for you

After activation, sonic uses its default config (sonic.ConfigDefault), which differs from encoding/json in two output-visible ways.

  • Map keys are not sorted. The standard library sorts map keys alphabetically on encode. Sonic leaves them unsorted. Code that depends on deterministic JSON, such as snapshot tests or content signatures, sees different bytes.
  • HTML is not escaped. The standard library escapes <, >, and & by default. Sonic does not. If you embed JSON in HTML, escape it yourself or use a config that sets EscapeHTML.

To keep stdlib-compatible behaviour for a given config, freeze a Config with EscapeHTML and SortMapKeys set, and use that frozen API instead of the package-level functions.

Configuration

import (
    sonicjson "piko.sh/piko/wdk/json/json_provider_sonic"
)

provider := sonicjson.New()

New() takes no arguments and returns the Provider interface from piko's JSON facade. There is no provider-specific option to set.

Per-config behaviour comes from piko's shared json.Config, which the sonic provider honours through its freeze path. The fields are CopyString, UseInt64, EscapeHTML, and SortMapKeys. CopyString and UseInt64 apply only on the sonic path. The stdlib fallback honours EscapeHTML and ignores the rest.

Bootstrap

ssr := piko.New(
    piko.WithJSONProvider(sonicjson.New()),
)

Architecture and toolchain

The sonic JIT compiles on amd64 (go1.17 and later) and arm64 (go1.20 and later). On any other architecture, or on go1.27 and later, sonic falls back to a path that is encoding/json. On that fallback path there is no JIT and no speed gain, and HTML escaping and map-key sorting return to the standard library behaviour. Match the deployment target to a supported architecture and Go version to keep the compiled path.

The provider package is pure Go and needs no build tag or CGO. It pulls in golang-asm, cpuid, base64x, and golang.org/x/sys through sonic.

See also

Framework docs:

External: