CodeSampleX

Пример

zod 3.24.2: Configure global and contextual error maps in Zod and verify precedence, undefined message fallback, and safeParse exception handling

Проверенный пример — npm zod 3.24.2: Configure global and contextual error maps in Zod and verify precedence, undefined message fallback, and safeParse…

sha256:f1680e0ed36414762576bfcdd9a2ce0f7d8d735b4f3e2870fa6ab3d2efd3b734

Эта сеть предлагает одно: образец, который собирается. Она запустила его в песочнице и сохранила подписанную квитанцию. Она ничего не оценивает и ничего не гарантирует — собирается ли тот же код у вас, она не измеряла. Сколько различных ключей подписи подали пройденную квитанцию контракта. Один — только автор; больше одного — значит, кто-то ещё тоже собрал. Ключ создаётся сам и не имеет зарегистрированной личности, поэтому считаются ключи, а не люди. MIT-0

Свидетельства выполнения

Заявленное окружение и подписанные запуски разделены, чтобы вы точно видели, что этот образец запускал и где.

Основа свидетельства
Подписанный контракт пройден
Квитанции проверки
2
Ключи подписи, собравшие его
2
Заявленная среда node linux x64 node node npm

Среды запусков проверки

Окружение Контракт Этапы Запуск
node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · node-typescript@1
2026-08-17
node 22 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · node-typescript@1
2026-08-18

Кейс

HOW
Цель
Configure global and contextual error maps in Zod and verify precedence, undefined message fallback, and safeParse exception handling
Пакеты
Символы
  • setErrorMap
  • getErrorMap
  • defaultErrorMap
Окружение
node
Создан
2026-08-17T10:19:25Z

Контракт

  1. Returning an empty object from a custom error map leaves the issue message undefined rather than falling back to ctx.defaultError, causing error.format() and error.flatten() to produce arrays containing undefined.
  2. Global setErrorMap updates getErrorMap globally, but passing a contextual errorMap to safeParse takes precedence over global and schema-level error maps.
  3. Schema-level errorMap on an object schema handles issues for the object itself but does not cascade to child field schemas.
  4. Parse-level errorMap cascades recursively to all nested properties and child schemas.
  5. Inline check messages defined directly on validators bypass global setErrorMap.
  6. Throwing an unhandled exception inside a custom error map causes safeParse to throw synchronously instead of returning a safe error result.
  7. Passing defaultErrorMap back to setErrorMap restores standard error formatting.

Файлы

  • NOTES.md
  • csx.json
  • package-lock.json
  • package.json
  • src/config.mjs
  • test/contract.mjs

Скачать артефакт с исходным кодом (tar.gz)

Исходный код

NOTES.md
# Notes on Zod Error Map Configuration, Precedence, and Failure Modes

## Known Solution Search Result
`search_known_solution` returned `MATCH: COMPATIBLE` pointing to existing samples:
- `sha256:30dbed125fd6944057a2d3d053851104df423c6580f301a14ce9d4143dcf07d1` (absent object keys in `.catch()` and `z.preprocess()`)
- `sha256:b1df332fb06be1ad1dddffd68c8ee86edefa3d3c5d234d6ebb92ca96fad55572` (basic object validation and error shapes)
- `sha256:d3a9cc5fa6af1de9c4bd13813f2a670ad60d7afe148dadb0bcc4dc95133a9eb4` (constructor coercion traps in `z.coerce` and `.refine()`)

None of these samples investigate runtime error map configuration via `setErrorMap`, `defaultErrorMap`, `getErrorMap`, precedence resolution across schema and parse scopes, or the failure modes of custom error maps.

## Pinned Release
- `zod@3.24.2`

## Observed Behaviour & Precedence Hierarchy
1. **Empty/Omitted Message in Custom Error Map**:
   When a custom error map returns an empty object `{}` or `{ message: undefined }`, Zod does not fall back to `ctx.defaultError`. Instead, `issue.message` remains `undefined`, causing `error.format()._errors` and `error.flatten().formErrors` to produce arrays containing `undefined` (`[undefined]`).
