Skip to content

Validation (Standard Schema & JSON Schema)

Every codec — Timestamp, Reverse Timestamp, Signed Timestamp, Opaque Timestamp, Wrapped key, and Digest — exposes two shared validation surfaces:

  • ~standardStandard Schema v1 support, letting any codec drop directly into ArkType, Valibot, tRPC, and other Standard Schema-compatible libraries without hand-rolling a refine callback.
  • toJsonSchema() — synchronous JSON Schema export for OpenAPI document generation, JSON Schema validators, and documentation tooling.

Both surfaces work on the wire form only — they validate prefix and base32 shape, require no key material, and are fully synchronous on every codec, including keyed ones (Opaque Timestamp, Wrapped key, Signed Timestamp, Digest) where key-dependent operations are async.

Each codec’s ~standard property implements Standard Schema v1:

codec["~standard"].version; // 1
codec["~standard"].vendor; // "@smonn/ids"
codec["~standard"].validate(value);
// → { value: Id<Brand> } on success
// → { issues: Array<{ message: string }> } on failure
Property Value Notes
version 1 Fixed — Standard Schema protocol version
vendor "@smonn/ids" Fixed — identifies this library
validate (unknown) => ... Sync; wraps safeParse; returns canonical ID
Outcome Return shape
Valid { value: Id<Brand> } (issues is undefined)
Invalid { issues: [{ message: string }, ...] }

validate accepts the same mixed-case and Crockford visual aliases (o → 0, i → 1, l → 1) that safeParse does, and returns the canonical Id<Brand> on success. Each failure maps to a distinct message:

ParseError issues[0].message
not_string "expected string"
invalid_prefix "expected prefix 'usr_'"
invalid_base32 "invalid base32 payload"

Any Standard Schema v1-compatible library accepts a codec directly as a schema member. For example, with ArkType:

import { type } from "arktype";
import { createTimestampId } from "@smonn/ids";
const users = createTimestampId("usr");
const Body = type({ userId: users });
const r = Body({ userId: "USR_06F80Z92D2DBSQQG28T5CY4TQG" });
// → { userId: "usr_06f80z92d2dbsqqg28t5cy4tqg" } typed as { userId: Id<"usr"> }

The same pattern works with any library that consumes a Standard Schema validator — tRPC, oRPC, TanStack, and others. Libraries that only produce Standard Schema (Zod, Valibot) need a small wrapper instead. See Framework integrations for a per-library guide.

If a library does not yet support Standard Schema, call validate directly:

const result = users["~standard"].validate(req.body.userId);
if ("issues" in result) {
return res.status(400).json({ error: result.issues[0].message });
}
const userId = result.value; // Id<"usr">, canonical

toJsonSchema() returns a plain object describing the canonical wire form:

users.toJsonSchema();
// {
// type: "string",
// pattern: "^usr_[0-9a-hjkmnp-tv-z]{25}[048cgmrw]$",
// description: "Branded ID for 'usr'",
// example: "usr_00000000000000000000000000",
// }
Field Type Notes
type string Always "string"
pattern string Anchored regex matching the canonical form only — see note below
description string "Branded ID for '<brand>'" — includes the brand literal
example string A deterministic structural placeholder — stable across calls

The regex matches generate() output and is()not the lenient forms accepted by safeParse(). Uppercase letters and Crockford visual aliases (O, I, L) do not match. The final character is constrained to one of [048cgmrw] because 16 bytes encoded in 26 Crockford base32 characters (130 bits) leave 2 surplus padding bits; canonical encoding sets them to zero. See ADR-0003.

example is a deterministic structural placeholder (prefix + "0".repeat(26)) computed once at codec construction — stable across repeated toJsonSchema() calls. It always satisfies the returned pattern.

Drop the result straight into an OpenAPI components.schemas entry:

const schema = users.toJsonSchema();
// in your OpenAPI document builder:
components.schemas.UserId = schema;
// → { type: "string", pattern: "^usr_...$", description: "...", example: "..." }

Both surfaces are available — and synchronous — on every codec variant:

Codec ~standard toJsonSchema() Notes
Timestamp sync sync
Reverse Timestamp sync sync
Signed Timestamp sync sync generate / verify are async; these two are not
Opaque Timestamp sync sync generate / extractTimestamp are async
Wrapped key sync sync wrap / unwrap are async
Digest sync sync digest is async

Neither surface reads the payload — they validate prefix and base32 structure only — so no key is needed and there is nothing to await.