Skip to content

Low-Level m API

m builds codecs directly from the wire format, without a validation library or JSON Schema. Use it when you need direct control over the wire shape.

import { m } from "shorn";
const Person = m.object({
name: m.string(),
age: m.uint(),
sex: m.enum(["M", "F", "X"]),
});
const bytes = Person.encode({ name: "Grace", age: 45, sex: "F" });
// [45, 5, 71, 114, 97, 99, 101, 0] — identical to the compiled Zod codec

Types are inferred: Person.decode(bytes) is { name: string; age: number; sex: "M" | "F" | "X" }.

m performs only the validation needed to write the wire format. For example, an integer must be safe and a string must actually be a string. It does not enforce .min(1), email formats, or business rules, and it has no .refine().

Use it when no validator schema is available, when you need float32 or raw bytes, or when writing a test fixture.

m is one object, so a bundler cannot drop the builders you never call: import { m } is about 3.9 KB gzip whether you use two or twelve. Everything else in shorn tree-shakes per export — see Footprint. If a bundle is tight and only needs the compiled path, importing codec without m is 137 gzip bytes cheaper. The reasoning, and the named-export alternative that was measured and declined, are in ADR 0004.

BuilderWireNotes
m.string()varint length + UTF-8fatal UTF-8 decoding
m.bytes()varint length + rawno JSON Schema equivalent
m.boolean()one byte
m.uint()varintnon-negative safe integers
m.int()ZigZag varint
m.float32()4 bytes LEno JSON Schema equivalent
m.float64()8 bytes LEwhat z.number() selects
m.literal(v)zero bytesstring | number | boolean | null
m.enum([...])varint index, sortedneeds ≥1 member
m.array(item)varint count + elements
m.tuple([...])elements onlylength from the schema
m.object({...})presence bitmap + valuescanonical key order

Every Schema also has .optional() and .nullable():

m.object({
name: m.string(),
nickname: m.string().optional(), // a bit in the presence bitmap
manager: m.string().nullable(), // a discriminator byte
});

m cannot override canonical field order or the enum index base because both are wire-format rules. m.object({ b, a }) writes a first, just like a compiled codec. test/golden.test.ts verifies this byte compatibility and cross-validator identity.

fingerprinted(m.object({ name: m.string() }));
// EncodeError: fingerprinted() needs a codec built from a Standard JSON Schema;
// compile() returns one, the low-level m API does not

The fingerprint hashes the canonical structural signature, derived only from a Standard JSON Schema. Raw wire is unframed by design.

import { Reader, Schema, Writer } from "shorn";
class Pair extends Schema<[number, number]> {
_minWidth = 2;
_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, the fewest bytes a value can use, if the schema may appear in an array. This lets m.array() reject an impossible count before allocating. A schema with width 0 cannot be an array element.

On performance-critical paths, prefer a byte() loop to reader.bytes(n). The latter returns a new subarray and costs about 24 ns in the benchmark.

This surface is unstable — _encode, _decode, and _minWidth can change in a minor release. See m Builders for full signatures.