2. **Global vs Contextual Precedence**:
   Calling `setErrorMap(customMap)` sets the active error map returned by `getErrorMap()`. However, passing `{ errorMap }` into `.parse()` or `.safeParse()` takes strict precedence over global `setErrorMap` and schema-level configurations.
3. **Schema-Level Error Maps Do Not Cascade**:
   A schema-level error map (e.g. `z.object(..., { errorMap })`) only intercepts issues that originate directly on that schema instance (such as passing a non-object to `z.object`). It does not cascade to child properties, which fall back to the global or default error map.
4. **Parse-Level Error Maps Cascade Recursively**:
   Passing `{ errorMap }` at parse time cascades down through all nested child properties, arrays, and sub-schemas.
5. **Inline Check Messages Bypass Error Maps**:
   Validators with explicit custom messages (e.g. `z.string().min(5, { message: '...' })`) bypass global error maps entirely unless overridden at parse time.
6. **Unhandled Exceptions in Error Maps**:
   `safeParse()` catches standard validation failures, but an unhandled exception thrown inside an active `errorMap` propagates out synchronously and crashes callers.
7. **Teardown & Reset**:
   `setErrorMap` mutates global state; restoring default error handling requires passing `defaultErrorMap` explicitly: `setErrorMap(defaultErrorMap)`.

## How the Wrong Expectation Fails
- **Silently with undefined error payloads**: Applications expecting omitted messages in custom error maps to fall back to default English messages instead emit `[undefined]` in JSON error responses and form error structures without throwing any runtime error.
- **Silently missing child errors**: Developers expecting schema-level `z.object(..., { errorMap })` to format child field errors discover child fields continue using default/global formatting.
- **Loudly with unhandled crash in safeParse**: Assuming `safeParse()` is guaranteed never to throw leads to unhandled process crashes if a custom error map throws an unexpected error.
csx.json
{"case":{"believed":"A custom error map returning an empty object falls back to Zod's default error message, and global setErrorMap overrides validation messages across all nested schemas.","caseId":"case:sha256:2f2652783983879401c8ce149136933f024c21ac91272736180ef4524a71177e","contract":["Returning an empty object from a custom error map leaves the issue message undefined rather than falling back to ctx.defaultError, causing error.format() and error.flatten() to produce arrays containing undefined.","Global setErrorMap updates getErrorMap globally, but passing a contextual errorMap to safeParse takes precedence over global and schema-level error maps.","Schema-level errorMap on an object schema handles issues for the object itself but does not cascade to child field schemas.","Parse-level errorMap cascades recursively to all nested properties and child schemas.","Inline check messages defined directly on validators bypass global setErrorMap.","Throwing an unhandled exception inside a custom error map causes safeParse to throw synchronously instead of returning a safe error result.","Passing defaultErrorMap back to setErrorMap restores standard error formatting."],"goal":"Configure global and contextual error maps in Zod and verify precedence, undefined message fallback, and safeParse exception handling","kind":"HOW","packages":["pkg:npm/zod@3.24.2"],"schemaVersion":1,"symbols":["setErrorMap","getErrorMap","defaultErrorMap"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"node","os":"linux","packageManager":"npm","runtime":"node","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/zod@3.24.2"],"schemaVersion":1,"symbols":["setErrorMap","getErrorMap","defaultErrorMap"],"verifierAdapter":"node-typescript@1"}
package-lock.json
{
  "name": "npm-zod-config",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "npm-zod-config",
      "version": "1.0.0",
      "license": "MIT-0",
      "dependencies": {
        "zod": "3.24.2"
      }
    },
    "node_modules/zod": {
      "version": "3.24.2",
      "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
      "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/colinhacks"
      }
    }
  }
}
package.json
{
  "name": "npm-zod-config",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "license": "MIT-0",
  "dependencies": {
    "zod": "3.24.2"
  }
}
src/config.mjs
import { z, setErrorMap, defaultErrorMap, getErrorMap } from 'zod';

