Skip to content

Primitives and builders

Leaf primitives, combinators, and schema() for objects, arrays, records, and discriminated unions.

AI generated, pending review Updated 10 days ago · Flare 0.3

The @flare-ts/lib/schema entrypoint exports everything for parsing and validating JSON-shaped data: leaf primitives, combinators that wrap them, the schema() builder, and supporting types. Import from @flare-ts/lib/schema (or the default @flare-ts/lib entry). @flare-ts/core does not re-export these symbols.

This page teaches the workflow. For per-symbol signatures and constraint tables, see the API Reference. For named DTO classes, see Models (DTOs).

import {
str, text, int, float, bool, uuid, email, url, date, enums, array,
optional, defaultTo, schema,
} from "@flare-ts/lib/schema";

Callable leaf primitives used as field values inside a descriptor. Each takes a string at the parse boundary and throws on invalid input; safeParse catches the throw and records a FieldError. Chain methods (.min, .max, .pattern, .format) return a new primitive and never mutate the original.

The alternative is parsing each boundary by hand: parseInt on one query value, Number(...) on the next, a typeof check wherever someone remembered one. Those coerce instead of reject, so parseInt("12x") is 12 and Number("") is 0, and the bad value travels on already wearing the type it violated, surfacing as a wrong result or a NaN several functions deep. A primitive makes the boundary the place the value is proven: int rejects anything that is not a whole number and turns the rejection into a FieldError carrying the field path, so malformed input fails at the edge with a located error instead of passing for a number.

PrimitiveSummary
strShort single-line strings; optional .min, .max, .pattern
textMultiline or untrusted text; JSON-escaped on serialize
intIntegers; rejects non-safe integers and decimals
floatFloating-point numbers with optional range
bool"true", "1", "false", "0" (case-insensitive)
uuidUUID v4 canonical format
emailSimplified RFC 5322; returns lowercased
urlWHATWG URL; http: and https: only; returns normalized href
dateDate strings; chain .format("ISO" | "YMD" | "DMY" | "MDY" | "TIMESTAMP")

Reserve str for short values. Use text when the value may contain quotes, newlines, or untrusted characters.

const User = schema({
id: uuid,
name: str.min(1).max(50),
bio: text.max(2_000),
age: int.min(0),
});

Functions that build or wrap a primitive. optional and defaultTo wrap a TypedPrimitive<T>; array primitives keep their string | string[] calling convention through the wrapper. For an optional nested object, call .optional() on the SchemaToken instead.

CombinatorSummary
array(primitive)Comma-separated string or string array of parsed items
optional(primitive)Missing or empty input becomes undefined
defaultTo(fallback, primitive)Missing or empty input becomes fallback
enums([...])Literal union from a readonly string tuple

There are two ways to mark a field optional, at different levels:

  • optional(innerPrimitive) on a primitive field inside a descriptor.
  • nestedToken.optional() when a whole nested object may be absent.
const Tags = schema({
labels: array(str),
count: defaultTo(0, int.min(0)),
role: enums(["admin", "user", "guest"]),
});

schema() creates a SchemaToken that parses JSON field by field. Four forms:

  1. Object descriptor: schema({ id: uuid, name: str })
  2. Top-level array: schema([ItemSchema])
  3. Record: schema([{ $record: ValueSchema }])
  4. Discriminated union: schema<T, "union">(discriminantKey, branches)

safeParse materializes only declared keys; extra input properties are ignored. On success you get typed data. On failure you get { success: false, error: SchemaError } with a fields array. The call never throws.

const result = User.safeParse({ id: "", name: "Ada" });
if (result.success) {
result.data; // { id: string; name: string; ... }
}

Use a schema token on an HTTP route descriptor’s body or response entry, or nest it inside another descriptor; route and query fields take primitives, not tokens. For a named, extendable class wrapper, use model() instead.