Skip to content

Introduction

shorn turns the validation schema you already have into a binary format. It reads two things from that schema: Standard Schema for validation, and Standard JSON Schema for structure.

Both are small shared interfaces that Zod, Valibot, and ArkType already implement, so shorn needs no adapter for any of them, nor for the next validator that implements the two. Because your validator already describes every field and its type, there is no separate schema file to write, no code to generate, and no second copy of your types to keep in sync.

import { z } from "zod";
import { decode, encode } from "@chichurita/shorn";
const Person = z.object({
name: z.string(),
age: z.int().nonnegative(),
sex: z.enum(["M", "F", "X"]),
});
const bytes = encode(Person, { name: "Grace", age: 45, sex: "F" }); // 8 bytes
const decoded = decode(Person, bytes);

As minified JSON, that value is 35 bytes. shorn writes 8, because the field names and type markers stay in the schema instead of being repeated in every payload. Where the bytes go walks from 35 down to 8 in three steps.

In every case:

  • The same schema written in Zod, Valibot, or ArkType produces the same bytes.
  • Your validator runs before encoding and again after decoding, so a payload never skips your rules.
  • The runtime is small. Helpers you do not import cost nothing in your bundle.
  • MIT licensed.

shorn is a good fit when all of these are true: both ends of the wire are TypeScript or JavaScript, your application already validates its data, both ends can share one schema, and payload size or serialization cost matters to you. If any of those is false, Comparisons says what to use instead.

Some of these are design decisions rather than gaps a later release will fill:

  • No streaming: nor random access or zero-copy views.
  • No cross-language decoder: TypeScript and JavaScript only.
  • Some values have no wire form: Date, bigint, Map, Set and date-time strings are supported natively. undefined, symbols, RegExp, class instances and one-way transforms are not, so convert those before encoding.
  • Not confidential: the bytes are compact, not secret. Encrypt them when secrecy matters.
  • No universal speed guarantee: results depend on your schema, your data, the runtime, and compression. See Throughput and measure your own workload.
  • No schema evolution: only the exact wire shape that wrote a payload can decode it. fingerprinted() puts a short identifier for the wire shape in front of the payload, so it catches most mismatches instead of misreading them, but nothing migrates old payloads for you.

If you are still deciding, read Comparisons first. Otherwise: Installation, then Quick start, then How it works for the model behind the bytes.

From there, Using payloads covers sending and storing them, and Wire fingerprints is worth reading before you store any.