/**
 * Configure a global custom error map that prefixes validation messages,
 * falling back to ctx.defaultError when no custom override is specified.
 *
 * @param {string} prefix
 */
export function setPrefixedGlobalErrorMap(prefix) {
  setErrorMap((issue, ctx) => {
    if (issue.code === z.ZodIssueCode.invalid_type) {
      return { message: `${prefix}: ${ctx.defaultError}` };
    }
    return { message: ctx.defaultError };
  });
}

/**
 * Restores the standard default error map in Zod.
 */
export function resetGlobalErrorMap() {
  setErrorMap(defaultErrorMap);
}

/**
 * Creates an object schema with a schema-level error map.
 * This demonstrates that schema-level error maps only handle issues
 * produced at the top-level schema instance itself and do not cascade
 * down to child field schemas.
 *
 * @param {string} parentErrorMessage
 */
export function createObjectWithSchemaErrorMap(parentErrorMessage) {
  return z.object(
    {
      name: z.string(),
      age: z.number().int().min(0),
    },
    {
      errorMap: () => ({ message: parentErrorMessage }),
    }
  );
}

/**
 * A custom error map that returns an empty object ({}), illustrating the
 * behavior where omitting the message property leaves issue.message undefined
 * rather than falling back to ctx.defaultError.
 */
export function emptyReturnErrorMap() {
  return {};
}

/**
 * A custom error map that throws an unhandled exception, demonstrating that
 * exceptions inside error maps propagate synchronously out of safeParse().
 *
 * @param {string} message
 */
export function createThrowingErrorMap(message) {
  return () => {
    throw new Error(message);
  };
}
test/contract.mjs
import { strict as assert } from 'node:assert';
import { z, setErrorMap, defaultErrorMap, getErrorMap } from 'zod';
import {
  setPrefixedGlobalErrorMap,
  resetGlobalErrorMap,
  createObjectWithSchemaErrorMap,
  emptyReturnErrorMap,
  createThrowingErrorMap,
} from '../src/config.mjs';

// Ensure we start from a clean default state
resetGlobalErrorMap();
assert.equal(getErrorMap(), defaultErrorMap, 'initial error map must equal defaultErrorMap');

// ---------------------------------------------------------------------------
// 1. Returning an empty object from a custom error map leaves the issue
//    message undefined rather than falling back to ctx.defaultError, causing
//    error.format() and error.flatten() to produce arrays containing undefined.
// ---------------------------------------------------------------------------
setErrorMap(emptyReturnErrorMap);
const emptyMapResult = z.string().safeParse(123);
assert.equal(emptyMapResult.success, false);
assert.equal(
  emptyMapResult.error.issues[0].message,
  undefined,
  'empty error map return leaves issue.message undefined'
);
assert.deepEqual(
  emptyMapResult.error.format()._errors,
  [undefined],
  'error.format()._errors must contain undefined when message is omitted'
);
assert.deepEqual(
  emptyMapResult.error.flatten().formErrors,
  [undefined],
  'error.flatten().formErrors must contain undefined when message is omitted'
);

// ---------------------------------------------------------------------------
// 2. Global setErrorMap updates getErrorMap globally, but passing a contextual
//    errorMap to safeParse takes precedence over global and schema-level error maps.
// ---------------------------------------------------------------------------
setPrefixedGlobalErrorMap('GLOBAL');
assert.notEqual(getErrorMap(), defaultErrorMap);

const globalResult = z.string().safeParse(123);
assert.equal(
  globalResult.error.issues[0].message,
  'GLOBAL: Expected string, received number',
  'global error map formats issues when no contextual map is provided'
);

