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:

  • ~standard — Standard 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.