zod
Version:
TypeScript-first schema declaration and validation library with static type inference
268 lines (267 loc) • 10.9 kB
JavaScript
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");
}