This is the full developer documentation for shorn
# Errors
> EncodeError, DecodeError, and what every message you can hit means.
```ts
class EncodeError extends Error {}
class DecodeError extends Error { readonly offset: number }
```
| Error | Thrown when |
| ------------- | ---------------------------------------------------------------- |
| `EncodeError` | validation failed on the way in, or the schema cannot be encoded |
| `DecodeError` | the bytes are malformed, **or** validation failed on the way out |
`DecodeError.offset` is the byte position reached. For a validation failure, it equals the payload length because structural decoding must consume every byte before validation runs.
```ts
try {
decode(Person, bytes);
} catch (error) {
if (error instanceof DecodeError) {
console.error(`bad payload at byte ${error.offset}: ${error.message}`);
}
}
```
To avoid exceptions, use `safeDecode`. It returns either `{ success: true, data }` or `{ success: false, error }` and wraps values that are not already `Error` objects.
## Schema-construction errors
[Section titled “Schema-construction errors”](#schema-construction-errors)
These are all `EncodeError` instances thrown when the codec is built. See [Rejected Shapes](/schemas/rejected-shapes/) for workarounds.
| Message | Cause |
| -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `Records and open objects are not currently supported` | `z.looseObject`, `z.record`, `v.record` |
| `Only nullable JSON Schema unions are currently supported` | a general or discriminated union |
| `Only nullable JSON Schema type arrays are currently supported` | a `type` array with >1 non-null entry |
| `Arrays require an item schema` | an array with no `items` |
| `Empty enums are unsupported` | an enum with no members |
| `Unsupported JSON Schema literal` | a literal that is not string, number, boolean, or null |
| `Unsupported Standard JSON Schema type X` | a type with no wire shape |
| `Unsupported Standard JSON Schema node` | a non-object node where a schema was expected |
| `Required property "x" has no schema` | `required` names a property absent from `properties` |
| `Schemas with different input and output wire shapes require a bidirectional codec and are not yet supported` | a default or widening refinement makes the sides differ |
| `Standard Schema provides validation but not structure; pass a Standard JSON Schema implementation as the second argument` | Valibot, Zod < 4.2, ArkType < 2.1.28 |
| `This schema already decodes to null; wrapping it in nullable() would give null two encodings` | `m.literal(null).nullable()`, or a second null marker over one already reachable |
| `This schema already decodes to undefined; wrapping it in optional() would give undefined two encodings` | a second presence marker over one already reachable |
| `fingerprinted() needs a codec built from a Standard JSON Schema; compile() returns one, the low-level m API does not` | `fingerprinted(m.object(...))` |
| `Fingerprint bytes must be 1, 2, 3 or 4, received X` | out-of-range `bytes` option |
### Rich types
[Section titled “Rich types”](#rich-types)
```plaintext
— shorn encodes the wire shape; convert rich types
at the edge (README: Dates, BigInt, Map and Set)
```
shorn preserves the validator’s original reason and appends guidance. This applies to `Date`, `bigint`, `Map`, `Set`, `undefined`, `NaN`, and transforms. See [Date, BigInt, Map, Set](/schemas/rich-types/).
### Async
[Section titled “Async”](#async)
```plaintext
This Standard Schema validates asynchronously; use encodeAsync/decodeAsync with
the Standard Schema. Neither accepts a compiled or fingerprinted codec.
```
The second sentence clarifies an important limitation: async entry points accept schemas, not compiled or fingerprinted codecs.
## Encode-time value errors
[Section titled “Encode-time value errors”](#encode-time-value-errors)
| Message | Cause |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `Unknown object property "x"` | an extra property where the vendor left `additionalProperties` absent — ArkType by default, Valibot’s `object` and `looseObject` |
| *validation issues, joined by `;`* | your refinements failed; paths prefixed as `field.nested: message` |
## Decode-time errors
[Section titled “Decode-time errors”](#decode-time-errors)
All `DecodeError` with an `offset`.
| Message | Cause |
| ------------------------------------------------------------------------- | -------------------------------------- |
| `Expected a Uint8Array, received X` | wrong input type; offset 0 |
| `Unexpected trailing data` | bytes remained after a complete value |
| `Payload was written by a different schema (expected fingerprint XXXXXX)` | the schema changed |
| *out-of-bounds read* | truncated payload |
| *non-canonical varint* | overlong, e.g. `[129, 0]` for `1` |
| *unsafe integer* | a varint beyond the safe integer range |
| *invalid UTF-8* | decoding is fatal, not replacing |
| *invalid boolean* | a byte other than `0` or `1` |
| *invalid enum index* | past the last member |
| *element count exceeds remaining input* | a count the payload cannot satisfy |
Handle fingerprint mismatches explicitly in production. In the mismatch test, decoding without a fingerprint produced a **wrong value without an error** about 27% of the time. See [Schema Evolution](/versioning/schema-evolution/).
## One error that is not a `DecodeError`
[Section titled “One error that is not a DecodeError”](#one-error-that-is-not-a-decodeerror)
A schema nested about 5,900 levels deep can overflow the JavaScript stack and throw `RangeError` instead of `DecodeError`. This requires a hostile **schema**, not only hostile bytes. Limit schema depth if schemas come from untrusted input. See [Hostile Input](/hostile-input/).
# Functions
> Signatures and behavior for encode, decode, the safe and async variants, compile, and fingerprinted.
Each function has two overloads. One accepts schemas that implement both Standard interfaces. The other accepts a Standard Schema plus `{ structure }`.
## `encode`
[Section titled “encode”](#encode)
```ts
encode(schema: S, value: InferOutput): Uint8Array;
encode(schema: S, value: InferOutput, options: CompileOptions): Uint8Array;
```
Validates, then writes bytes. Throws `EncodeError` if validation fails or the schema cannot be encoded.
The returned `Uint8Array` is an **exact-size copy**, not a view into a reused buffer. It is safe to retain. The wire plan is cached by schema identity.
## `decode`
[Section titled “decode”](#decode)
```ts
decode(schema: S, bytes: Uint8Array): InferOutput;
decode(schema: S, bytes: Uint8Array, options: CompileOptions): InferOutput;
```
Reads the structure, then validates. Throws `DecodeError` for malformed bytes **and** for validation failures on the way out.
Trailing bytes cause an error. An input that is not a `Uint8Array` produces a `DecodeError` rather than a raw `TypeError`. Cross-realm arrays from `node:vm`, an iframe, or jsdom are accepted through a tag check when `instanceof` fails.
## `safeEncode` / `safeDecode`
[Section titled “safeEncode / safeDecode”](#safeencode--safedecode)
```ts
safeEncode(schema, value, options?): SafeResult;
safeDecode(schema, bytes, options?): SafeResult>;
type SafeResult = { success: true; data: T } | { success: false; error: Error };
```
Same behavior without throwing. Non-`Error` throws are wrapped, so `result.error` is always an `Error`.
## `encodeAsync` / `decodeAsync`
[Section titled “encodeAsync / decodeAsync”](#encodeasync--decodeasync)
```ts
encodeAsync(schema, value, options?): Promise;
decodeAsync(schema, bytes, options?): Promise>;
```
Use these functions for schemas with **asynchronous** refinements. Both accept a schema, not a codec, so async validation does not compose with `fingerprinted()`. Carry `codec.fingerprintHex` separately if you need both. There are no safe async variants. See [Validation](/core-concepts/validation/).
Calling `encode`/`decode` on an async schema throws:
> This Standard Schema validates asynchronously; use encodeAsync/decodeAsync with the Standard Schema. Neither accepts a compiled or fingerprinted codec.
## `compile`
[Section titled “compile”](#compile)
```ts
compile(schema: S): Schema>;
compile(schema: S, options: CompileOptions): Schema>;
```
Returns the cached wire plan as a codec with `.encode()` and `.decode()`. Aliases: `codec`, `fromStandard`.
**No build step.** `compile` builds a tree of `Schema` objects in memory and writes nothing to disk. Repeated calls with the same schema and structure object return the **same** cached instance. An object schema with no optional fields also builds a decoder with `new Function`, falling back to the interpreted path where a Content Security Policy forbids it — see [Compilation and Caching](/core-concepts/compile-and-caching/).
## `fingerprinted`
[Section titled “fingerprinted”](#fingerprinted)
```ts
fingerprinted(codec: Schema, options?: FingerprintOptions): FingerprintedSchema;
```
Prefixes payloads with a short FNV-1a digest of the schema’s canonical wire signature.
```ts
const codec = fingerprinted(compile(Person));
codec.encode(person); // 3 + payload bytes
codec.fingerprint; // Uint8Array — a fresh copy every read
codec.fingerprintHex; // "7236d1" — the Map key for dispatch
```
Throws `EncodeError` for a codec without a signature:
> fingerprinted() needs a codec built from a Standard JSON Schema; compile() returns one, the low-level m API does not
It also throws if `bytes` is outside 1–4. Performance is effectively the same at every width, so use the default 3 bytes unless you have a specific protocol constraint. See [Fingerprinting](/versioning/fingerprinting/).
### `FingerprintedSchema`
[Section titled “FingerprintedSchema”](#fingerprintedschema)
| Member | Type | Notes |
| ---------------- | ------------ | ------------------------------------------ |
| `fingerprint` | `Uint8Array` | Fresh copy every read; cannot key a `Map` |
| `fingerprintHex` | `string` | Lowercase hex, immutable, the dispatch key |
`fingerprint` returns a copy so callers cannot mutate the codec’s internal bytes. Otherwise, an accidental write could make the codec non-canonical while it still round-trips against itself.
## `Schema`
[Section titled “Schema\”](#schemat)
```ts
abstract class Schema {
encode(value: T): Uint8Array;
decode(value: Uint8Array): T;
optional(): OptionalSchema;
nullable(): NullableSchema;
readonly signature?: string; // only on codecs built from a JSON Schema
}
```
`signature` is type-only on the base class, so users who do not import `fingerprinted()` pay no runtime cost for it. `_encode`, `_decode`, and `_minWidth` are internal and may change in a minor release. See [Low-Level m API](/wire-format/low-level-api/).
# m Builders
> Reference for the low-level wire builders, Reader and Writer, and Infer.
`m` builds a codec directly from the wire format without a validation library. It is an escape hatch, not a replacement for your validator. See [Low-Level m API](/wire-format/low-level-api/).
```ts
import { m, type Infer } from "shorn";
const Point = m.object({ x: m.int(), y: m.int() });
type Point = Infer; // { x: number; y: number }
```
## Primitives
[Section titled “Primitives”](#primitives)
| Builder | Type | Wire |
| ------------- | -------------------- | ---------------------------------- |
| `m.string()` | `Schema` | varint byte length + UTF-8 |
| `m.bytes()` | `Schema` | varint byte length + raw |
| `m.boolean()` | `Schema` | one byte, `0` or `1` |
| `m.uint()` | `Schema` | varint; non-negative safe integers |
| `m.int()` | `Schema` | ZigZag varint |
| `m.float32()` | `Schema` | 4 bytes, little-endian |
| `m.float64()` | `Schema` | 8 bytes, little-endian |
`m.bytes()` and `m.float32()` have no JSON Schema form, so no validator selects them for you. They are also the only two builders a `compile()` codec can never reach, which is why the Standard Schema compiler does not go through `m` at all.
`m` does not tree-shake per builder
`m` is a single object, so `import { m }` retains all twelve builders whether you call two or twelve — about 3.9 KB gzip. Bundlers cannot drop a property of a live object. Splitting the builders into named exports would cut that to roughly 3.0 KB for a typical three-builder import; it was measured and declined, because `m` is the escape hatch and `compile()` users see none of the saving. See [ADR 0004](https://github.com/ChiChuRita/shorn/blob/main/docs/adr/0004-m-stays-one-object.md).
## `m.literal(value)`
[Section titled “m.literal(value)”](#mliteralvalue)
```ts
m.literal(value: T): Schema;
```
Literals use zero bytes. They cannot be array elements because the decoder could not verify the declared element count against the payload length.
## `m.enum(values)`
[Section titled “m.enum(values)”](#menumvalues)
```ts
m.enum(values: T): Schema;
```
A varint index into the **sorted, deduplicated** member list, so declaration order does not affect bytes. At least one member required; an index past the last is a `DecodeError`.
## `m.array(item)`
[Section titled “m.array(item)”](#marrayitem)
```ts
m.array(item: Schema): Schema;
```
Writes a varint count followed by the elements in order. Codec construction fails if `item` can use zero bytes. Arrays are limited to 1,000,000 elements, and impossible counts are rejected before allocation.
## `m.tuple(items)`
[Section titled “m.tuple(items)”](#mtupleitems)
```ts
m.tuple[]>(items: S): Schema>;
```
Elements only; length from the schema, positions never reordered. **May** contain zero-width elements, unlike `m.array`.
## `m.object(shape)`
[Section titled “m.object(shape)”](#mobjectshape)
```ts
m.object(shape: S): Schema>;
```
Presence bitmap for optional fields (`ceil(n / 8)` bytes, omitted when nothing is optional), then values in **canonical key order** — UTF-16 ascending. Declaration order is irrelevant. Unknown properties throw on encode.
## `.optional()` and `.nullable()`
[Section titled “.optional() and .nullable()”](#optional-and-nullable)
```ts
m.object({
name: m.string(),
nickname: m.string().optional(), // a bit in the presence bitmap
manager: m.string().nullable(), // discriminator byte, then the value
});
```
`optional()` is only meaningful as an object field — it *is* the bitmap bit. `nullable()` works anywhere and always costs one byte.
### Markers never stack
[Section titled “Markers never stack”](#markers-never-stack)
A second marker for a value the schema can already produce would give that value two encodings, so `[0]` and `[1, 0]` would both decode to `undefined` and decoding would stop being injective. shorn refuses that, in one of two ways.
**Repeating the same wrapper is a no-op.** It returns the identical object, so this is safe in generic code that does not know what it was handed:
```ts
const a = m.string().optional();
a.optional() === a; // true
```
**Producing a second marker for the same value throws an `EncodeError`** when the codec is built:
```ts
m.literal(null).nullable(); // throws: already decodes to null
m.string().optional().nullable().optional(); // throws: already decodes to undefined
m.string().nullable().optional().nullable(); // throws: already decodes to null
compile(z.string().nullable()).nullable(); // throws, carried through compile
```
Both flags propagate through the other wrapper, `compile()`, and `fingerprinted()`, so a stack three deep is caught as readily as one. Mixing the two markers once is fine and is a real shape: `m.string().optional().nullable()` distinguishes absent from null.
## `Reader` and `Writer`
[Section titled “Reader and Writer”](#reader-and-writer)
```ts
class Writer {
byte(value: number): void;
bytes(value: Uint8Array): void;
varuint(value: number): void;
varuintBigInt(value: bigint): void;
string(value: string): void;
finish(): Uint8Array;
reset(): void;
}
class Reader {
readonly position: number;
readonly done: boolean;
byte(): number;
bytes(length: number): Uint8Array;
string(): string;
varuint(): number;
varuintBigInt(): bigint;
varuintWide(): number | bigint;
}
```
```ts
class Pair extends Schema<[number, number]> {
_minWidth = 2; // fewest bytes a value can occupy
_encode(writer: Writer, value: [number, number]) {
writer.varuint(value[0]);
writer.varuint(value[1]);
}
_decode(reader: Reader): [number, number] {
return [reader.varuint(), reader.varuint()];
}
}
```
Set `_minWidth` if the schema may be used as an array element. It lets `m.array` reject an impossible count before allocating. A schema with width 0 cannot be an array element.
`reader.bytes(n)` returns a fresh subarray and costs about 24 ns in the benchmark. Prefer a `byte()` loop on performance-critical paths.
This surface is unstable: `_encode`, `_decode`, and `_minWidth` can change in a minor release.
## Types
[Section titled “Types”](#types)
```ts
type Infer> = S["_output"];
type Shape = Record>;
type ObjectOutput; // the decoded object type for a shape
```
## Byte-compatible with `compile`
[Section titled “Byte-compatible with compile”](#byte-compatible-with-compile)
```ts
compile(z.object({ name: z.string(), age: z.int().nonnegative() }));
m.object({ name: m.string(), age: m.uint() });
// byte-identical output for the same value
```
`m` cannot override canonical field order or the enum index base, which keeps it byte-compatible with `compile`. `test/golden.test.ts` verifies this behavior.
# API Overview
> The whole public surface on one page.
```ts
// Encode and decode
encode(schema, value, options?): Uint8Array;
decode(schema, bytes, options?): Output;
safeEncode(schema, value, options?): SafeResult;
safeDecode(schema, bytes, options?): SafeResult