UNPKG

zod

Version:

TypeScript-first schema declaration and validation library with static type inference

268 lines (267 loc) • 10.9 kB
import { $constructor } from "./core.js"; import * as util from "./util.js"; /* Computing the message eagerly is expensive (pretty-printed JSON of all * issues), so defer it until first read. The accessor functions and * descriptors are shared across instances to keep error construction * cheap; the computed message is cached on the internals object. The * setter preserves plain assignment semantics for consumers that * overwrite `message`. */ function _getMessage() { const internals = this._zod; internals.message ?? (internals.message = JSON.stringify(internals.def, util.jsonStringifyReplacer, 2)); return internals.message; } function _setMessage(value) { this._zod.message = value; } const _messageDesc = { get: _getMessage, set: _setMessage, enumerable: true, configurable: true, }; const _issuesDesc = { value: undefined, enumerable: false }; /* Prototypes that already carry the lazy `toString`. Seeded with the * intrinsics so that `init` on a foreign object — it accepts any object — * can never install an accessor onto a prototype we do not own. */ const _installedToString = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]); const initializer = (inst, def) => { inst.name = "$ZodError"; // `_zod` is already non-enumerable: $constructor's init defined it with this same descriptor _issuesDesc.value = def; Object.defineProperty(inst, "issues", _issuesDesc); // Clear the shared slot; a retained `value` pins the last error's issues. _issuesDesc.value = undefined; Object.defineProperty(inst, "message", _messageDesc); /* `toString` lives as a non-enumerable lazy getter on the shared * prototype; on first access it caches a per-instance closure so * detached usage still works. */ const proto = Object.getPrototypeOf(inst); if (!_installedToString.has(proto)) { _installedToString.add(proto); Object.defineProperty(proto, "toString", { configurable: true, enumerable: false, get() { const value = () => this.message; Object.defineProperty(this, "toString", { value, configurable: true, writable: true }); return value; }, set(value) { Object.defineProperty(this, "toString", { value, configurable: true, writable: true }); }, }); } }; export const $ZodError = $constructor("$ZodError", initializer); export const $ZodRealError = $constructor("$ZodError", initializer, undefined, { Parent: Error, }); /** Get-or-create `obj[key]` as an own data property. A path segment naming an inherited member * ("toString", "constructor") would otherwise read through to the prototype, and assigning * "__proto__" would hit the setter instead of creating a key. */ function node(obj, key, make) { if (!Object.prototype.hasOwnProperty.call(obj, key)) { if (key === "__proto__") { Object.defineProperty(obj, key, { value: make(), writable: true, enumerable: true, configurable: true }); } else { obj[key] = make(); } } return obj[key]; } export function flattenError(error, mapper = (issue) => issue.message) { const fieldErrors = {}; const formErrors = []; for (const sub of error.issues) { if (sub.path.length > 0) { node(fieldErrors, sub.path[0], () => []).push(mapper(sub)); } else { formErrors.push(mapper(sub)); } } return { formErrors, fieldErrors }; } export function formatError(error, mapper = (issue) => issue.message) { const fieldErrors = { _errors: [] }; const processError = (error, path = []) => { for (const issue of error.issues) { if (issue.code === "invalid_union" && issue.errors.length) { issue.errors.map((issues) => processError({ issues }, [...path, ...issue.path])); } else if (issue.code === "invalid_key") { processError({ issues: issue.issues }, [...path, ...issue.path]); } else if (issue.code === "invalid_element") { processError({ issues: issue.issues }, [...path, ...issue.path]); } else { const fullpath = [...path, ...issue.path]; if (fullpath.length === 0) { fieldErrors._errors.push(mapper(issue)); } else { let curr = fieldErrors; let i = 0; while (i < fullpath.length) { const el = fullpath[i]; const terminal = i === fullpath.length - 1; // `_errors` is reserved by this legacy format, so merge a matching path segment into the current node instead of treating its array as a child. if (el === "_errors") { if (terminal) curr._errors.push(mapper(issue)); i++; continue; } // A path element may collide with an inherited property name such as // "__proto__" or "constructor". Truthiness checks read the prototype // (so no node is created, then ._errors.push throws), and bracket // assignment of "__proto__" hits the setter instead of creating an // own key. Guard the read with hasOwnProperty and create the node // with defineProperty so any path element becomes a real own key. if (!Object.prototype.hasOwnProperty.call(curr, el)) { Object.defineProperty(curr, el, { value: { _errors: [] }, enumerable: true, writable: true, configurable: true, }); } const node = curr[el]; if (terminal) { node._errors.push(mapper(issue)); } curr = node; i++; } } } } }; processError(error); return fieldErrors; } export function treeifyError(error, mapper = (issue) => issue.message) { const result = { errors: [] }; const processError = (error, path = []) => { var _a; for (const issue of error.issues) { if (issue.code === "invalid_union" && issue.errors.length) { // regular union error issue.errors.map((issues) => processError({ issues }, [...path, ...issue.path])); } else if (issue.code === "invalid_key") { processError({ issues: issue.issues }, [...path, ...issue.path]); } else if (issue.code === "invalid_element") { processError({ issues: issue.issues }, [...path, ...issue.path]); } else { const fullpath = [...path, ...issue.path]; if (fullpath.length === 0) { result.errors.push(mapper(issue)); continue; } let curr = 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 may collide with an inherited property name ("__proto__", // "constructor", ...); ??= reads the prototype so the node is never // created and curr.errors.push throws. Guard with hasOwnProperty and // create the node with defineProperty so "__proto__" becomes a real // own key rather than invoking the prototype setter. if (!Object.prototype.hasOwnProperty.call(curr.properties, el)) { Object.defineProperty(curr.properties, el, { value: { errors: [] }, enumerable: true, writable: true, configurable: true, }); } curr = curr.properties[el]; } else { curr.items ?? (curr.items = []); (_a = curr.items)[el] ?? (_a[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) { const segs = []; const path = _path.map((seg) => (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) { const lines = []; // 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"); }