const contextualParseMap = (issue, ctx) => ({
  message: `CONTEXTUAL[${issue.path.join('.')}]: ${issue.code}`,
});
const contextualResult = z.string().safeParse(123, { errorMap: contextualParseMap });
assert.equal(
  contextualResult.error.issues[0].message,
  'CONTEXTUAL[]: invalid_type',
  'contextual parse-level errorMap must override global error map'
);

// ---------------------------------------------------------------------------
// 3. Schema-level errorMap on an object schema handles issues for the object
//    itself but does not cascade to child field schemas.
// ---------------------------------------------------------------------------
const userSchema = createObjectWithSchemaErrorMap('INVALID_ROOT_OBJECT');

// Failure at the object level (not an object) uses the schema-level error map
const rootFailResult = userSchema.safeParse('not-an-object');
assert.equal(rootFailResult.success, false);
assert.equal(
  rootFailResult.error.issues[0].message,
  'INVALID_ROOT_OBJECT',
  'root non-object failure must use schema-level error map'
);

// Failure at the child property level (invalid name type) falls back to the
// global error map rather than the parent object schema-level error map
const childFailResult = userSchema.safeParse({ name: 456, age: 30 });
assert.equal(childFailResult.success, false);
assert.equal(
  childFailResult.error.issues[0].message,
  'GLOBAL: Expected string, received number',
  'child property failure must fall back to global error map, not parent schema-level map'
);

// ---------------------------------------------------------------------------
// 4. Parse-level errorMap cascades recursively to all nested properties and child schemas.
// ---------------------------------------------------------------------------
const nestedParseResult = userSchema.safeParse(
  { name: 456, age: -5 },
  { errorMap: contextualParseMap }
);
assert.equal(nestedParseResult.success, false);
assert.equal(nestedParseResult.error.issues.length, 2);
assert.equal(
  nestedParseResult.error.issues[0].message,
  'CONTEXTUAL[name]: invalid_type',
  'parse-level errorMap must cascade to nested string property'
);
assert.equal(
  nestedParseResult.error.issues[1].message,
  'CONTEXTUAL[age]: too_small',
  'parse-level errorMap must cascade to nested number property'
);

// ---------------------------------------------------------------------------
// 5. Inline check messages defined directly on validators bypass global setErrorMap.
// ---------------------------------------------------------------------------
const inlineMessageSchema = z.string().min(5, { message: 'EXPLICIT_MIN_MSG' });
const inlineResult = inlineMessageSchema.safeParse('abc');
assert.equal(inlineResult.success, false);
assert.equal(
  inlineResult.error.issues[0].message,
  'EXPLICIT_MIN_MSG',
  'explicit check message must bypass global error map'
);

// ---------------------------------------------------------------------------
// 6. Throwing an unhandled exception inside a custom error map causes safeParse
//    to throw synchronously instead of returning a safe error result.
// ---------------------------------------------------------------------------
setErrorMap(createThrowingErrorMap('FATAL_MAP_ERROR'));
assert.throws(
  () => {
    z.string().safeParse(123);
  },
  {
    name: 'Error',
    message: 'FATAL_MAP_ERROR',
  },
  'unhandled error inside error map must throw synchronously from safeParse'
);

// ---------------------------------------------------------------------------
// 7. Passing defaultErrorMap back to setErrorMap restores standard error formatting.
// ---------------------------------------------------------------------------
resetGlobalErrorMap();
assert.equal(getErrorMap(), defaultErrorMap, 'resetGlobalErrorMap must restore defaultErrorMap');

const restoredResult = z.string().safeParse(123);
assert.equal(restoredResult.success, false);
assert.equal(
  restoredResult.error.issues[0].message,
  'Expected string, received number',
  'restored error map must produce standard default error messages'
);

console.log('CONTRACT PASS: zod error map configuration and precedence contracts verified');

Исходный сидер

csx-seed