Validation
shorn runs your schema both ways: encode validates before writing bytes, decode validates the structurally-decoded value before returning it.
const bytes = encode(Person, person); // validates, then writesconst back = decode(Person, bytes); // reads, then validatesThese checks are not redundant. The wire format knows that a field is a string; only your schema knows that it must be a non-empty email address under 64 characters.
| 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. See Errors.
Results instead of exceptions
Section titled “Results instead of exceptions”const result = safeDecode(Person, bytes);if (result.success) result.data; // typedelse result.error; // always an Error, non-Error throws are wrappedUse the safe variants at boundaries where malformed input is expected rather than exceptional.
Async validation
Section titled “Async validation”A schema with an async refinement cannot use encode/decode — they throw and say so.
const bytes = await encodeAsync(Person, person);const back = await decodeAsync(Person, bytes);Both take the Standard Schema, never a codec. There are no safe async variants — use try/catch.
Async and fingerprinted do not compose
Section titled “Async and fingerprinted do not compose”fingerprinted() produces a codec, and no async entry point accepts one. A schema with an async refinement must choose.
The reason is bundle size: class methods do not tree-shake.
| import set | minified | gzip |
|---|---|---|
codec | 17,389 | 5,383 |
+ m | 17,147 | 5,314 |
+ safeEncode / safeDecode | 17,435 | 5,382 |
+ encodeAsync / decodeAsync | 17,748 | 5,456 |
+ fingerprinted | 18,946 | 5,854 |
| everything | 19,446 | 6,023 |
Async support adds 313 minified and 74 gzip bytes, and only users who import it pay that cost. Moving async methods onto Schema would add those bytes for everyone, including users of only the low-level m API. See ADR 0001.
Workaround: carry codec.fingerprintHex separately, such as in a Kafka header or database column. Keep the payload bare and run async validation through the schema.
Which entry point
Section titled “Which entry point”| Situation | Use |
|---|---|
| Ordinary code | encode / decode |
| Untrusted input | safeEncode / safeDecode |
| Async refinement | encodeAsync / decodeAsync |
| A codec to pass around | compile |
| Stored, queued, version-crossing | fingerprinted(compile(schema)) |
All five use the same structural decode path through Schema.decode, so they report the same errors. test/fuzz.test.ts verifies this because a previous private path skipped the Uint8Array check and leaked a raw TypeError for invalid input.