zod
Version:
TypeScript-first schema declaration and validation library with static type inference
424 lines (373 loc) • 13.5 kB
text/typescript
import type { $ZodCheck, $ZodStringFormats } from "./checks.js";
import { $constructor } from "./core.js";
import type { $ZodType } from "./schemas.js";
import type { StandardSchemaV1 } from "./standard-schema.js";
import * as util from "./util.js";
///////////////////////////
//// base type ////
///////////////////////////
export interface $ZodIssueBase {
readonly code?: string;
readonly input?: unknown;
readonly path: PropertyKey[];
readonly message: string;
}
////////////////////////////////
//// issue subtypes ////
////////////////////////////////
export interface $ZodIssueInvalidType<Input = unknown> extends $ZodIssueBase {
readonly code: "invalid_type";
readonly expected: $ZodType["_zod"]["def"]["type"];
readonly input?: Input;
}
export interface $ZodIssueTooBig<Input = unknown> extends $ZodIssueBase {
readonly code: "too_big";
readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
readonly maximum: number | bigint;
readonly inclusive?: boolean;
readonly exact?: boolean;
readonly input?: Input;
}
export interface $ZodIssueTooSmall<Input = unknown> extends $ZodIssueBase {
readonly code: "too_small";
readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
readonly minimum: number | bigint;
/** True if the allowable range includes the minimum */
readonly inclusive?: boolean;
/** True if the allowed value is fixed (e.g.` z.length(5)`), not a range (`z.minLength(5)`) */
readonly exact?: boolean;
readonly input?: Input;
}
export interface $ZodIssueInvalidStringFormat extends $ZodIssueBase {
readonly code: "invalid_format";
readonly format: $ZodStringFormats | (string & {});
readonly pattern?: string;
readonly input?: string;
}
export interface $ZodIssueNotMultipleOf<Input extends number | bigint = number | bigint> extends $ZodIssueBase {
readonly code: "not_multiple_of";
readonly divisor: number;
readonly input?: Input;
}
export interface $ZodIssueUnrecognizedKeys extends $ZodIssueBase {
readonly code: "unrecognized_keys";
readonly keys: string[];
readonly input?: Record<string, unknown>;
}
export interface $ZodIssueInvalidUnion extends $ZodIssueBase {
readonly code: "invalid_union";
readonly errors: $ZodIssue[][];
readonly input?: unknown;
readonly discriminator?: string | undefined;
}
export interface $ZodIssueInvalidKey<Input = unknown> extends $ZodIssueBase {
readonly code: "invalid_key";
readonly origin: "map" | "record";
readonly issues: $ZodIssue[];
readonly input?: Input;
}
export interface $ZodIssueInvalidElement<Input = unknown> extends $ZodIssueBase {
readonly code: "invalid_element";
readonly origin: "map" | "set";
readonly key: unknown;
readonly issues: $ZodIssue[];
readonly input?: Input;
}
export interface $ZodIssueInvalidValue<Input = unknown> extends $ZodIssueBase {
readonly code: "invalid_value";
readonly values: util.Primitive[];
readonly input?: Input;
}
export interface $ZodIssueCustom extends $ZodIssueBase {
readonly code: "custom";
readonly params?: Record<string, any> | undefined;
readonly input?: unknown;
}
////////////////////////////////////////////
//// first-party string formats ////
////////////////////////////////////////////
export interface $ZodIssueStringCommonFormats extends $ZodIssueInvalidStringFormat {
format: Exclude<$ZodStringFormats, "regex" | "jwt" | "starts_with" | "ends_with" | "includes">;
}
export interface $ZodIssueStringInvalidRegex extends $ZodIssueInvalidStringFormat {
format: "regex";
pattern: string;
}
export interface $ZodIssueStringInvalidJWT extends $ZodIssueInvalidStringFormat {
format: "jwt";
algorithm?: string;
}
export interface $ZodIssueStringStartsWith extends $ZodIssueInvalidStringFormat {
format: "starts_with";
prefix: string;
}
export interface $ZodIssueStringEndsWith extends $ZodIssueInvalidStringFormat {
format: "ends_with";
suffix: string;
}
export interface $ZodIssueStringIncludes extends $ZodIssueInvalidStringFormat {
format: "includes";
includes: string;
}
export type $ZodStringFormatIssues =
| $ZodIssueStringCommonFormats
| $ZodIssueStringInvalidRegex
| $ZodIssueStringInvalidJWT
| $ZodIssueStringStartsWith
| $ZodIssueStringEndsWith
| $ZodIssueStringIncludes;
////////////////////////
//// utils /////
////////////////////////
export type $ZodIssue =
| $ZodIssueInvalidType
| $ZodIssueTooBig
| $ZodIssueTooSmall
| $ZodIssueInvalidStringFormat
| $ZodIssueNotMultipleOf
| $ZodIssueUnrecognizedKeys
| $ZodIssueInvalidUnion
| $ZodIssueInvalidKey
| $ZodIssueInvalidElement
| $ZodIssueInvalidValue
| $ZodIssueCustom;
export type $ZodIssueCode = $ZodIssue["code"];
export type $ZodInternalIssue<T extends $ZodIssueBase = $ZodIssue> = T extends any ? RawIssue<T> : never;
type RawIssue<T extends $ZodIssueBase> = T extends any
? util.Flatten<
util.MakePartial<T, "message" | "path"> & {
/** The input data */
readonly input: unknown;
/** The schema or check that originated this issue. */
readonly inst?: $ZodType | $ZodCheck;
/** If `true`, Zod will continue executing checks/refinements after this issue. */
readonly continue?: boolean | undefined;
} & Record<string, unknown>
>
: never;
export type $ZodRawIssue<T extends $ZodIssueBase = $ZodIssue> = $ZodInternalIssue<T>;
export interface $ZodErrorMap<T extends $ZodIssueBase = $ZodIssue> {
// biome-ignore lint:
(issue: $ZodRawIssue<T>): { message: string } | string | undefined | null;
}
//////////////////////// ERROR CLASS ////////////////////////
// const ZOD_ERROR: symbol = Symbol.for("{{zod.error}}");
export interface $ZodError<T = unknown> extends Error {
type: T;
issues: $ZodIssue[];
_zod: {
output: T;
def: $ZodIssue[];
};
stack?: string;
name: string;
}
const initializer = (inst: $ZodError, def: $ZodIssue[]): void => {
inst.name = "$ZodError";
Object.defineProperty(inst, "_zod", {
value: inst._zod,
enumerable: false,
});
Object.defineProperty(inst, "issues", {
value: def,
enumerable: false,
});
inst.message = JSON.stringify(def, util.jsonStringifyReplacer, 2);
Object.defineProperty(inst, "toString", {
value: () => inst.message,
enumerable: false,
});
};
export const $ZodError: $constructor<$ZodError> = $constructor("$ZodError", initializer);
interface $ZodRealError<T = any> extends $ZodError<T> {}
export const $ZodRealError: $constructor<$ZodRealError> = $constructor("$ZodError", initializer, { Parent: Error });
/////////////////// ERROR UTILITIES ////////////////////////
// flatten
export type $ZodFlattenedError<T, U = string> = _FlattenedError<T, U>;
type _FlattenedError<T, U = string> = {
formErrors: U[];
fieldErrors: {
[P in keyof T]?: U[];
};
};
export function flattenError<T>(error: $ZodError<T>): _FlattenedError<T>;
export function flattenError<T, U>(error: $ZodError<T>, mapper?: (issue: $ZodIssue) => U): _FlattenedError<T, U>;
export function flattenError(error: $ZodError, mapper = (issue: $ZodIssue) => issue.message): any {
const fieldErrors: any = {};
const formErrors: any[] = [];
for (const sub of error.issues) {
if (sub.path.length > 0) {
fieldErrors[sub.path[0]!] = fieldErrors[sub.path[0]!] || [];
fieldErrors[sub.path[0]!].push(mapper(sub));
} else {
formErrors.push(mapper(sub));
}
}
return { formErrors, fieldErrors };
}
type _ZodFormattedError<T, U = string> = T extends [any, ...any[]]
? { [K in keyof T]?: $ZodFormattedError<T[K], U> }
: T extends any[]
? { [k: number]: $ZodFormattedError<T[number], U> }
: T extends object
? util.Flatten<{ [K in keyof T]?: $ZodFormattedError<T[K], U> }>
: any;
export type $ZodFormattedError<T, U = string> = {
_errors: U[];
} & util.Flatten<_ZodFormattedError<T, U>>;
export function formatError<T>(error: $ZodError<T>): $ZodFormattedError<T>;
export function formatError<T, U>(error: $ZodError<T>, mapper?: (issue: $ZodIssue) => U): $ZodFormattedError<T, U>;
export function formatError<T>(error: $ZodError, _mapper?: any) {
const mapper: (issue: $ZodIssue) => any =
_mapper ||
function (issue: $ZodIssue) {
return issue.message;
};
const fieldErrors: $ZodFormattedError<T> = { _errors: [] } as any;
const processError = (error: { issues: $ZodIssue[] }) => {
for (const issue of error.issues) {
if (issue.code === "invalid_union" && issue.errors.length) {
issue.errors.map((issues) => processError({ issues }));
} else if (issue.code === "invalid_key") {
processError({ issues: issue.issues });
} else if (issue.code === "invalid_element") {
processError({ issues: issue.issues });
} else if (issue.path.length === 0) {
(fieldErrors as any)._errors.push(mapper(issue));
} else {
let curr: any = fieldErrors;
let i = 0;
while (i < issue.path.length) {
const el = issue.path[i]!;
const terminal = i === issue.path.length - 1;
if (!terminal) {
curr[el] = curr[el] || { _errors: [] };
} else {
curr[el] = curr[el] || { _errors: [] };
curr[el]._errors.push(mapper(issue));
}
curr = curr[el];
i++;
}
}
}
};
processError(error);
return fieldErrors;
}
export type $ZodErrorTree<T, U = string> = T extends util.Primitive
? { errors: U[] }
: T extends [any, ...any[]]
? { errors: U[]; items?: { [K in keyof T]?: $ZodErrorTree<T[K], U> } }
: T extends any[]
? { errors: U[]; items?: Array<$ZodErrorTree<T[number], U>> }
: T extends object
? { errors: U[]; properties?: { [K in keyof T]?: $ZodErrorTree<T[K], U> } }
: { errors: U[] };
export function treeifyError<T>(error: $ZodError<T>): $ZodErrorTree<T>;
export function treeifyError<T, U>(error: $ZodError<T>, mapper?: (issue: $ZodIssue) => U): $ZodErrorTree<T, U>;
export function treeifyError<T>(error: $ZodError, _mapper?: any) {
const mapper: (issue: $ZodIssue) => any =
_mapper ||
function (issue: $ZodIssue) {
return issue.message;
};
const result: $ZodErrorTree<T> = { errors: [] } as any;
const processError = (error: { issues: $ZodIssue[] }, path: PropertyKey[] = []) => {
for (const issue of error.issues) {
if (issue.code === "invalid_union" && issue.errors.length) {
// regular union error
issue.errors.map((issues) => processError({ issues }, issue.path));
} else if (issue.code === "invalid_key") {
processError({ issues: issue.issues }, issue.path);
} else if (issue.code === "invalid_element") {
processError({ issues: issue.issues }, issue.path);
} else {
const fullpath = [...path, ...issue.path];
if (fullpath.length === 0) {
result.errors.push(mapper(issue));
continue;
}
let curr: any = result;
let i = 0;
while (i < fullpath.length) {
const el = fullpath[i]!;
const terminal = i === fullpath.length - 1;
if (typeof el === "string") {
curr.properties ??= {};
curr.properties[el] ??= { errors: [] };
curr = curr.properties[el];
} else {
curr.items ??= [];
curr.items[el] ??= { errors: [] };
curr = curr.items[el];
}
if (terminal) {
curr.errors.push(mapper(issue));
}
i++;
}
}
}
};
processError(error);
return result;
}
/** Format a ZodError as a human-readable string in the following form.
*
* From
*
* ```ts
* ZodError {
* issues: [
* {
* expected: 'string',
* code: 'invalid_type',
* path: [ 'username' ],
* message: 'Invalid input: expected string'
* },
* {
* expected: 'number',
* code: 'invalid_type',
* path: [ 'favoriteNumbers', 1 ],
* message: 'Invalid input: expected number'
* }
* ];
* }
* ```
*
* to
*
* ```
* username
* ✖ Expected number, received string at "username
* favoriteNumbers[0]
* ✖ Invalid input: expected number
* ```
*/
export function toDotPath(_path: readonly (string | number | symbol | StandardSchemaV1.PathSegment)[]): string {
const segs: string[] = [];
const path: PropertyKey[] = _path.map((seg: any) => (typeof seg === "object" ? seg.key : seg));
for (const seg of path) {
if (typeof seg === "number") segs.push(`[${seg}]`);
else if (typeof seg === "symbol") segs.push(`[${JSON.stringify(String(seg))}]`);
else if (/[^\w$]/.test(seg)) segs.push(`[${JSON.stringify(seg)}]`);
else {
if (segs.length) segs.push(".");
segs.push(seg);
}
}
return segs.join("");
}
export function prettifyError(error: StandardSchemaV1.FailureResult): string {
const lines: string[] = [];
// sort by path length
const issues = [...error.issues].sort((a, b) => (a.path ?? []).length - (b.path ?? []).length);
// Process each issue
for (const issue of issues) {
lines.push(`✖ ${issue.message}`);
if (issue.path?.length) lines.push(` → at ${toDotPath(issue.path)}`);
}
// Convert Map to formatted string
return lines.join("\n");
}