Safe Schema

Declarative input validation with a Zod-shaped chained-method surface. Built for request-body validation, config validation, API payload validation, anywhere operators have an unknown shape they need to confirm before reading. Vendor-free; built on framework primitives. No JIT, no codegen, no chained-Promise weirdness.

Schemas are immutable — every chained check returns a new schema; the original is untouched. parse(input) throws SafeSchemaError carrying a full per-field issues array; safeParse(input) never throws and returns { ok, value?, errors? } (operator-friendly at HTTP boundaries).

Security guarantees: prototype-pollution defense — __proto__ / constructor / prototype keys are rejected at object-shape construction AND at parse-time on object + record inputs (mirrors b.safeJson's POISONED_KEYS). Per-format defensive length caps (email 254, url 8 KiB, uuid 50, datetime 100 chars …) bound input BEFORE the regex engine runs, so a hostile payload can't ReDoS .email() / .url() / .datetime(). All format regexes are static module-level constants — no string→regex parsing on the validation path. .refine() predicates that throw turn into a regular validation issue rather than crashing the request.

Coercion is deliberately not shipped (.coerce is a footgun: "0" → 0 vs "0" → "0" ambiguity, truthy/falsy edge cases). Operators do explicit s.preprocess(fn, schema) instead.

Relationship to b.forms.validate: forms.validate carries HTML-spec concerns (checkbox coercion, select option allowlist) that don't belong on the general-purpose validator, so the two surfaces stay distinct rather than one wrapping the other.

Surface (every schema has these chained methods unless noted):

Type constructors: string() .min, .max, .length, .regex, .email, .url, .uuid, .datetime (ISO-8601), .date (YYYY-MM-DD), .ip, .ipv4, .ipv6, .nonempty, .startsWith, .endsWith, .includes, .cuid, .ulid, .base64, .trim, .toLowerCase, .toUpperCase number() .int, .min, .max, .gt, .lt, .positive, .negative, .nonnegative, .nonpositive, .finite, .safe, .multipleOf boolean() literal(v) enum_([...]) | oneOf([...]) null_(), undefined_(), any(), unknown()

Composites: object({...}) .strict, .passthrough, .pick, .omit, .extend, .partial, .required array(item) .min, .max, .length, .nonempty tuple([...]) .rest(item) union([...]) first matching option wins discriminatedUnion(key, [...]) tagged-union dispatch record(value) | record(key, value) lazy(() => schema) deferred (recursion) preprocess(fn, schema) pre-validation transform

Modifiers (any schema): .optional(), .nullable(), .default(v|fn), .catch(v|fn), .refine(fn, opts), .transform(fn), .pipe(next)

Deliberately not shipped (with structural reason): z.bigint / z.date / z.map / z.set (no JSON representation), z.nativeEnum / z.never / z.void / z.function (TypeScript-specific), z.coerce (security foot-gun — use s.preprocess), z.intersection (use .extend() for object schemas), z.brand (compile-time tag, no runtime effect), per-schema errorMap (chain .refine() with custom message instead).

Design choices: - Schemas are immutable. Chaining returns a new schema with one additional check; the original is untouched. Cheap because checks are concat'd into a small array, not deep-copied. - .optional() means "may be undefined"; .nullable() means "may be null"; .default(v) means "if undefined, substitute v"; .catch(v) means "on ANY validation failure, substitute v". These compose: optional().default(0) → "may be undefined, in which case use 0". - Objects are STRICT by default: unknown keys produce an issue. Use .passthrough() to retain unknown keys, .strict() to flip back if a parent .passthrough() set the mode. - Sync-only: no async refinements; operators await at the boundary.

b.safeSchema.SafeSchemaError #

stable0.1.0

Error class thrown by every b.safeSchema primitive on construction-time misuse (bad shape / bad enum / bad union / poisoned key) and by schema.parse(...) on validation failure. Built via b.framework.defineClass and marked alwaysPermanent so it never round-trips through retry or transient-error logic. The thrown instance carries .issues — the full per-field issues array ({ path, code, message }[]) — so HTTP middleware can surface every failure in one 400 response.

var b = require("blamejs");
var s = b.safeSchema;

try {
  s.string().min(3).parse("ab");
} catch (e) {
  e instanceof s.SafeSchemaError;
  // → true
  e.issues[0].code;
  // → "string/too-short"
}

b.safeSchema.string() #

stable0.1.0

Construct a string-typed schema. Chain .min, .max, .length, .regex, .email, .url, .uuid, .datetime, .date, .ipv4, .ipv6, .ip, .cuid, .ulid, .base64, .startsWith, .endsWith, .includes, .nonempty, .trim, .toLowerCase, .toUpperCase to add checks and coercions. Each chained call returns a new immutable schema.

The named-format methods (.email / .url / .uuid / etc.) apply a defensive length cap BEFORE running the regex so a hostile payload cannot drive the regex engine with an arbitrarily long string.

var b = require("blamejs");
var s = b.safeSchema;

var name = s.string().min(1).max(80);
name.parse("alice");
// → "alice"

var email = s.string().trim().toLowerCase().email();
email.parse("  Alice@Example.COM  ");
// → "alice@example.com"

var safe = s.string().min(3).safeParse("ab");
safe.ok;
// → false
safe.errors[0].code;
// → "string/too-short"

b.safeSchema.number() #

stable0.1.0

Construct a number-typed schema. Rejects NaN at the type check. Chain .int, .min, .max, .gt, .lt, .positive, .negative, .nonnegative, .nonpositive, .finite, .safe, .multipleOf to bound the value. .safe() enforces the Number.isSafeInteger range — important for IDs that round-trip through JSON (no BigInt support) and need to survive without precision loss.

var b = require("blamejs");
var s = b.safeSchema;

var age = s.number().int().min(0).max(150);
age.parse(30);
// → 30

try { age.parse(200); }
catch (e) { e.issues[0].code; }
// → "number/too-large"

var price = s.number().finite().multipleOf(0.01);
price.parse(19.99);
// → 19.99

b.safeSchema.boolean() #

stable0.1.0

Construct a boolean-typed schema. Strict typeof === "boolean" check — does not coerce truthy/falsy values. To accept the strings "true" / "false" from query parameters, wrap in s.preprocess(fn, s.boolean()).

var b = require("blamejs");
var s = b.safeSchema;

s.boolean().parse(true);
// → true

try { s.boolean().parse("true"); }
catch (e) { e.issues[0].code; }
// → "type"

b.safeSchema.literal(expected) #

stable0.1.0

Construct a schema that accepts exactly one specific value (compared via ===). Useful as the discriminator on tagged unions — see s.discriminatedUnion.

var b = require("blamejs");
var s = b.safeSchema;

var version = s.literal("v1");
version.parse("v1");
// → "v1"

try { version.parse("v2"); }
catch (e) { e.issues[0].code; }
// → "literal"

b.safeSchema.enum_(values) #

stable0.1.0

Construct a schema that accepts any value from the given non-empty array (compared via Set.has). Also exported as b.safeSchema.oneOf because enum is a reserved word in some tooling. Throws SafeSchemaError (safe-schema/bad-enum) when values is not a non-empty array.

var b = require("blamejs");
var s = b.safeSchema;

var role = s.enum_(["admin", "editor", "viewer"]);
role.parse("editor");
// → "editor"

try { role.parse("guest"); }
catch (e) { e.issues[0].code; }
// → "enum"

// oneOf is the same primitive under a friendlier name.
var same = s.oneOf(["a", "b"]);
same.parse("a");
// → "a"

b.safeSchema.null_() #

stable0.1.0

Construct a schema that accepts only null. Trailing-underscore name because null is a reserved word. Useful inside unions (e.g. s.union([s.string(), s.null_()])), though s.string().nullable() is the more common idiom.

var b = require("blamejs");
var s = b.safeSchema;

s.null_().parse(null);
// → null

try { s.null_().parse(0); }
catch (e) { e.issues[0].code; }
// → "type"

b.safeSchema.undefined_() #

stable0.1.0

Construct a schema that accepts only undefined. Trailing- underscore name because undefined shadows poorly. The schema is implicitly optionalparse(undefined) succeeds.

var b = require("blamejs");
var s = b.safeSchema;

s.undefined_().parse(undefined);
// → undefined

try { s.undefined_().parse(null); }
catch (e) { e.issues[0].code; }
// → "type"

b.safeSchema.any() #

stable0.1.0

Construct a schema that accepts any value, including null and undefined. Useful as a placeholder while iterating on a schema shape, or inside s.record(s.any()) when the operator wants the keys validated but not the values.

var b = require("blamejs");
var s = b.safeSchema;

s.any().parse({ anything: "goes" });
// → { anything: "goes" }

s.any().parse(null);
// → null

b.safeSchema.unknown() #

stable0.1.0

Alias for b.safeSchema.any. Some operators prefer the spelling unknown to signal "we accept anything but expect downstream code to narrow the type". Behavior is identical.

var b = require("blamejs");
var s = b.safeSchema;

s.unknown().parse({ raw: 1 });
// → { raw: 1 }

b.safeSchema.object(shape) #

stable0.1.0

Construct an object schema from a { key: schema } shape map. Strict by default — unknown keys produce an object/unknown-key issue. Chain .passthrough() to retain extras, .strict() to flip back, .pick, .omit, .extend, .partial, .required to derive related shapes.

Prototype-pollution defense — __proto__ / constructor / prototype keys are rejected at shape-construction time AND at parse time regardless of mode (.passthrough() does NOT permit them). Throws SafeSchemaError (safe-schema/poisoned-shape-key / safe-schema/bad-shape) on invalid input.

var b = require("blamejs");
var s = b.safeSchema;

var user = s.object({
  email: s.string().email(),
  age:   s.number().int().min(0).max(150),
});

user.parse({ email: "alice@example.com", age: 30 });
// → { email: "alice@example.com", age: 30 }

// Unknown keys rejected by default.
try { user.parse({ email: "a@b.com", age: 30, extra: 1 }); }
catch (e) { e.issues[0].code; }
// → "object/unknown-key"

// Prototype-pollution attempt rejected even with passthrough.
// A hostile __proto__ only becomes an own key through JSON input;
// object-literal `__proto__:` sets the prototype instead.
var loose = user.passthrough();
var hostile = JSON.parse('{ "email": "a@b.com", "age": 30, "__proto__": { "admin": true } }');
var report = loose.safeParse(hostile);
report.ok;
// → false
report.errors[0].code;
// → "object/poisoned-key"

b.safeSchema.array(itemSchema) #

stable0.1.0

Construct an array schema where every element is validated against itemSchema. Chain .min, .max, .length, .nonempty to bound the length. Issues from individual items carry their index in the path (e.g. [3]). Throws SafeSchemaError (safe-schema/bad-item) when the item argument is not a schema.

var b = require("blamejs");
var s = b.safeSchema;

var tags = s.array(s.string().min(1)).max(10);
tags.parse(["alpha", "beta"]);
// → ["alpha", "beta"]

var report = tags.safeParse(["ok", "", "also-ok"]);
report.ok;
// → false
report.errors[0].path;
// → [1]

b.safeSchema.tuple(items) #

stable0.1.0

Construct a fixed-length heterogeneous array schema. items is a non-empty array of schemas, one per slot. Chain .rest(item) to allow a variadic tail (common for [verb, ...args] / [event, payload, ...metadata] shapes). Throws SafeSchemaError (safe-schema/bad-tuple) on invalid input.

var b = require("blamejs");
var s = b.safeSchema;

var pair = s.tuple([s.string(), s.number()]);
pair.parse(["count", 42]);
// → ["count", 42]

// Variadic tail via .rest()
var event = s.tuple([s.string()]).rest(s.number());
event.parse(["sum", 1, 2, 3]);
// → ["sum", 1, 2, 3]

b.safeSchema.union(options) #

stable0.1.0

Construct a schema that accepts a value matching ANY of the given option schemas. First match wins. When no option matches, issues from every branch are collected in the failure for deep diagnostics, plus a parent-level union issue summarizing the miss. For tagged-variant shapes prefer s.discriminatedUnion — it dispatches in O(1) on the tag and produces clearer error messages.

var b = require("blamejs");
var s = b.safeSchema;

var idOrName = s.union([s.number().int().positive(), s.string().min(1)]);
idOrName.parse(42);
// → 42
idOrName.parse("alice");
// → "alice"

try { idOrName.parse(true); }
catch (e) { e.issues[0].code; }
// → "union"

b.safeSchema.record(a, b) #

stable0.1.0

Construct a schema for an object whose KEYS are arbitrary strings and whose VALUES match a given schema. Two call shapes: record(valueSchema) accepts any string key; record(keySchema, valueSchema) validates both keys and values. Prototype-pollution defense — __proto__ / constructor / prototype keys are rejected at parse time, mirroring s.object. Throws SafeSchemaError on invalid arguments.

var bjs = require("blamejs");
var s = bjs.safeSchema;

var counts = s.record(s.number().int().nonnegative());
counts.parse({ apples: 3, pears: 0 });
// → { apples: 3, pears: 0 }

// Validate keys too: only ULIDs allowed.
var byId = s.record(s.string().ulid(), s.string());
byId.parse({ "01HF5Z6Q9P8R7S4T3V2W1X0Y9Z": "alice" });
// → { "01HF5Z6Q9P8R7S4T3V2W1X0Y9Z": "alice" }

b.safeSchema.discriminatedUnion(discriminator, options) #

stable0.1.0

Construct a tagged-union schema. discriminator names a key present on every option as a s.literal(...) schema; the validator dispatches in O(1) on that key's value rather than trying every option in turn (faster + clearer error messages than s.union). Every option must be an object schema whose shape carries a literal at the discriminator key. The discriminator name itself cannot be __proto__ / constructor / prototype. Throws SafeSchemaError on invalid input.

var b = require("blamejs");
var s = b.safeSchema;

var event = s.discriminatedUnion("kind", [
  s.object({ kind: s.literal("created"), at: s.string().datetime() }),
  s.object({ kind: s.literal("deleted"), reason: s.string() }),
]);

event.parse({ kind: "created", at: "2026-01-01T00:00:00Z" });
// → { kind: "created", at: "2026-01-01T00:00:00Z" }

var report = event.safeParse({ kind: "unknown" });
report.ok;
// → false
report.errors[0].code;
// → "discriminated-union/no-match"

b.safeSchema.preprocess(fn, inner) #

stable0.1.0

Wrap a schema with a transform that runs BEFORE validation. Common at HTTP boundaries where query strings arrive as strings but the operator wants a number / boolean schema downstream. Errors thrown by fn propagate as a preprocess issue at the parent path rather than crashing the parse. Throws SafeSchemaError (safe-schema/bad-preprocess) when args are the wrong shape.

Prefer s.preprocess over a hypothetical .coerce because coercion ambiguity ("0" → 0 vs "0" → "0") is a security foot-gun; s.preprocess makes the conversion explicit at the call site.

var b = require("blamejs");
var s = b.safeSchema;

var port = s.preprocess(
  function (v) { return Number(v); },
  s.number().int().min(1).max(65535)
);

port.parse("8080");
// → 8080

var report = port.safeParse("not-a-port");
report.ok;
// → false

b.safeSchema.lazy(getter) #

stable0.1.0

Defer schema construction until first parse, enabling recursive shapes (comment threads, file-tree nodes, AST nodes). getter is a no-arg function that returns the schema; it's called once on the first parse and cached. The returned schema can reference its enclosing variable, breaking the chicken-and-egg cycle. Throws SafeSchemaError (safe-schema/bad-lazy) when getter is not a function.

var b = require("blamejs");
var s = b.safeSchema;

var commentSchema = s.object({
  id:       s.string(),
  replies:  s.array(s.lazy(function () { return commentSchema; })),
});

var input = { id: "a", replies: [{ id: "b", replies: [] }] };
commentSchema.parse(input);
// → { id: "a", replies: [{ id: "b", replies: [] }] }

b.safeSchema.optional(inner) #

stable0.1.0

Composition-style alias for inner.optional(). Returns a schema that accepts undefined in addition to whatever inner accepts. Equivalent to chaining .optional() on the schema; provided for operators who prefer composition over chaining (e.g. mapping over a list of schemas).

var b = require("blamejs");
var s = b.safeSchema;

var maybeName = s.optional(s.string().min(1));
maybeName.parse(undefined);
// → undefined
maybeName.parse("alice");
// → "alice"

b.safeSchema.nullable(inner) #

stable0.1.0

Composition-style alias for inner.nullable(). Returns a schema that accepts null in addition to whatever inner accepts. Compose with s.optional for "may be undefined OR null".

var b = require("blamejs");
var s = b.safeSchema;

var maybeAge = s.nullable(s.number().int().min(0));
maybeAge.parse(null);
// → null
maybeAge.parse(30);
// → 30

Last updated 2026-08-08T16:39:49.652Z by seeder.