shorn

Your validator is the wire format.

Already have a Zod, Valibot, or ArkType schema? Then you already have a binary format. Install shorn and pass that schema to encode and decode. There is no schema file to write, no code to generate, and nothing to migrate.

// The validator you already use.
const Person = z.object({
name: z.string(),
age: z.int().nonnegative(),
role: z.enum(["viewer", "editor", "admin"]),
});
// It is also the wire format.
const bytes = encode(Person, person);
// → 8 canonical bytes, validated before encoding
const back = decode(Person, bytes);
// → { name: string; age: number; role: "viewer" | "editor" | "admin" }
Read the docs npm install @chichurita/shorn
The field names stay in your schema, so only the values go on the wire. A field is identified by its position, not by its name. Nothing was lost: the names are still in your schema, where they were the whole time.

Where the bytes go

Your schema already knows the field names, their order, and the enum members, so shorn sends only the values.

{"name":"Grace","age":45,"role":"admin"} is 40 bytes as JSON, 20 as a positional array, and 8 bytes through shorn: 2d is age 45, 05 is the string length, 47 to 65 is Grace, 00 is the enum index of "admin"

How it works walks through the eight bytes one at a time.

Install it. Keep your schema.

shorn reads the wire layout from your schema. Zod, Valibot, or ArkType still owns validation, transformations, and error messages, exactly as before.

shorn
Nothing to set up. Call encode(schema, value) and decode(schema, bytes) with the validator you already have.
Avro
Write and maintain a second schema in Avro's own format, then build an Avro codec from it.
SchemaPack
Describe the same shape a second time with SchemaPack's builder.
Protobuf
Write a .proto file, then either generate code from it or set up runtime reflection.
That same Person record, encoded by each of them.
shorn 8 bytes
Avro (own schema) 8 bytes
Protobuf (.proto + codegen) 11 bytes
SchemaPack (own builder) 13 bytes
msgpackr plain (field names) 30 bytes
JSON (field names + text) 40 bytes
Avro ties on bytes. It needs a second schema in its own format to get there, and shorn needs nothing you have not already written.

Try it

Pick a validator, then edit its schema and the payload. The real encoder runs in your browser tab, and nothing you type leaves it.

Validator

Suggestions appear as you type: arrows move, Enter or Tab picks. Otherwise Tab indents, and Escape first lets it leave the box.

loading the encoder…

The record above is a deliberate best case: booleans and enum members are what shorn shrinks hardest. Every figure quoted elsewhere on this page is measured on the benchmark fixtures instead, which are ordinary records.

Built for shared TypeScript schemas

For caches, RPC, worker messages, job queues, and storage, where both ends share the schema.

Not for readers in other languages, self-describing payloads, or schemas that change independently. fingerprinted() rejects bytes written by an older shape.

Canonical bytes

The same value always encodes to the same bytes, so a payload can be a cache key.

Defensive decoding

Every length is checked before allocating. Invalid UTF-8 and leftover bytes are rejected.

Smallest or tied in every raw fixture

Small payloads normally cost CPU, because a compressor has to produce them. These come from the schema instead, in the same pass that encodes the value.

Person { age, name, sex }
Event { active, actor: Person, id,
metrics: { cpu, memory }, tags[], timestamp }

Four fixtures: a Person, the same Person with non-ASCII text, one Event, and a batch of 100 Events in an array.

4.3 MB
for 100,000 events that take 16.5 MB as JSON, before any compressor runs. Gzipping those shorn bytes afterwards costs a further 48 ms of CPU.
up to 6.7×
faster than JSON to encode, and up to 13.0× faster to decode.
up to 77%
smaller than JSON before compression.

Read the results, caveats, and reproduction steps.