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 exception…

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