shorn vs JSON
JSON is the baseline most TypeScript projects are replacing.
| Fixture | shorn | JSON | Saving |
|---|---|---|---|
| Person | 8 | 35 | 77% |
| Unicode person | 31 | 58 | 47% |
| Nested event | 43 | 163 | 74% |
| 100 events | 4,135 | 16,148 | 74% |
| 100,000 events | 4,231,777 | 16,340,686 | 74% |
The Unicode row shows where the savings come from: shorn removes field names and syntax, not string content. Payloads dominated by structure and numbers can be about 75% smaller than JSON. Payloads dominated by free text see smaller gains.
Compressed, on 100,000 repetitive events:
| shorn | JSON | Saving | |
|---|---|---|---|
| Gzip | 924,494 | 1,474,952 | 38% |
| Brotli q6 | 603,189 | 985,919 | 39% |
Repeated keys are what a compressor is good at, so compression narrows the gap without closing it.
The benchmark uses two JSON baselines.
JSON bytes encodes to and decodes from a Uint8Array. This is the direct comparison for binary transports. shorn wins every result:
| Fixture | shorn enc | JSON enc | shorn dec | JSON dec |
|---|---|---|---|---|
| Person | 25.16M | 4.74M | 67.55M | 4.67M |
| Unicode person | 7.74M | 3.77M | 7.68M | 3.58M |
| Nested event | 8.64M | 1.40M | 11.45M | 1.74M |
| 100-event batch | 100.1k | 36.3k | 116.3k | 21.2k |
| Person, validated | 8.93M | 3.69M | 12.07M | 3.54M |
Decode is where the record decoder shows: for Person, shorn is 14.5× JSON bytes, and the margin holds at 6.6× for the nested event and 5.5× for the batch. The encode margins are now 5.3× on Person and 6.2× on the nested event.
JSON string stops at a JavaScript string and reaches 10.82M encodes/s for Person, against shorn’s 25.16M. It no longer beats shorn on any fixture in either direction. This baseline does less work because it never produces bytes, even though the size result reports UTF-8 byte length. Sending the string over a socket still requires that conversion.
Text output was measured and rejected
Section titled “Text output was measured and rejected”- base64: −31% throughput, +33% size.
- Latin-1 binary string: −25% throughput, and inflates on the wire, since every byte above
0x7Fbecomes two UTF-8 bytes when actually sent.
Neither format matches the JSON string result because that baseline skips byte conversion. Base64 may still make sense for text-only transports because it is about 3× smaller than JSON in this test, but it is not faster.
What JSON keeps
Section titled “What JSON keeps”- Universal. Every language, every tool, no schema needed.
- Inspectable.
curl | jqworks; a shorn payload is opaque without its schema. - No schema coupling. A JSON payload outlives any schema version; a shorn payload does not.
- Streaming. Incremental JSON parsers exist.
- Zero setup. 0.08 µs cold against shorn’s 52–66 µs.
When to switch
Section titled “When to switch”Switch when payload size is a real cost (metered egress, mobile clients, high-volume queues, edge-to-origin), you already validate with Zod, Valibot, or ArkType, and both ends are TypeScript.
Stay with JSON when you need cross-language readers, inspectable payloads, or streaming. JSON is also the simpler choice when payloads are too small or infrequent for the savings to matter.
Migrating
Section titled “Migrating”// Beforeconst body = JSON.stringify(person);const parsed = Person.parse(JSON.parse(text));
// Afterconst body = encode(Person, person);const parsed = decode(Person, body);Handle two migration details. First, Date and bigint fields need an explicit wire representation; see rich types. Second, set Content-Type: application/octet-stream because shorn has no registered media type.