Why shorn?
Many formats describe the same payload twice: once in the schema and again through field names and type tags in the bytes. Self-describing formats need that information because they can decode without a schema. If both endpoints already use the same validation schema, shorn can leave it out of the payload.
Against JSON
Section titled “Against JSON”// JSON: field names travel with every record.const bytes = new TextEncoder().encode(JSON.stringify(person)); // 35 bytes
// shorn: field names stay in the schema.const bytes = encode(Person, person); // 8 bytesCompared with JSON converted to and from a Uint8Array, shorn is 77% smaller for this record and faster in both directions on every measured fixture:
| 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 |
shorn is 2.1–6.2× faster to encode and 2.1–14.5× faster to decode. It now wins all ten comparisons against JSON.stringify, even though that baseline stops at a string instead of producing bytes. Sending the string over a socket still requires a byte conversion, whether your code or the platform performs it.
Against the other binary codecs
Section titled “Against the other binary codecs”The formats that beat shorn on speed require a different tradeoff:
| Codec | What it requires |
|---|---|
| Avro | A second schema model, kept in sync with your validator |
| Protobuf | A .proto file, a compiler step, and per-field tags on every record |
| SchemaPack | A custom DSL, weak TS inference, Node Buffer heritage |
| msgpackr records | Out-of-band record state both ends must synchronize |
| MessagePack / CBOR | No schema, but payloads are about 3× larger in these fixtures |
shorn’s ask is that you already use a Standard Schema validator.
Tradeoff summary
Section titled “Tradeoff summary”| Axis | Result |
|---|---|
| Raw size | Best or tied in every fixture |
| Gzip size | Best in both 100,000-event profiles |
| Brotli size | Sixth on repetitive data, second on high-entropy |
| Bundle | 5.39 KB gzip, smallest measured; 9% margin over /msgpack |
| Speed | Fastest measured in both directions on every fixture but Unicode |
| Cold setup | 52–66 µs including Zod schema construction |
| Evolution | None. Detection only |
| Cross-language | TypeScript only |
The supported claim is: small TypeScript-native payloads from the validator you already use, with no schema language, generator, or build step. shorn is not the smallest after every compressor; Brotli can make msgpackr records or Protobuf smaller. It leads the measured field in both directions now, but only on these fixtures, on one machine, against these versions.
When not to use it
Section titled “When not to use it”- Cross-language readers → Avro or Protobuf.
- Evolution with resolution, not detection → Avro.
- Streaming or random access → shorn encodes whole values.
- You are not validating → shorn has no schema to use; choose a self-describing format such as MessagePack.
- Both ends pinned and size is not a cost → JSON is universal and inspectable.