스키마 정의하기
데이터를 검증하려면 먼저 스키마를 정의해야 합니다. 스키마는 단순한 원시 값부터 복잡하게 중첩된 객체와 배열까지 다양한 타입을 표현합니다.
원시 타입
import * as z from "zod";
// primitive types
z.string();
z.number();
z.bigint();
z.boolean();
z.symbol();
z.undefined();
z.null();
강제 변환
입력 데이터를 적절한 타입으로 강제 변환하려면 z.coerce를 사용합니다.
z.coerce.string(); // String(input)
z.coerce.number(); // Number(input)
z.coerce.boolean(); // Boolean(input)
z.coerce.bigint(); // BigInt(input)
이 강제 변환 스키마는 입력 값을 적절한 타입으로 변환하려고 시도합니다.
const schema = z.coerce.string();
schema.parse("tuna"); // => "tuna"
schema.parse(42); // => "42"
schema.parse(true); // => "true"
schema.parse(null); // => "null"
이 강제 변환 스키마의 입력 타입은 기본적으로 unknown입니다. 더 구체적인 입력 타입을 지정하려면 제네릭 매개변수를 전달합니다.
const A = z.coerce.number();
type AInput = z.input<typeof A>; // => unknown
const B = z.coerce.number<number>();
type BInput = z.input<typeof B>; // => number
Zod의 강제 변환 작동 방식
Zod는 내장 생성자를 사용하여 모든 입력을 강제 변환합니다.
| Zod API | 강제 변환 |
|---|---|
z.coerce.string() | String(value) |
z.coerce.number() | Number(value) |
z.coerce.boolean() | Boolean(value) |
z.coerce.bigint() | BigInt(value) |
z.coerce.date() | new Date(value) |
z.coerce.boolean()을 사용한 불리언 강제 변환은 예상과 다르게 작동할 수 있습니다. 참으로 평가되는 값은 모두 true로, 거짓으로 평가되는 값은 모두 false로 강제 변환됩니다.
const schema = z.coerce.boolean(); // Boolean(input)
schema.parse("tuna"); // => true
schema.parse("true"); // => true
schema.parse("false"); // => true
schema.parse(1); // => true
schema.parse([]); // => true
schema.parse(0); // => false
schema.parse(""); // => false
schema.parse(undefined); // => false
schema.parse(null); // => false
강제 변환 로직을 완전히 제어하려면 z.transform() 또는 z.pipe()를 사용하는 방안을 고려하세요.
입력 타입 사용자 지정
모든 z.coerce 스키마의 입력 타입은 기본적으로 unknown입니다. 경우에 따라 입력 타입을 더 구체적으로 지정하는 편이 좋을 수 있습니다. 제네릭 매개변수로 입력 타입을 지정할 수 있습니다.
const regularCoerce = z.coerce.string();
type RegularInput = z.input<typeof regularCoerce>; // => unknown
type RegularOutput = z.output<typeof regularCoerce>; // => string
const customInput = z.coerce.string<string>();
type CustomInput = z.input<typeof customInput>; // => string
type CustomOutput = z.output<typeof customInput>; // => string
리터럴
리터럴 스키마는 "hello world"나 5 같은 리터럴 타입을 표현합니다.
const tuna = z.literal("tuna");
const twelve = z.literal(12);
const twobig = z.literal(2n);
const tru = z.literal(true);
JavaScript 리터럴 null과 undefined를 표현하려면 다음을 사용합니다.
z.null();
z.undefined();
z.void(); // equivalent to z.undefined()
여러 리터럴 값을 허용하려면 다음을 사용합니다.
const colors = z.literal(["red", "green", "blue"]);
colors.parse("green"); // ✅
colors.parse("yellow"); // ❌
리터럴 스키마에서 허용된 값의 집합을 추출하려면 다음을 사용합니다.
- Zod
- Zod Mini
colors.values; // => Set<"red" | "green" | "blue">
// no equivalent
문자열
Zod는 여러 내장 문자열 검증 및 변환 API를 제공합니다. 일반적인 문자열 검증을 수행하려면 다음을 사용합니다.
- Zod
- Zod Mini
z.string().max(5);
z.string().min(5);
z.string().length(5);
z.string().nonempty(); // alias for .min(1)
z.string().regex(/^[a-z]+$/);
z.string().startsWith("aaa");
z.string().endsWith("zzz");
z.string().includes("---");
z.string().uppercase();
z.string().lowercase();
z.string().check(z.maxLength(5));
z.string().check(z.minLength(5));
z.string().check(z.length(5));
z.string().check(z.minLength(1)); // alias for .nonempty()
z.string().check(z.regex(/^[a-z]+$/));
z.string().check(z.startsWith("aaa"));
z.string().check(z.endsWith("zzz"));
z.string().check(z.includes("---"));
z.string().check(z.uppercase());
z.string().check(z.lowercase());
길이는 UTF-16 코드 단위가 아니라 Unicode 코드 포인트로 측정합니다. 기본 다국어 평면 밖의 이모지는 하나로 계산하고, 결합 문자와 ZWJ 시퀀스는 여러 개로 계산합니다.
z.string().length(1).parse("😀"); // one code point, two UTF-16 units
z.string().length(2).parse("e\u0301"); // "é" — an e plus a combining acute
z.string().length(3).parse("🧑🍼"); // person + ZWJ + baby bottle
간단한 문자열 변환을 수행하려면 다음을 사용합니다.
- Zod
- Zod Mini
z.string().trim(); // trim whitespace
z.string().toLowerCase(); // toLowerCase
z.string().toUpperCase(); // toUpperCase
z.string().normalize(); // normalize unicode characters
z.string().check(z.trim()); // trim whitespace
z.string().check(z.toLowerCase()); // toLowerCase
z.string().check(z.toUpperCase()); // toUpperCase
z.string().check(z.normalize()); // normalize unicode characters
문자열 형식
일반적인 문자열 형식을 검증하려면 다음을 사용합니다.
z.email();
z.uuid();
z.url();
z.httpUrl(); // http or https URLs only
z.hostname();
z.e164(); // E.164 phone numbers
z.emoji(); // validates a single emoji character
z.base64();
z.base64url();
z.hex();
z.jwt();
z.nanoid();
z.cuid();
z.cuid2();
z.ulid();
z.ipv4();
z.ipv6();
z.mac();
z.cidrv4(); // ipv4 CIDR block
z.cidrv6(); // ipv6 CIDR block
z.creditCard(); // credit card number (Luhn checksum)
z.hash("sha256"); // or "sha1", "sha384", "sha512", "md5"
z.iso.date();
z.iso.time();
z.iso.datetime();
z.iso.duration();
이메일
이메일 주소를 검증하려면 다음을 사용합니다.
z.email();
기본적으로 Zod는 일반적인 문자를 포함한 보통의 이메일 주소를 검증하도록 설계된 비교적 엄격한 이메일 정규식을 사용합니다. Gmail이 적용하는 규칙과 대체로 비슷합니다. 이 정규식에 관한 자세한 내용은 이 글을 참고하세요.
/^(?!\.)(?!.*\.\.)([a-z0-9_'+\-\.]*)[a-z0-9_+-]@([a-z0-9][a-z0-9\-]*\.)+[a-z]{2,}$/i
이메일 검증 방식을 바꾸려면 pattern 매개변수에 원하는 정규식을 전달합니다.
z.email({ pattern: /your regex here/ });
Zod는 유용한 정규식 여러 개를 내보냅니다.
// Zod's default email regex
z.email();
z.email({ pattern: z.regexes.email }); // equivalent
// the regex used by browsers to validate input[type=email] fields
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email
z.email({ pattern: z.regexes.html5Email });
// the classic emailregex.com regex (RFC 5322)
z.email({ pattern: z.regexes.rfc5322Email });
// a loose regex that allows Unicode (good for intl emails)
z.email({ pattern: z.regexes.unicodeEmail });
UUID
UUID를 검증하려면 다음을 사용합니다.
z.uuid();
특정 UUID 버전을 지정하려면 다음을 사용합니다.
// supports "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8"
z.uuid({ version: "v4" });
// for convenience
z.uuidv4();
z.uuidv6();
z.uuidv7();
RFC 9562/4122 UUID 명세에서는 8번째 바이트의 처음 두 비트가 10이어야 합니다. 다른 UUID 유사 식별자는 이 제약 조건을 강제하지 않습니다. 모든 UUID 유사 식별자를 검증하려면 다음을 사용합니다.
z.guid();
URL
WHATWG 호환 URL을 검증하려면 다음을 사용합니다.
const schema = z.url();
schema.parse("https://example.com"); // ✅
schema.parse("http://localhost"); // ✅
schema.parse("mailto:noreply@zod.dev"); // ✅
보다시피 허용 범위가 상당히 넓습니다. 내부적으로 new URL() 생성자를 사용하여 입력을 검증합니다. 동작은 플랫폼과 런타임에 따라 다를 수 있지만, 각 JS 런타임이나 엔진에서 URI/URL을 검증하는 방식 가운데 비교적 엄격한 편입니다.
호스트 이름을 특정 정규식으로 검증하려면 다음을 사용합니다.
const schema = z.url({ hostname: /^example\.com$/ });
schema.parse("https://example.com"); // ✅
schema.parse("https://zombo.com"); // ❌
프로토콜을 특정 정규식으로 검증하려면 protocol 매개변수를 사용합니다.
const schema = z.url({ protocol: /^https$/ });
schema.parse("https://example.com"); // ✅
schema.parse("http://example.com"); // ❌
URL을 정규화하려면 normalize 플래그를 사용합니다. 입력 값은 new URL()이 반환하는 정규화된 URL로 덮어씁니다.
new URL("HTTP://ExAmPle.com:80/./a/../b?X=1#f oo").href
// => "http://example.com/b?X=1#f%20oo"
전화번호
E.164 형식의 전화번호를 검증하려면 다음을 사용합니다.
const phone = z.e164();
phone.parse("+15555555555"); // ✅
phone.parse("555-555-5555"); // ❌
이 스키마는 앞에 +가 붙고 0이 아닌 국가 코드가 있으며 숫자 부분이 총 7~15자리인 문자열을 검증합니다.
ISO 날짜 및 시간
앞에서 보았듯이 Zod 문자열 스키마에는 여러 날짜·시간 검증이 포함되어 있습니다. 정규식을 기반으로 하므로 완전한 날짜·시간 라이브러리만큼 엄격하지는 않지만 사용자 입력을 검증하기에는 매우 편리합니다.
z.iso.datetime() 메서드는 ISO 8601의 엄격한 하위 집합을 허용하며, 기본적으로 시간대 오프셋은 허용하지 않습니다.
const datetime = z.iso.datetime();
datetime.parse("2020-01-01T06:15:00Z"); // ✅
datetime.parse("2020-01-01T06:15:00.123Z"); // ✅
datetime.parse("2020-01-01T06:15:00.123456Z"); // ✅ (arbitrary precision)
datetime.parse("2020-01-01T06:15:00+02:00"); // ❌ (offsets not allowed)
datetime.parse("2020-01-01T06:15:00"); // ❌ (local not allowed)
시간대 오프셋을 허용하려면 다음을 사용합니다.
const datetime = z.iso.datetime({ offset: true });
// allows timezone offsets
datetime.parse("2020-01-01T06:15:00+02:00"); // ✅
// basic offsets not allowed
datetime.parse("2020-01-01T06:15:00+02"); // ❌
datetime.parse("2020-01-01T06:15:00+0200"); // ❌
// Z is still supported
datetime.parse("2020-01-01T06:15:00Z"); // ✅
한정되지 않은(시간대가 없는) 날짜 및 시간을 허용하려면 다음을 사용합니다.
const schema = z.iso.datetime({ local: true });
schema.parse("2020-01-01T06:15:01"); // ✅
schema.parse("2020-01-01T06:15"); // ✅ seconds optional
schema.parse("2020-01-01T06:15:00Z"); // ✅
schema.parse("2020-01-01T06:15Z"); // ❌ (a `Z` requires seconds)
허용할 시간 precision을 제한할 수 있습니다. 기본적으로 초는 필수이며 초 미만 정밀도에는 제한이 없습니다. RFC 3339에서는 Z 또는 오프셋이 있으면 반드시 초를 표기해야 하므로, local처럼 시간대가 없는 형식에서만 초를 생략할 수 있습니다.
const a = z.iso.datetime();
a.parse("2020-01-01T06:15Z"); // ❌ (seconds required)
a.parse("2020-01-01T06:15:00Z"); // ✅
a.parse("2020-01-01T06:15:00.123Z"); // ✅
const b = z.iso.datetime({ precision: -1 }); // minute precision (no seconds)
b.parse("2020-01-01T06:15Z"); // ✅
b.parse("2020-01-01T06:15:00Z"); // ❌
b.parse("2020-01-01T06:15:00.123Z"); // ❌
const c = z.iso.datetime({ precision: 0 }); // second precision only
c.parse("2020-01-01T06:15Z"); // ❌
c.parse("2020-01-01T06:15:00Z"); // ✅
c.parse("2020-01-01T06:15:00.123Z"); // ❌
const d = z.iso.datetime({ precision: 3 }); // millisecond precision only
d.parse("2020-01-01T06:15Z"); // ❌
d.parse("2020-01-01T06:15:00Z"); // ❌
d.parse("2020-01-01T06:15:00.123Z"); // ✅
분 단위 정밀도를 나머지 형식과 함께 허용하려면 두 스키마를 유니온으로 결합합니다. 하나의 precision 값으로는 둘 다 처리할 수 없습니다.
const mixed = z.union([z.iso.datetime(), z.iso.datetime({ precision: -1 })]);
mixed.parse("2020-01-01T06:15Z"); // ✅
mixed.parse("2020-01-01T06:15:00.123Z"); // ✅
mixed.parse("2020-01-01T06:15"); // ❌ (still qualified-only)
ISO 날짜
z.iso.date() 메서드는 YYYY-MM-DD 형식의 문자열을 검증합니다.
const date = z.iso.date();
date.parse("2020-01-01"); // ✅
date.parse("2020-1-1"); // ❌
date.parse("2020-01-32"); // ❌
ISO 시간
z.iso.time() 메서드는 HH:MM[:SS[.s+]] 형식의 문자열을 검증합니다. 기본적으로 초와 초 미만 소수 부분은 선택 사항입니다.
const time = z.iso.time();
time.parse("03:15"); // ✅
time.parse("03:15:00"); // ✅
time.parse("03:15:00.9999999"); // ✅ (arbitrary precision)
어떤 종류의 오프셋도 허용하지 않습니다.
time.parse("03:15:00Z"); // ❌ (no `Z` allowed)
time.parse("03:15:00+02:00"); // ❌ (no offsets allowed)
허용되는 소수 정밀도를 제한하려면 precision 매개변수를 사용합니다.
z.iso.time({ precision: -1 }); // HH:MM (minute precision)
z.iso.time({ precision: 0 }); // HH:MM:SS (second precision)
z.iso.time({ precision: 1 }); // HH:MM:SS.s (decisecond precision)
z.iso.time({ precision: 2 }); // HH:MM:SS.ss (centisecond precision)
z.iso.time({ precision: 3 }); // HH:MM:SS.sss (millisecond precision)
IP 주소
const ipv4 = z.ipv4();
ipv4.parse("192.168.0.0"); // ✅
const ipv6 = z.ipv6();
ipv6.parse("2001:db8:85a3::8a2e:370:7334"); // ✅
IP 블록(CIDR)
CIDR 표기법으로 지정된 IP 주소 범위를 검증합니다.
const cidrv4 = z.cidrv4();
cidrv4.parse("192.168.0.0/24"); // ✅
const cidrv6 = z.cidrv6();
cidrv6.parse("2001:db8::/32"); // ✅
MAC 주소
표준 48비트 MAC 주소 IEEE 802를 검증합니다.
const mac = z.mac();
mac.parse("00:1A:2B:3C:4D:5E"); // ✅
mac.parse("00-1a-2b-3c-4d-5e"); // ❌ colon-delimited by default
mac.parse("001A:2B3C:4D5E"); // ❌ standard formats only
mac.parse("00:1A:2b:3C:4d:5E"); // ❌ no mixed case
// custom delimiter
const dashMac = z.mac({ delimiter: "-" });
dashMac.parse("00-1A-2B-3C-4D-5E"); // ✅
신용카드 번호
카드 번호를 검증합니다. 유효한 Luhn 체크섬을 가진 12~19자리 숫자를 허용합니다. 발급사를 식별하지 않으므로 모든 체계의 카드를 허용합니다.
const card = z.creditCard();
card.parse("4111111111111111"); // ✅
card.parse("4111 1111 1111 1111"); // ✅ single spaces
card.parse("4111-1111-1111-1111"); // ✅ single hyphens
card.parse("4111 1111 1111 1111"); // ❌ no repeated separators
card.parse(" 4111111111111111"); // ❌ no surrounding whitespace
card.parse("4111.1111.1111.1111"); // ❌ spaces and hyphens only
card.parse("4111111111111112"); // ❌ failed checksum
JWT
JSON Web Token을 검증합니다.
z.jwt();
z.jwt({ alg: "HS256" });
해시
암호화 해시 값을 검증하려면 다음을 사용합니다.
z.hash("md5");
z.hash("sha1");
z.hash("sha256");
z.hash("sha384");
z.hash("sha512");
z.hash()는 관례에 따라 기본적으로 16진수 인코딩을 사용합니다. enc 매개변수로 다른 인코딩을 지정할 수 있습니다.
z.hash("sha256", { enc: "hex" }); // default
z.hash("sha256", { enc: "base64" }); // base64 encoding
z.hash("sha256", { enc: "base64url" }); // base64url encoding (no padding)
예상 길이와 패딩
| 알고리즘 / 인코딩 | "hex" | "base64" | "base64url" |
|---|---|---|---|
"md5" | 32 | 24 (22 + "==") | 22 |
"sha1" | 40 | 28 (27 + "=") | 27 |
"sha256" | 64 | 44 (43 + "=") | 43 |
"sha384" | 96 | 64 (패딩 없음) | 64 |
"sha512" | 128 | 88 (86 + "==") | 86 |
사용자 지정 형식
사용자 지정 문자열 형식을 정의하려면 다음을 사용합니다.
const coolId = z.stringFormat("cool-id", (val)=>{
// arbitrary validation here
return val.length === 100 && val.startsWith("cool-");
});
// a regex is also accepted
z.stringFormat("cool-id", /^cool-[a-z0-9]{95}$/);
이 스키마는 "invalid_format" 이슈를 생성합니다. 이 이슈는 "custom" 오류보다 더 구체적입니다. 후자는 세부 검증이나 z.custom()에서 생성됩니다.
myFormat.parse("invalid input!");
// ZodError: [
// {
// "code": "invalid_format",
// "format": "cool-id",
// "path": [],
// "message": "Invalid cool-id"
// }
// ]
템플릿 리터럴
zod@4.0에서 도입되었습니다.
템플릿 리터럴 스키마를 정의하려면 다음을 사용합니다.
const schema = z.templateLiteral([ "hello, ", z.string(), "!" ]);
// `hello, ${string}!`
z.templateLiteral API에는 문자열 리터럴(예: "hello")과 스키마를 원하는 만큼 전달할 수 있습니다. 추론된 타입이 string | number | bigint | boolean | null | undefined에 할당 가능한 스키마라면 무엇이든 사용할 수 있습니다.
z.templateLiteral([ "hi there" ]);
// `hi there`
z.templateLiteral([ "email: ", z.string() ]);
// `email: ${string}`
z.templateLiteral([ "high", z.literal(5) ]);
// `high5`
z.templateLiteral([ z.nullable(z.literal("grassy")) ]);
// `grassy` | `null`
z.templateLiteral([ z.number(), z.enum(["px", "em", "rem"]) ]);
// `${number}px` | `${number}em` | `${number}rem`
숫자
숫자를 검증하려면 z.number()를 사용합니다. 모든 유한한 숫자를 허용합니다.
const schema = z.number();
schema.parse(3.14); // ✅
schema.parse(NaN); // ❌
schema.parse(Infinity); // ❌
Zod는 여러 숫자 전용 검증을 구현합니다.
- Zod
- Zod Mini
z.number().gt(5);
z.number().gte(5); // alias .min(5)
z.number().lt(5);
z.number().lte(5); // alias .max(5)
z.number().positive(); // alias .gt(0)
z.number().nonnegative();
z.number().negative();
z.number().nonpositive();
z.number().multipleOf(5); // alias .step(5)
z.number().check(z.gt(5));
z.number().check(z.gte(5)); // alias .minimum(5)
z.number().check(z.lt(5));
z.number().check(z.lte(5)); // alias .maximum(5)
z.number().check(z.positive()); // alias .gt(0)
z.number().check(z.nonnegative());
z.number().check(z.negative());
z.number().check(z.nonpositive());
z.number().check(z.multipleOf(5)); // alias .step(5)
어떤 이유로든 NaN을 검증하려면 z.nan()을 사용합니다.
z.nan().parse(NaN); // ✅
z.nan().parse("anything else"); // ❌
정수
정수를 검증하려면 다음을 사용합니다.
z.int(); // restricts to safe integer range
z.int32(); // restrict to int32 range
BigInt
BigInt를 검증하려면 다음을 사용합니다.
z.bigint();
Zod에는 여러 bigint 전용 검증이 포함되어 있습니다.
- Zod
- Zod Mini
z.bigint().gt(5n);
z.bigint().gte(5n); // alias `.min(5n)`
z.bigint().lt(5n);
z.bigint().lte(5n); // alias `.max(5n)`
z.bigint().positive(); // alias `.gt(0n)`
z.bigint().nonnegative();
z.bigint().negative();
z.bigint().nonpositive();
z.bigint().multipleOf(5n); // alias `.step(5n)`
z.bigint().check(z.gt(5n));
z.bigint().check(z.gte(5n)); // alias `.minimum(5n)`
z.bigint().check(z.lt(5n));
z.bigint().check(z.lte(5n)); // alias `.maximum(5n)`
z.bigint().check(z.positive()); // alias `.gt(0n)`
z.bigint().check(z.nonnegative());
z.bigint().check(z.negative());
z.bigint().check(z.nonpositive());
z.bigint().check(z.multipleOf(5n)); // alias `.step(5n)`
불리언
불리언 값을 검증하려면 다음을 사용합니다.
z.boolean().parse(true); // => true
z.boolean().parse(false); // => false
날짜
z.date()를 사용하면 Date 인스턴스를 검증할 수 있습니다.
z.date().safeParse(new Date()); // success: true
z.date().safeParse("2022-01-12T06:15:00.000Z"); // success: false
오류 메시지를 사용자 지정하려면 다음을 사용합니다.
z.date({
error: issue => issue.input === undefined ? "Required" : "Invalid date"
});
Zod는 여러 날짜 전용 검증을 제공합니다.
- Zod
- Zod Mini
z.date().min(new Date("1900-01-01"), { error: "Too old!" });
z.date().max(new Date(), { error: "Too young!" });
z.date().check(z.minimum(new Date("1900-01-01"), { error: "Too old!" }));
z.date().check(z.maximum(new Date(), { error: "Too young!" }));
열거형
고정된 허용 문자열 값 집합을 기준으로 입력을 검증하려면 z.enum을 사용합니다.
const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
FishEnum.parse("Salmon"); // => "Salmon"
FishEnum.parse("Swordfish"); // => ❌
열거형과 유사한 객체 리터럴({ [key: string]: string | number })도 지원합니다.
const Fish = {
Salmon: 0,
Tuna: 1
} as const
const FishEnum = z.enum(Fish)
FishEnum.parse(Fish.Salmon); // => ✅
FishEnum.parse(0); // => ✅
FishEnum.parse(2); // => ❌
외부에서 선언한 TypeScript 열거형도 전달할 수 있습니다.
enum Fish {
Salmon = 0,
Tuna = 1
}
const FishEnum = z.enum(Fish);
FishEnum.parse(Fish.Salmon); // => ✅
FishEnum.parse(0); // => ✅
FishEnum.parse(2); // => ❌
enum Fish {
Salmon = "Salmon",
Tuna = "Tuna",
Trout = "Trout",
}
const FishEnum = z.enum(Fish);
.enum
스키마의 값을 열거형과 유사한 객체로 추출하려면 다음을 사용합니다.
- Zod
- Zod Mini
const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
FishEnum.enum;
// => { Salmon: "Salmon", Tuna: "Tuna", Trout: "Trout" }
const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
FishEnum.def.entries;
// => { Salmon: "Salmon", Tuna: "Tuna", Trout: "Trout" }
.exclude()
특정 값을 제외한 새 열거형 스키마를 만들려면 다음을 사용합니다.
- Zod
- Zod Mini
const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
const TunaOnly = FishEnum.exclude(["Salmon", "Trout"]);
// no equivalent
.extract()
특정 값만 추출한 새 열거형 스키마를 만들려면 다음을 사용합니다.
- Zod
- Zod Mini
const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
const SalmonAndTroutOnly = FishEnum.extract(["Salmon", "Trout"]);
// no equivalent
문자열 불리언 [#stringbool]
zod@4.0에서 도입되었습니다.
환경 변수를 파싱할 때처럼 불리언을 나타내는 특정 문자열을 일반 boolean 값으로 파싱해야 할 때가 있습니다. 이때 z.stringbool()을 사용합니다.
const strbool = z.stringbool();
strbool.parse("true") // => true
strbool.parse("1") // => true
strbool.parse("yes") // => true
strbool.parse("on") // => true
strbool.parse("y") // => true
strbool.parse("enabled") // => true
strbool.parse("false"); // => false
strbool.parse("0"); // => false
strbool.parse("no"); // => false
strbool.parse("off"); // => false
strbool.parse("n"); // => false
strbool.parse("disabled"); // => false
strbool.parse(/* anything else */); // ZodError<[{ code: "invalid_value" }]>
참과 거짓으로 평가할 값을 사용자 지정하려면 다음을 사용합니다.
// these are the defaults
z.stringbool({
truthy: ["true", "1", "yes", "on", "y", "enabled"],
falsy: ["false", "0", "no", "off", "n", "disabled"],
});
기본적으로 이 스키마는 대소문자를 구분하지 않습니다. 모든 입력은 truthy/falsy 값과 비교하기 전에 소문자로 변환됩니다. 대소문자를 구분하려면 다음을 사용합니다.
z.stringbool({
case: "sensitive"
});
선택적 타입
스키마를 선택적으로 만들어 undefined 입력을 허용하려면 다음을 사용합니다.
- Zod
- Zod Mini
z.optional(z.literal("yoda")); // or z.literal("yoda").optional()
z.optional(z.literal("yoda"));
이 메서드는 원본 스키마를 감싸는 ZodOptional 인스턴스를 반환합니다. 내부 스키마를 추출하려면 다음을 사용합니다.
- Zod
- Zod Mini
optionalYoda.unwrap(); // ZodLiteral<"yoda">
optionalYoda.def.innerType; // ZodMiniLiteral<"yoda">
정확한 선택적 타입
명시적인 undefined는 허용하지 않으면서 키가 없는 상태를 허용하려면 TypeScript의 exactOptionalPropertyTypes에 따라 다음을 사용합니다.
- Zod
- Zod Mini
z.exactOptional(z.string()); // or z.string().exactOptional()
z.exactOptional(z.string());
const User = z.object({ name: z.string().exactOptional() });
// { name?: string }
User.parse({}); // ✅
User.parse({ name: "yoda" }); // ✅
User.parse({ name: undefined }); // ❌
nullable 타입
스키마를 nullable로 만들어 null 입력을 허용하려면 다음을 사용합니다.
- Zod
- Zod Mini
z.nullable(z.literal("yoda")); // or z.literal("yoda").nullable()
const nullableYoda = z.nullable(z.literal("yoda"));
이 메서드는 원본 스키마를 감싸는 ZodNullable 인스턴스를 반환합니다. 내부 스키마를 추출하려면 다음을 사용합니다.
- Zod
- Zod Mini
nullableYoda.unwrap(); // ZodLiteral<"yoda">
nullableYoda.def.innerType; // ZodMiniLiteral<"yoda">
nullish 타입
스키마를 nullish로 만들어 선택적이면서 nullable로 만들려면 다음을 사용합니다.
- Zod
- Zod Mini
const nullishYoda = z.nullish(z.literal("yoda"));
const nullishYoda = z.nullish(z.literal("yoda"));
nullish 개념에 관한 자세한 내용은 TypeScript 매뉴얼을 참고하세요.
Unknown
Zod는 TypeScript의 타입 시스템을 일대일로 반영하는 것을 목표로 합니다. 따라서 다음 특수 타입을 표현하는 API를 제공합니다.
// allows any values
z.any(); // inferred type: `any`
z.unknown(); // inferred type: `unknown`
객체 속성으로 사용할 때 키는 필수이며 TypeScript의 { a: any }와 일치합니다.
z.object({ a: z.any() }).parse({}); // ❌
z.object({ a: z.any() }).parse({ a: undefined }); // ✅
z.object({ a: z.any().optional() }).parse({}); // ✅
Never
어떤 값도 검증을 통과하지 못합니다.
z.never(); // inferred type: `never`
객체
객체 타입을 정의하려면 다음을 사용합니다.
// all properties are required by default
const Person = z.object({
name: z.string(),
age: z.number(),
});
type Person = z.infer<typeof Person>;
// => { name: string; age: number; }
기본적으로 모든 속성은 필수입니다. 특정 속성을 선택적으로 만들려면 다음을 사용합니다.
- Zod
- Zod Mini
const Dog = z.object({
name: z.string(),
age: z.number().optional(),
});
Dog.parse({ name: "Yeller" }); // ✅
const Dog = z.object({
name: z.string(),
age: z.optional(z.number())
});
Dog.parse({ name: "Yeller" }); // ✅
기본적으로 인식되지 않는 키는 파싱 결과에서 제거됩니다.
Dog.parse({ name: "Yeller", extraKey: true });
// => { name: "Yeller" }
z.strictObject
알 수 없는 키가 발견되면 오류를 던지는 엄격한 스키마를 정의하려면 다음을 사용합니다.
const StrictDog = z.strictObject({
name: z.string(),
});
StrictDog.parse({ name: "Yeller", extraKey: true });
// ❌ throws
z.looseObject
알 수 없는 키를 그대로 통과시키는 느슨한 스키마를 정의하려면 다음을 사용합니다.
const LooseDog = z.looseObject({
name: z.string(),
});
LooseDog.parse({ name: "Yeller", extraKey: true });
// => { name: "Yeller", extraKey: true }
.catchall()
인식되지 않는 모든 키를 검증할 캐치올 스키마를 정의하려면 다음을 사용합니다.
- Zod
- Zod Mini
const DogWithStrings = z
.object({
name: z.string(),
age: z.number().optional(),
})
.catchall(z.string());
DogWithStrings.parse({ name: "Yeller", extraKey: "extraValue" }); // ✅
DogWithStrings.parse({ name: "Yeller", extraKey: 42 }); // ❌
const DogWithStrings = z.catchall(
z.object({
name: z.string(),
age: z.number().optional(),
}),
z.string()
);
DogWithStrings.parse({ name: "Yeller", extraKey: "extraValue" }); // ✅
DogWithStrings.parse({ name: "Yeller", extraKey: 42 }); // ❌
.shape
내부 스키마에 접근하려면 다음을 사용합니다.
- Zod
- Zod Mini
Dog.shape.name; // => string schema
Dog.shape.age; // => number schema
Dog.def.shape.name; // => string schema
Dog.def.shape.age; // => number schema
.keyof()
객체 스키마의 키로 ZodEnum 스키마를 만들려면 다음을 사용합니다.
- Zod
- Zod Mini
const keySchema = Dog.keyof();
// => ZodEnum<{ name: "name"; age: "age" }>
const keySchema = z.keyof(Dog);
// => ZodEnum<{ name: "name"; age: "age" }>
.extend()
객체 스키마에 필드를 추가하려면 다음을 사용합니다.
- Zod
- Zod Mini
const DogWithBreed = Dog.extend({
breed: z.string(),
});
const DogWithBreed = z.extend(Dog, {
breed: z.string(),
});
이 API로 기존 필드를 덮어쓸 수 있으므로 주의해야 합니다. 두 스키마에 같은 키가 있으면 B가 A를 덮어씁니다.
.safeExtend()
.safeExtend() 메서드는 .extend()와 비슷하게 작동하지만 기존 속성을 호환되지 않는 스키마로 덮어쓰지 못하게 합니다. 즉 .safeExtend() 결과의 추론 타입은 TypeScript 관점에서 원본을 extends합니다.
z.object({ a: z.string() }).safeExtend({ a: z.string().min(5) }); // ✅
z.object({ a: z.string() }).safeExtend({ a: z.any() }); // ✅
z.object({ a: z.string() }).safeExtend({ a: z.number() });
// ^ ❌ ZodNumber is not assignable
세부 검증이 포함된 스키마를 확장하려면 .safeExtend()를 사용합니다. 일반 .extend()를 세부 검증이 포함된 스키마에 사용하면 오류를 던집니다.
- Zod
- Zod Mini
const Base = z.object({
a: z.string(),
b: z.string()
}).refine(user => user.a === user.b);
// Extended inherits the refinements of Base
const Extended = Base.safeExtend({
a: z.string().min(10)
});
const Base = z.object({
a: z.string(),
b: z.string()
}).check(z.refine(user => user.a === user.b));
// Extended inherits the refinements of Base
const Extended = z.safeExtend(Base, {
a: z.string().min(10)
});
.pick()
Zod는 TypeScript의 내장 Pick 및 Omit 유틸리티 타입에서 착안하여 객체 스키마에서 특정 키를 선택하거나 제외하는 전용 API를 제공합니다.
다음 스키마에서 시작합니다.
const Recipe = z.object({
title: z.string(),
description: z.string().optional(),
ingredients: z.array(z.string()),
});
// { title: string; description?: string | undefined; ingredients: string[] }
특정 키를 선택하려면 다음을 사용합니다.
- Zod
- Zod Mini
const JustTheTitle = Recipe.pick({ title: true });
const JustTheTitle = z.pick(Recipe, { title: true });
.omit()
특정 키를 제외하려면 다음을 사용합니다.
- Zod
- Zod Mini
const RecipeNoId = Recipe.omit({ id: true });
const RecipeNoId = z.omit(Recipe, { id: true });
.partial()
편의를 위해 Zod는 TypeScript의 내장 유틸리티 타입 Partial에서 착안하여 일부 또는 모든 속성을 선택적으로 만드는 전용 API를 제공합니다.
모든 필드를 선택적으로 만들려면 다음을 사용합니다.
- Zod
- Zod Mini
const PartialRecipe = Recipe.partial();
// { title?: string | undefined; description?: string | undefined; ingredients?: string[] | undefined }
const PartialRecipe = z.partial(Recipe);
// { title?: string | undefined; description?: string | undefined; ingredients?: string[] | undefined }
특정 속성을 선택적으로 만들려면 다음을 사용합니다.
- Zod
- Zod Mini
const RecipeOptionalIngredients = Recipe.partial({
ingredients: true,
});
// { title: string; description?: string | undefined; ingredients?: string[] | undefined }
const RecipeOptionalIngredients = z.partial(Recipe, {
ingredients: true,
});
// { title: string; description?: string | undefined; ingredients?: string[] | undefined }
.exactPartial()
.partial()과 동일하지만 각 필드를 exactOptional()로 감싸며 optional()은 사용하지 않습니다.
- Zod
- Zod Mini
const PartialRecipe = Recipe.exactPartial();
// { title?: string; description?: string | undefined; ingredients?: string[] }
const PartialRecipe = z.exactPartial(Recipe);
// { title?: string; description?: string | undefined; ingredients?: string[] }
z.deepPartial() [#deep-partial]
.partial()은 최상위 형태에만 적용되지만 z.deepPartial()은 배열, 튜플, 유니온, 레코드, 래퍼를 거쳐 트리의 모든 객체에 재귀적으로 적용됩니다.
const Post = z.object({
title: z.string(),
author: z.object({ name: z.string(), email: z.string() }),
});
z.deepPartial(Post).parse({ author: {} }); // ✅
원본 스키마는 수정되지 않으며 결과도 여전히 ZodObject이므로 .shape와 .extend()가 계속 작동합니다. 판별자가 선택적이면 판별자 기반 조회를 사용할 수 없으므로 판별 유니온은 일반 z.union()으로 바뀝니다. .partial()과 마찬가지로 자체 세부 검증을 가진 객체에서는 오류를 던집니다.
.required()
Zod는 TypeScript의 Required 유틸리티 타입에서 착안하여 일부 또는 모든 속성을 필수로 만드는 API를 제공합니다.
모든 속성을 필수로 만들려면 다음을 사용합니다.
- Zod
- Zod Mini
const RequiredRecipe = Recipe.required();
// { title: string; description: string; ingredients: string[] }
const RequiredRecipe = z.required(Recipe);
// { title: string; description: string; ingredients: string[] }
특정 속성을 필수로 만들려면 다음을 사용합니다.
- Zod
- Zod Mini
const RecipeRequiredDescription = Recipe.required({description: true});
// { title: string; description: string; ingredients: string[] }
const RecipeRequiredDescription = z.required(Recipe, {description: true});
// { title: string; description: string; ingredients: string[] }
재귀 객체
자기 참조 타입을 정의하려면 키에 getter를 사용합니다. 그러면 JavaScript가 런타임에 순환 스키마를 해석할 수 있습니다.
const Category = z.object({
name: z.string(),
get subcategories(){
return z.array(Category)
}
});
type Category = z.infer<typeof Category>;
// { name: string; subcategories: Category[] }
Zod에서는 순환 입력이 별도 설정 없이 작동합니다. 번들 크기를 줄이기 위해 Zod Mini에서는 메모이저를 명시적으로 등록해야 합니다(코드 예제 참고).
- Zod
- Zod Mini
const input: any = { name: "root", subcategories: [] };
input.subcategories.push(input);
const result = Category.parse(input);
result.subcategories[0] === result; // true
// the output graph mirrors the input graph
result.subcategories[0].subcategories[0] === result; // true
// Zod Mini requires a memoizer, registered before schemas are defined
z.config({ memoizer: z.memoizer() });
const input: any = { name: "root", subcategories: [] };
input.subcategories.push(input);
const result = Category.parse(input);
result.subcategories[0] === result; // true
상호 재귀 타입도 표현할 수 있습니다.
const User = z.object({
email: z.email(),
get posts(){
return z.array(Post)
}
});
const Post = z.object({
title: z.string(),
get author(){
return User
}
});
모든 객체 API(.pick(), .omit(), .required(), .partial() 등)가 예상대로 작동합니다.
순환성 오류
TypeScript의 한계 때문에 재귀 타입 추론은 까다로울 수 있으며 특정 상황에서만 작동합니다. 조금 더 복잡한 타입에서는 다음과 같은 재귀 타입 오류가 발생할 수 있습니다.
const Activity = z.object({
name: z.string(),
get subactivities() {
// ^ ❌ 'subactivities' implicitly has return type 'any' because it does not
// have a return type annotation and is referenced directly or indirectly
// in one of its return expressions.ts(7023)
return z.nullable(z.array(Activity));
},
});
이 경우 문제가 되는 getter에 타입 표기를 추가하여 오류를 해결할 수 있습니다.
const Activity = z.object({
name: z.string(),
get subactivities(): z.ZodNullable<z.ZodArray<typeof Activity>> {
return z.nullable(z.array(Activity));
},
});
배열
배열 스키마를 정의하려면 다음을 사용합니다.
- Zod
- Zod Mini
const stringArray = z.array(z.string()); // or z.string().array()
const stringArray = z.array(z.string());
배열 요소의 내부 스키마에 접근하려면 다음을 사용합니다.
- Zod
- Zod Mini
stringArray.unwrap(); // => string schema
stringArray.def.element; // => string schema
Zod는 여러 배열 전용 검증을 구현합니다.
- Zod
- Zod Mini
z.array(z.string()).nonempty(); // must contain at least 1 item
z.array(z.string()).min(5); // must contain 5 or more items
z.array(z.string()).max(5); // must contain 5 or fewer items
z.array(z.string()).length(5); // must contain 5 items exactly
z.array(z.string()).check(z.minLength(1)); // alias for .nonempty()
z.array(z.string()).check(z.minLength(5)); // must contain 5 or more items
z.array(z.string()).check(z.maxLength(5)); // must contain 5 or fewer items
z.array(z.string()).check(z.length(5)); // must contain 5 items exactly
튜플
배열과 달리 튜플은 일반적으로 각 인덱스에 서로 다른 스키마를 지정하는 고정 길이 배열입니다.
const MyTuple = z.tuple([
z.string(),
z.number(),
z.boolean()
]);
type MyTuple = z.infer<typeof MyTuple>;
// [string, number, boolean]
가변 길이("나머지") 인수를 추가하려면 다음을 사용합니다.
const variadicTuple = z.tuple([z.string()], z.number());
// => [string, ...number[]];
모든 요소를 선택적으로 만들려면 다음을 사용합니다.
- Zod
- Zod Mini
const PartialTuple = z.tuple([z.string(), z.number()]).partial();
// => [(string | undefined)?, (number | undefined)?]
z.tuple([z.string()], z.number()).partial();
// => [(string | undefined)?, ...number[]]
const PartialTuple = z.partial(z.tuple([z.string(), z.number()]));
// => [(string | undefined)?, (number | undefined)?]
z.partial(z.tuple([z.string()], z.number()));
// => [(string | undefined)?, ...number[]]
유니온
유니온 타입(A | B)은 논리적 "OR"를 나타냅니다. Zod 유니온 스키마는 입력을 각 옵션과 순서대로 대조하며, 처음으로 검증에 성공한 값을 반환합니다.
const stringOrNumber = z.union([z.string(), z.number()]);
// string | number
stringOrNumber.parse("foo"); // passes
stringOrNumber.parse(14); // passes
내부 옵션 스키마를 추출하려면 다음을 사용합니다.
- Zod
- Zod Mini
stringOrNumber.options; // [ZodString, ZodNumber]
stringOrNumber.def.options; // [ZodString, ZodNumber]
배타적 유니온(XOR)
배타적 유니온(XOR)은 정확히 하나의 옵션만 일치해야 하는 유니온입니다. 옵션 중 하나라도 일치하면 성공하는 일반 유니온과 달리 z.xor()은 일치하는 옵션이 없거나 여러 옵션이 일치하면 실패합니다.
const schema = z.xor([z.string(), z.number()]);
schema.parse("hello"); // ✅ passes
schema.parse(42); // ✅ passes
schema.parse(true); // ❌ fails (zero matches)
옵션 사이의 상호 배타성을 보장하려 할 때 유용합니다.
// Validate that exactly ONE of these matches
const payment = z.xor([
z.object({ type: z.literal("card"), cardNumber: z.string() }),
z.object({ type: z.literal("bank"), accountNumber: z.string() }),
]);
payment.parse({ type: "card", cardNumber: "1234" }); // ✅ passes
입력이 여러 옵션과 일치하면 z.xor()은 실패합니다. 생성된 이슈에는 inclusive: false와 일치한 옵션의 인덱스를 나열하는 matches 배열이 포함됩니다.
const overlapping = z.xor([z.string(), z.any()]);
overlapping.parse("hello"); // ❌ fails (matches both string and any)
// ZodError: Invalid input: more than one option matched
z.object()은 알 수 없는 키를 거부하지 않고 제거하므로 객체 옵션은 예상보다 자주 겹칩니다. 아래에서 두 키를 모두 가진 입력은 두 옵션과 모두 일치합니다. 첫 번째 옵션은 version을 제거하고 두 번째 옵션은 유지합니다. 분기를 상호 배타적으로 만들려면 더 좁은 옵션에 z.strictObject() 또는 .strict()를 사용합니다.
const name = z.object({ name: z.string() });
const version = name.extend({ version: z.string() });
z.xor([name, version]).parse({ name: "zod", version: "4" }); // ❌ fails (matches both)
z.xor([name.strict(), version]).parse({ name: "zod", version: "4" }); // ✅ passes
판별 유니온
판별 유니온은 모든 옵션이 객체 스키마이면서 특정 키("판별자")를 공유하는 특수한 유니온입니다. TypeScript는 판별자 키의 값을 바탕으로 예상대로 타입 시그니처를 "좁힐" 수 있습니다.
type MyResult =
| { status: "success"; data: string }
| { status: "failed"; error: string };
function handleResult(result: MyResult){
if(result.status === "success"){
result.data; // string
} else {
result.error; // string
}
}
일반 z.union()으로도 이를 표현할 수 있습니다. 하지만 일반 유니온은 입력을 각 옵션과 순서대로 대조하여 처음 통과한 옵션을 반환하는 단순한 방식으로 작동합니다. 유니온이 크면 느려질 수 있습니다.
따라서 Zod는 판별자 키를 사용해 파싱 효율을 높이는 z.discriminatedUnion() API를 제공합니다.
const MyResult = z.discriminatedUnion("status", [
z.object({ status: z.literal("success"), data: z.string() }),
z.object({ status: z.literal("failed"), error: z.string() }),
]);
각 옵션은 판별자 속성(위 예제의 status)이 특정 리터럴 값 또는 값 집합에 대응하는 객체 스키마여야 합니다. 일반적으로 z.enum(), z.literal(), z.null(), z.undefined()를 사용합니다.
판별 유니온 중첩
고급 사용 사례에서는 판별 유니온을 중첩할 수 있습니다. Zod는 각 계층의 판별자를 활용할 수 있도록 최적의 파싱 전략을 결정합니다.
const BaseError = { status: z.literal("failed"), message: z.string() };
const MyErrors = z.discriminatedUnion("code", [
z.object({ ...BaseError, code: z.literal(400) }),
z.object({ ...BaseError, code: z.literal(401) }),
z.object({ ...BaseError, code: z.literal(500) }),
]);
const MyResult = z.discriminatedUnion("status", [
z.object({ status: z.literal("success"), data: z.string() }),
MyErrors
]);
교차 타입
교차 타입(A & B)은 논리적 "AND"를 나타냅니다.
const a = z.union([z.number(), z.string()]);
const b = z.union([z.number(), z.boolean()]);
const c = z.intersection(a, b);
type c = z.infer<typeof c>; // => number
두 객체 타입을 교차시킬 때 유용할 수 있습니다.
const Person = z.object({ name: z.string() });
type Person = z.infer<typeof Person>;
const Employee = z.object({ role: z.string() });
type Employee = z.infer<typeof Employee>;
const EmployedPerson = z.intersection(Person, Employee);
type EmployedPerson = z.infer<typeof EmployedPerson>;
// Person & Employee
레코드
레코드 스키마는 Record<string, string> 같은 타입을 검증할 때 사용합니다.
z.record
const IdCache = z.record(z.string(), z.string());
type IdCache = z.infer<typeof IdCache>; // Record<string, string>
IdCache.parse({
carlotta: "77d2586b-9e8e-4ecf-8b21-ea7e0530eadd",
jimmie: "77d2586b-9e8e-4ecf-8b21-ea7e0530eadd",
});
키 스키마에는 string | number | symbol에 할당할 수 있는 모든 Zod 스키마를 사용할 수 있습니다.
const Keys = z.union([z.string(), z.number(), z.symbol()]);
const AnyObject = z.record(Keys, z.unknown());
// Record<string | number | symbol, unknown>
열거형으로 정의된 키를 포함하는 객체 스키마를 만들려면 다음을 사용합니다.
const Keys = z.enum(["id", "name", "email"]);
const Person = z.record(Keys, z.string());
// { id: string; name: string; email: string }
Zod는 TypeScript와 거의 같은 방식으로 레코드 안의 숫자 키를 지원합니다. number 스키마를 레코드 키로 사용하면 키가 유효한 "숫자 문자열"인지 검증합니다. 추가 숫자 제약 조건(최솟값, 최댓값, 간격 등)도 함께 검증합니다.
const numberKeys = z.record(z.number(), z.string());
numberKeys.parse({
1: "one", // ✅
2: "two", // ✅
"1.5": "one", // ✅
"-3": "two", // ✅
abc: "one" // ❌
});
// further validation is also supported
const intKeys = z.record(z.int().step(1).min(0).max(10), z.string());
intKeys.parse({
0: "zero", // ✅
1: "one", // ✅
2: "two", // ✅
12: "twelve", // ❌
abc: "one" // ❌
});
없는 키를 대신할 수 있는 값 스키마인 .default(), .prefault(), .optional()은 z.object() 안에서와 마찬가지로 입력 타입에서 해당 키를 선택적으로 만듭니다.
const Person = z.record(z.enum(["id", "name"]), z.string().default(""));
type Input = z.input<typeof Person>; // { id?: string; name?: string }
type Output = z.output<typeof Person>; // { id: string; name: string }
z.partialRecord
부분 레코드 타입이 필요하면 z.partialRecord()를 사용합니다. 이 메서드는 Zod가 일반적으로 z.enum() 및 z.literal() 키 스키마에서 수행하는 특수한 완전성 검사를 건너뜁니다.
const Keys = z.enum(["id", "name", "email"]).or(z.never());
const Person = z.partialRecord(Keys, z.string());
// { id?: string; name?: string; email?: string }
z.looseRecord
기본적으로 z.record()는 키 스키마와 일치하지 않는 키에서 오류를 발생시킵니다. 일치하지 않는 키를 변경하지 않고 통과시키려면 z.looseRecord()를 사용합니다. 여러 패턴 속성을 모델링하기 위해 교차 타입과 결합할 때 특히 유용합니다.
const schema = z
.object({ name: z.string() })
.and(z.looseRecord(z.string().regex(/_phone$/), z.e164()));
type schema = z.infer<typeof schema>;
// => { name: string } & Record<string, string>
schema.parse({
name: "John",
home_phone: "+12345678900", // validated as phone number
work_phone: "+12345678900", // validated as phone number
});
Map
const StringNumberMap = z.map(z.string(), z.number());
type StringNumberMap = z.infer<typeof StringNumberMap>; // Map<string, number>
const myMap: StringNumberMap = new Map();
myMap.set("one", 1);
myMap.set("two", 2);
StringNumberMap.parse(myMap);
다음 유틸리티 메서드로 Map 스키마에 제약 조건을 더할 수 있습니다.
- Zod
- Zod Mini
z.map(z.string(), z.number()).nonempty(); // must contain at least 1 item
z.map(z.string(), z.number()).min(5); // must contain 5 or more items
z.map(z.string(), z.number()).max(5); // must contain 5 or fewer items
z.map(z.string(), z.number()).size(5); // must contain 5 items exactly
z.map(z.string(), z.number()).check(z.minSize(1)); // alias for .nonempty()
z.map(z.string(), z.number()).check(z.minSize(5)); // must contain 5 or more items
z.map(z.string(), z.number()).check(z.maxSize(5)); // must contain 5 or fewer items
z.map(z.string(), z.number()).check(z.size(5)); // must contain 5 items exactly
Set
const NumberSet = z.set(z.number());
type NumberSet = z.infer<typeof NumberSet>; // Set<number>
const mySet: NumberSet = new Set();
mySet.add(1);
mySet.add(2);
NumberSet.parse(mySet);
다음 유틸리티 메서드로 Set 스키마에 제약 조건을 더할 수 있습니다.
- Zod
- Zod Mini
z.set(z.string()).nonempty(); // must contain at least 1 item
z.set(z.string()).min(5); // must contain 5 or more items
z.set(z.string()).max(5); // must contain 5 or fewer items
z.set(z.string()).size(5); // must contain 5 items exactly
z.set(z.string()).check(z.minSize(1)); // alias for .nonempty()
z.set(z.string()).check(z.minSize(5)); // must contain 5 or more items
z.set(z.string()).check(z.maxSize(5)); // must contain 5 or fewer items
z.set(z.string()).check(z.size(5)); // must contain 5 items exactly
파일
File 인스턴스를 검증하려면 다음을 사용합니다.
- Zod
- Zod Mini
const fileSchema = z.file();
fileSchema.min(10_000); // minimum .size (bytes)
fileSchema.max(1_000_000); // maximum .size (bytes)
fileSchema.mime("image/png"); // MIME type
fileSchema.mime(["image/png", "image/jpeg"]); // multiple MIME types
const fileSchema = z.file();
fileSchema.check(z.minSize(10_000)); // minimum .size (bytes)
fileSchema.check(z.maxSize(1_000_000)); // maximum .size (bytes)
fileSchema.check(z.mime("image/png")); // MIME type
fileSchema.check(z.mime(["image/png", "image/jpeg"])); // multiple MIME types
Promise
z.promise() 문서 보기
const numberPromise = z.promise(z.number());
Promise 스키마에서는 "파싱" 방식이 조금 다릅니다. 검증은 두 단계로 진행됩니다.
- Zod는 입력이 Promise의 인스턴스인지, 즉
.then과.catch메서드가 있는 객체인지 동기적으로 확인합니다. - Zod는
.then을 사용하여 기존 Promise에 추가 검증 단계를 연결합니다. 검증 실패를 처리하려면 반환된 Promise에.catch를 사용해야 합니다.
numberPromise.parse("tuna");
// ZodError: Non-Promise type: string
numberPromise.parse(Promise.resolve("tuna"));
// => Promise<number>
const test = async () => {
await numberPromise.parse(Promise.resolve("tuna"));
// ZodError: Non-number type: string
await numberPromise.parse(Promise.resolve(3.14));
// => 3.14
};
Instanceof
z.instanceof를 사용하여 입력이 클래스의 인스턴스인지 확인할 수 있습니다. 서드파티 라이브러리가 내보낸 클래스를 기준으로 입력을 검증할 때 유용합니다.
class Test {
name: string;
}
const TestSchema = z.instanceof(Test);
TestSchema.parse(new Test()); // ✅
TestSchema.parse("whatever"); // ❌
내장 클래스에서도 작동합니다.
z.instanceof(RegExp);
z.instanceof(URL);
z.instanceof(Error);
속성
클래스 인스턴스의 특정 속성을 Zod 스키마로 검증하려면 다음을 사용합니다.
const blobSchema = z.instanceof(URL).check(
z.property("protocol", z.literal("https:" as string, "Only HTTPS allowed"))
);
blobSchema.parse(new URL("https://example.com")); // ✅
blobSchema.parse(new URL("http://example.com")); // ❌
z.property() API는 모든 데이터 타입에서 작동하지만 z.instanceof()와 함께 사용할 때 가장 유용합니다.
const blobSchema = z.string().check(
z.property("length", z.number().min(10))
);
blobSchema.parse("hello there!"); // ✅
blobSchema.parse("hello."); // ❌
객체 리터럴로 여러 속성 검사를 선언하려면 z.properties()를 사용합니다. 이 메서드는 .check()에 스프레드할 배열을 반환합니다.
const httpsUrl = z.instanceof(URL).check(
...z.properties({
protocol: z.literal("https:" as string),
hostname: z.string().regex(z.regexes.domain),
})
);
httpsUrl.parse(new URL("https://example.com")); // ✅
httpsUrl.parse(new URL("http://localhost")); // ❌ protocol
세부 검증
각 Zod 스키마에는 세부 검증 배열이 저장됩니다. 세부 검증은 Zod가 전용 API를 제공하지 않는 사용자 지정 검증을 수행하는 방법입니다.
.refine()
- Zod
- Zod Mini
const myString = z.string().refine((val) => val.length <= 255);
const myString = z.string().check(z.refine((val) => val.length <= 255));
error
오류 메시지를 사용자 지정하려면 다음을 사용합니다.
- Zod
- Zod Mini
const myString = z.string().refine((val) => val.length > 8, {
error: "Too short!"
});
const myString = z.string().check(
z.refine((val) => val.length > 8, { error: "Too short!" })
);
error 옵션에는 이슈를 인수로 받는 함수도 전달할 수 있습니다.
- Zod
- Zod Mini
const myString = z.string().refine((val) => val.length > 8, {
error: (iss) => `Too short: "${iss.input}"`
});
myString.parse("OH NO"); // ❌ Too short: "OH NO"
const myString = z.string().check(
z.refine((val) => val.length > 8, { error: (iss) => `Too short: "${iss.input}"` })
);
z.parse(myString, "OH NO"); // ❌ Too short: "OH NO"
abort
기본적으로 검사에서 발생한 검증 이슈는 계속 진행 가능한 것으로 간주됩니다. 즉 검사 중 하나에서 검증 오류가 발생해도 Zod는 모든 검사를 순서대로 실행합니다. Zod가 한 번에 가능한 한 많은 오류를 보여줄 수 있으므로 일반적으로 바람직한 동작입니다.
- Zod
- Zod Mini
const myString = z.string()
.refine((val) => val.length > 8, { error: "Too short!" })
.refine((val) => val === val.toLowerCase(), { error: "Must be lowercase" });
const result = myString.safeParse("OH NO");
result.error?.issues;
/* [
{ "code": "custom", "message": "Too short!" },
{ "code": "custom", "message": "Must be lowercase" }
] */
const myString = z.string().check(
z.refine((val) => val.length > 8, { error: "Too short!" }),
z.refine((val) => val === val.toLowerCase(), { error: "Must be lowercase" })
);
const result = z.safeParse(myString, "OH NO");
result.error?.issues;
/* [
{ "code": "custom", "message": "Too short!" },
{ "code": "custom", "message": "Must be lowercase" }
] */
특정 세부 검증을 계속 진행 불가로 표시하려면 abort 매개변수를 사용합니다. 검사가 실패하면 검증이 종료됩니다.
- Zod
- Zod Mini
const myString = z.string()
.refine((val) => val.length > 8, { error: "Too short!", abort: true })
.refine((val) => val === val.toLowerCase(), { error: "Must be lowercase", abort: true });
const result = myString.safeParse("OH NO");
result.error?.issues;
// => [{ "code": "custom", "message": "Too short!" }]
const myString = z.string().check(
z.refine((val) => val.length > 8, { error: "Too short!", abort: true }),
z.refine((val) => val === val.toLowerCase(), { error: "Must be lowercase", abort: true })
);
const result = z.safeParse(myString, "OH NO");
result.error?.issues;
// [ { "code": "custom", "message": "Too short!" }]
path
오류 경로를 사용자 지정하려면 path 매개변수를 사용합니다. 일반적으로 객체 스키마에서만 유용합니다.
- Zod
- Zod Mini
const passwordForm = z
.object({
password: z.string(),
confirm: z.string(),
})
.refine((data) => data.password === data.confirm, {
error: "Passwords don't match",
path: ["confirm"], // path of error
});
const passwordForm = z
.object({
password: z.string(),
confirm: z.string(),
})
.check(z.refine((data) => data.password === data.confirm, {
error: "Passwords don't match",
path: ["confirm"], // path of error
}));
그러면 해당 이슈의 path 매개변수가 설정됩니다.
- Zod
- Zod Mini
const result = passwordForm.safeParse({ password: "asdf", confirm: "qwer" });
result.error.issues;
/* [{
"code": "custom",
"path": [ "confirm" ],
"message": "Passwords don't match"
}] */
const result = z.safeParse(passwordForm, { password: "asdf", confirm: "qwer" });
result.error.issues;
/* [{
"code": "custom",
"path": [ "confirm" ],
"message": "Passwords don't match"
}] */
비동기 세부 검증을 정의하려면 async 함수를 전달합니다.
const userId = z.string().refine(async (id) => {
// verify that ID exists in database
return true;
});
when
참고 — 고급 사용자용 기능이며, 잘못 사용하면 세부 검증 내부의 오류가 포착되지 않을 가능성이 크게 높아질 수 있습니다.
기본적으로 계속 진행 불가 이슈가 이미 발생했다면 세부 검증을 실행하지 않습니다. Zod는 값을 세부 검증 함수에 전달하기 전에 해당 값의 타입 시그니처가 올바른지 먼저 확인합니다.
const schema = z.string().refine((val) => {
return val.length > 8
});
schema.parse(1234); // invalid_type: refinement won't be executed
경우에 따라 세부 검증이 실행되는 시점을 더 세밀하게 제어해야 합니다. 다음 "비밀번호 확인" 검사를 살펴보겠습니다.
- Zod
- Zod Mini
const schema = z
.object({
password: z.string().min(8),
confirmPassword: z.string(),
anotherField: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
schema.parse({
password: "asdf",
confirmPassword: "asdf",
anotherField: 1234 // ❌ this error will prevent the password check from running
});
const schema = z
.object({
password: z.string().check(z.minLength(8)),
confirmPassword: z.string(),
anotherField: z.string(),
})
.check(z.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
}));
schema.parse({
password: "asdf",
confirmPassword: "asdf",
anotherField: 1234 // ❌ this error will prevent the password check from running
});
검사가 anotherField에 의존하지 않는데도 anotherField의 오류 때문에 비밀번호 확인 검사가 실행되지 않습니다. 세부 검증의 실행 시점을 제어하려면 when 매개변수를 사용합니다.
- Zod
- Zod Mini
const baseSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
anotherField: z.string(),
});
const schema = baseSchema
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
// run if password & confirmPassword are valid
when(payload) { // [!code ++]
return baseSchema // [!code ++]
.pick({ password: true, confirmPassword: true }) // [!code ++]
.safeParse(payload.value).success; // [!code ++]
}, // [!code ++]
});
schema.parse({
password: "asdf",
confirmPassword: "asdf",
anotherField: 1234 // ❌ this error will not prevent the password check from running
});
const schema = z
.object({
password: z.string().min(8),
confirmPassword: z.string(),
anotherField: z.string(),
})
.check(z.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
when(payload) { // [!code ++]
// no issues with `password` or `confirmPassword` // [!code ++]
return payload.issues.every((iss) => { // [!code ++]
const firstPathEl = iss.path?.[0]; // [!code ++]
return firstPathEl !== "password" && firstPathEl !== "confirmPassword"; // [!code ++]
}); // [!code ++]
}, // [!code ++]
}));
schema.parse({
password: "asdf",
confirmPassword: "asdf",
anotherField: 1234 // ❌ this error will prevent the password check from running
});
.superRefine()
일반 .refine API는 "custom" 오류 코드가 있는 단일 이슈만 생성하지만, .superRefine()을 사용하면 Zod의 내부 이슈 타입 중 하나를 사용해 여러 이슈를 만들 수 있습니다.
- Zod
- Zod Mini
const UniqueStringArray = z.array(z.string()).superRefine((val, ctx) => {
if (val.length > 3) {
ctx.addIssue({
code: "too_big",
maximum: 3,
origin: "array",
inclusive: true,
message: "Too many items 😡",
input: val,
});
}
if (val.length !== new Set(val).size) {
ctx.addIssue({
code: "custom",
message: `No duplicates allowed.`,
input: val,
});
}
});
const UniqueStringArray = z.array(z.string()).check(
z.superRefine((val, ctx) => {
if (val.length > 3) {
ctx.addIssue({
code: "too_big",
maximum: 3,
origin: "array",
inclusive: true,
message: "Too many items 😡",
input: val,
});
}
if (val.length !== new Set(val).size) {
ctx.addIssue({
code: "custom",
message: `No duplicates allowed.`,
input: val,
});
}
})
);
.check()
예제 보기
.refine() API는 더 유연하고 장황한 .check() API를 간편하게 쓰기 위한 편의 문법입니다. 이 API를 사용하면 하나의 세부 검증에서 여러 이슈를 만들거나 생성되는 이슈 객체를 완전히 제어할 수 있습니다.
- Zod
- Zod Mini
const UniqueStringArray = z.array(z.string()).check((ctx) => {
if (ctx.value.length > 3) {
// full control of issue objects
ctx.issues.push({
code: "too_big",
maximum: 3,
origin: "array",
inclusive: true,
message: "Too many items 😡",
input: ctx.value
});
}
// create multiple issues in one refinement
if (ctx.value.length !== new Set(ctx.value).size) {
ctx.issues.push({
code: "custom",
message: `No duplicates allowed.`,
input: ctx.value,
continue: true // make this issue continuable (default: false)
});
}
});
const UniqueStringArray = z.array(z.string()).check((ctx) => {
// full control of issue objects
if (ctx.value.length > 3) {
ctx.issues.push({
code: "too_big",
maximum: 3,
origin: "array",
inclusive: true,
message: "Too many items 😡",
input: ctx.value
});
}
// create multiple issues in one refinement
if (ctx.value.length !== new Set(ctx.value).size) {
ctx.issues.push({
code: "custom",
message: `No duplicates allowed.`,
input: ctx.value,
continue: true // make this issue continuable (default: false)
});
}
});
코덱
zod@4.1에서 도입되었습니다. 자세한 내용은 코덱 전용 페이지를 참고하세요.
코덱은 두 스키마 사이의 양방향 변환을 구현하는 특수 스키마입니다.
const stringToDate = z.codec(
z.iso.datetime(), // input schema: ISO date string
z.date(), // output schema: Date object
{
decode: (isoString) => new Date(isoString), // ISO string → Date
encode: (date) => date.toISOString(), // Date → ISO string
}
);
일반 .parse() 호출은 순방향 변환을 수행하며 코덱의 decode 함수를 호출합니다.
stringToDate.parse("2024-01-15T10:30:00.000Z"); // => Date
대신 최상위 z.decode() 함수를 사용할 수도 있습니다. .parse()는 unknown 입력을 허용하지만, 그와 달리 z.decode()는 구체적인 타입의 입력(이 예제에서는 string)을 요구합니다.
z.decode(stringToDate, "2024-01-15T10:30:00.000Z"); // => Date
역방향 변환을 수행하려면 반대 함수인 z.encode()를 사용합니다.
z.encode(stringToDate, new Date("2024-01-15")); // => "2024-01-15T00:00:00.000Z"
입력 및 출력 스키마가 서로 바뀐 새 코덱을 만들려면 z.invertCodec()을 사용합니다.
const dateToString = z.invertCodec(stringToDate);
z.decode(dateToString, new Date("2024-01-15")); // => string
z.encode(dateToString, "2024-01-15T00:00:00.000Z"); // => Date
자세한 내용은 코덱 전용 페이지를 참고하세요. 이 페이지에는 자주 쓰는 코덱 구현이 있으며 프로젝트에 그대로 복사해 사용할 수 있습니다.
stringToNumberstringToIntstringToBigIntnumberToBigIntisoDatetimeToDateepochSecondsToDateepochMillisToDatejsonCodecutf8ToBytesbytesToUtf8base64ToBytesbase64urlToByteshexToBytesstringToURLstringToHttpURLuriComponentstringToBoolean
파이프
스키마를 "파이프"로 연결할 수 있습니다. 파이프는 주로 변환과 함께 사용할 때 유용합니다.
- Zod
- Zod Mini
const stringToLength = z.string().pipe(z.transform(val => val.length));
stringToLength.parse("hello"); // => 5
const stringToLength = z.pipe(z.string(), z.transform(val => val.length));
z.parse(stringToLength, "hello"); // => 5
z.input() 및 z.output() [#input-output]
이들은 타입 수준 z.input<T> / z.output<T>에 대응하는 런타임 API입니다. 스키마의 모든 파이프를 입력 쪽 또는 출력 쪽으로 바꿉니다. .in / .out으로는 접근할 수 없는 객체, 레코드 또는 Map 안에 중첩된 코덱에 접근할 때 유용합니다.
const Event = z.object({ name: z.string(), at: stringToDate });
z.input(Event).parse({ name: "launch", at: "2024-01-01T00:00:00Z" }); // ✅
z.output(Event).parse({ name: "launch", at: new Date() }); // ✅
실제 입력과 출력 양쪽을 모두 갖는 스키마는 코덱뿐입니다. 단방향 변환에 z.output()을 사용하면 아무것도 검증하지 않는 변환을 반환하고, z.input()을 z.preprocess()에 사용하면 전처리기가 값을 전달하는 스키마를 반환합니다.
래퍼에 저장된 값은 자신이 속한 쪽에서만 유지됩니다. 따라서 내부에 코덱이 있으면 z.input()은 .default() 또는 .catch()를 제거하고 z.output()은 .prefault()를 제거합니다.
변환
참고 — 양방향 변환에는 코덱을 사용합니다.
변환은 단방향으로 데이터를 바꾸는 특수 스키마입니다. 입력을 검증하는 대신 모든 값을 받아 특정 변환을 수행합니다. 변환을 정의하려면 다음을 사용합니다.
- Zod
- Zod Mini
const castToString = z.transform((val) => String(val));
castToString.parse("asdf"); // => "asdf"
castToString.parse(123); // => "123"
castToString.parse(true); // => "true"
const castToString = z.transform((val) => String(val));
z.parse(castToString, "asdf"); // => "asdf"
z.parse(castToString, 123); // => "123"
z.parse(castToString, true); // => "true"
변환 안에서 검증 로직을 수행하려면 ctx를 사용합니다. 검증 이슈를 보고하려면 새 이슈를 ctx.issues에 추가합니다. 이는 .check() API와 유사합니다.
const coercedInt = z.transform((val, ctx) => {
try {
const parsed = Number.parseInt(String(val));
return parsed;
} catch (e) {
ctx.issues.push({
code: "custom",
message: "Not a number",
input: val,
});
// this is a special constant with type `never`
// returning it lets you exit the transform without impacting the inferred return type
return z.NEVER;
}
});
변환은 일반적으로 파이프와 함께 사용합니다. 이 조합은 초기 검증을 수행한 다음 파싱된 데이터를 다른 형태로 변환할 때 유용합니다.
- Zod
- Zod Mini
const stringToLength = z.string().pipe(z.transform(val => val.length));
stringToLength.parse("hello"); // => 5
const stringToLength = z.pipe(z.string(), z.transform(val => val.length));
z.parse(stringToLength, "hello"); // => 5
.transform()
스키마를 변환에 연결하는 패턴이 자주 쓰이므로 Zod는 편의 메서드 .transform()을 제공합니다.
- Zod
- Zod Mini
const stringToLength = z.string().transform(val => val.length);
// no equivalent
비동기 변환도 지원합니다.
- Zod
- Zod Mini
const idToUser = z
.string()
.transform(async (id) => {
// fetch user from database
return db.getUserById(id);
});
const user = await idToUser.parseAsync("abc123");
const idToUser = z.pipe(
z.string(),
z.transform(async (id) => {
// fetch user from database
return db.getUserById(id);
}));
const user = await idToUser.parse("abc123");
.preprocess()
변환 결과를 다른 스키마에 연결하는 패턴도 자주 쓰이므로 Zod는 편의 함수 z.preprocess()를 제공합니다.
const coercedInt = z.preprocess((val) => {
if (typeof val === "string") {
return Number.parseInt(val);
}
return val;
}, z.int());
전처리기는 임의의 입력을 처리해야 하므로 z.preprocess() 스키마의 입력 타입은 기본적으로 unknown입니다. 입력 타입을 좁히려면 전처리기의 매개변수에 직접 타입을 표기합니다.
const trimmed = z.preprocess(
(val: string | null | undefined) => val?.trim() ?? "",
z.string()
);
type Input = z.input<typeof trimmed>; // string | null | undefined
type Output = z.output<typeof trimmed>; // string
react-hook-form처럼 폼 값 타입을 z.input<>에서 파생하는 라이브러리와 통합할 때 유용합니다.
기본값
스키마의 기본값을 설정하려면 다음을 사용합니다.
- Zod
- Zod Mini
const defaultTuna = z.string().default("tuna");
defaultTuna.parse(undefined); // => "tuna"
const defaultTuna = z._default(z.string(), "tuna");
defaultTuna.parse(undefined); // => "tuna"
기본값을 생성할 때마다 실행할 함수를 대신 전달할 수도 있습니다.
- Zod
- Zod Mini
const randomDefault = z.number().default(Math.random);
randomDefault.parse(undefined); // => 0.4413456736055323
randomDefault.parse(undefined); // => 0.1871840107401901
randomDefault.parse(undefined); // => 0.7223408162401552
const randomDefault = z._default(z.number(), Math.random);
z.parse(randomDefault, undefined); // => 0.4413456736055323
z.parse(randomDefault, undefined); // => 0.1871840107401901
z.parse(randomDefault, undefined); // => 0.7223408162401552
파싱 전 기본값
Zod에서 기본값을 설정하면 파싱을 즉시 종료합니다. 입력이 undefined이면 기본값을 바로 반환하므로, 기본값은 스키마의 출력 타입에 할당할 수 있어야 합니다.
const schema = z.string().transform(val => val.length).default(0);
schema.parse(undefined); // => 0
때로는 prefault("파싱 전 기본값") 값을 정의하는 것이 유용합니다. 입력이 undefined이면 prefault 값을 대신 파싱합니다. 파싱 과정은 중간에 종료되지 않으므로 prefault 값은 스키마의 입력 타입에 할당할 수 있어야 합니다.
const schema = z.string().transform(val => val.length).prefault("tuna");
schema.parse(undefined); // => 4
입력을 변경하는 세부 검증에 값을 통과시킬 때도 유용합니다.
const a = z.string().trim().toUpperCase().prefault(" tuna ");
a.parse(undefined); // => "TUNA"
const b = z.string().trim().toUpperCase().default(" tuna ");
b.parse(undefined); // => " tuna "
Catch
검증 오류가 발생할 때 반환할 대체 값을 정의하려면 .catch()를 사용합니다.
- Zod
- Zod Mini
const numberWithCatch = z.number().catch(42);
numberWithCatch.parse(5); // => 5
numberWithCatch.parse("tuna"); // => 42
const numberWithCatch = z.catch(z.number(), 42);
numberWithCatch.parse(5); // => 5
numberWithCatch.parse("tuna"); // => 42
대체 값을 생성할 때마다 실행할 함수를 대신 전달할 수도 있습니다.
- Zod
- Zod Mini
const numberWithRandomCatch = z.number().catch((ctx) => {
ctx.error; // the caught ZodError
return Math.random();
});
numberWithRandomCatch.parse("sup"); // => 0.4413456736055323
numberWithRandomCatch.parse("sup"); // => 0.1871840107401901
numberWithRandomCatch.parse("sup"); // => 0.7223408162401552
const numberWithRandomCatch = z.catch(z.number(), (ctx) => {
ctx.value; // the input value
ctx.issues; // the caught validation issue
return Math.random();
});
z.parse(numberWithRandomCatch, "sup"); // => 0.4413456736055323
z.parse(numberWithRandomCatch, "sup"); // => 0.1871840107401901
z.parse(numberWithRandomCatch, "sup"); // => 0.7223408162401552
브랜드 타입
TypeScript는 구조적 타입 시스템을 사용하므로 구조가 같은 두 타입을 동일하게 취급합니다.
type Cat = { name: string };
type Dog = { name: string };
const pluto: Dog = { name: "pluto" };
const simba: Cat = pluto; // works fine
경우에 따라 TypeScript 안에서 명목적 타이핑을 모방해야 할 수 있습니다. 브랜드 타입("불투명 타입"이라고도 함)으로 이를 구현할 수 있습니다.
const Cat = z.object({ name: z.string() }).brand<"Cat">();
const Dog = z.object({ name: z.string() }).brand<"Dog">();
type Cat = z.infer<typeof Cat>; // { name: string } & z.$brand<"Cat">
type Dog = z.infer<typeof Dog>; // { name: string } & z.$brand<"Dog">
const pluto = Dog.parse({ name: "pluto" });
const simba: Cat = pluto; // ❌ not allowed
내부적으로는 스키마의 추론 타입에 "브랜드"를 연결하는 방식으로 작동합니다.
const Cat = z.object({ name: z.string() }).brand<"Cat">();
type Cat = z.output<typeof Cat>; // { name: string } & z.$brand<"Cat">
이 브랜드를 사용하면 브랜드가 없는 일반 데이터 구조는 추론 타입에 할당할 수 없습니다. 스키마로 데이터를 파싱해야 브랜드가 적용된 데이터를 얻을 수 있습니다.
브랜드 타입은
.parse의 런타임 결과에 영향을 주지 않으며 정적 타입 검사에만 사용됩니다.
기본적으로 출력 타입에만 브랜드가 적용됩니다.
const USD = z.string().brand<"USD">();
type USDOutput = z.output<typeof USD>; // string & z.$brand<"USD">
type USDInput = z.input<typeof USD>; // string
적용 방향을 바꾸려면 .brand()에 두 번째 제네릭을 전달합니다.
// requires Zod 4.2+
z.string().brand<"Cat", "out">(); // output is branded (default)
z.string().brand<"Cat", "in">(); // input is branded
z.string().brand<"Cat", "inout">(); // both are branded
읽기 전용
스키마를 읽기 전용으로 표시하려면 다음을 사용합니다.
- Zod
- Zod Mini
const ReadonlyUser = z.object({ name: z.string() }).readonly();
type ReadonlyUser = z.infer<typeof ReadonlyUser>;
// Readonly<{ name: string }>
const ReadonlyUser = z.readonly(z.object({ name: z.string() }));
type ReadonlyUser = z.infer<typeof ReadonlyUser>;
// Readonly<{ name: string }>
추론 타입이 readonly로 표시됩니다. TypeScript에서는 객체, 배열, 튜플, Set, Map에만 영향을 준다는 점에 유의하세요.
- Zod
- Zod Mini
z.object({ name: z.string() }).readonly(); // { readonly name: string }
z.array(z.string()).readonly(); // readonly string[]
z.tuple([z.string(), z.number()]).readonly(); // readonly [string, number]
z.map(z.string(), z.date()).readonly(); // ReadonlyMap<string, Date>
z.set(z.string()).readonly(); // ReadonlySet<string>
z.readonly(z.object({ name: z.string() })); // { readonly name: string }
z.readonly(z.array(z.string())); // readonly string[]
z.readonly(z.tuple([z.string(), z.number()])); // readonly [string, number]
z.readonly(z.map(z.string(), z.date())); // ReadonlyMap<string, Date>
z.readonly(z.set(z.string())); // ReadonlySet<string>
입력을 일반적인 방식으로 파싱한 뒤 수정할 수 없도록 결과를 Object.freeze()로 동결합니다.
- Zod
- Zod Mini
const result = ReadonlyUser.parse({ name: "fido" });
result.name = "simba"; // throws TypeError
const result = z.parse(ReadonlyUser, { name: "fido" });
result.name = "simba"; // throws TypeError
JSON
JSON으로 인코딩할 수 있는 모든 값을 검증하려면 다음을 사용합니다.
const jsonSchema = z.json();
이는 다음 유니온 스키마를 반환하는 편의 API입니다.
const jsonSchema = z.lazy(() => {
return z.union([
z.string(params),
z.number(),
z.boolean(),
z.null(),
z.array(jsonSchema),
z.record(z.string(), jsonSchema)
]);
});
함수
Zod는 입력과 출력을 검증하는 함수를 정의할 수 있도록 z.function() 유틸리티를 제공합니다. 이를 사용하면 검증 코드와 비즈니스 로직이 섞이지 않습니다.
const MyFunction = z.function({
input: [z.string()], // parameters (must be an array or a ZodTuple)
output: z.number() // return type
});
type MyFunction = z.infer<typeof MyFunction>;
// (input: string) => number
함수 스키마의 .implement() 메서드는 구현 함수를 받아 입력과 출력을 자동으로 검증하는 함수로 감싸서 반환합니다.
const computeTrimmedLength = MyFunction.implement((input) => {
// TypeScript knows input is a string!
return input.trim().length;
});
computeTrimmedLength("sandwich"); // => 8
computeTrimmedLength(" asdf "); // => 4
입력이 유효하지 않으면 이 함수는 ZodError를 던집니다.
computeTrimmedLength(42); // throws ZodError
입력 검증만 필요하다면 output 필드를 생략할 수 있습니다.
const MyFunction = z.function({
input: [z.string()], // parameters (must be an array or a ZodTuple)
});
const computeTrimmedLength = MyFunction.implement((input) => input.trim.length);
비동기 함수를 만들려면 .implementAsync() 메서드를 사용합니다.
const computeTrimmedLengthAsync = MyFunction.implementAsync(
async (input) => input.trim().length
);
computeTrimmedLengthAsync("sandwich"); // => Promise<8>
사용자 지정
z.custom()을 사용하면 모든 TypeScript 타입에 대한 Zod 스키마를 만들 수 있습니다. 서드파티 라이브러리의 타입이나 내장 스키마가 다루지 않는 다른 타입을 검증할 때 유용합니다. 클래스 인스턴스에는 z.instanceof()를, 템플릿 리터럴 타입에는 z.templateLiteral()을 권장합니다.
import { Decimal } from "decimal.js";
const decimalSchema = z.custom<Decimal>((val) => Decimal.isDecimal(val));
decimalSchema.parse(new Decimal("1.5")); // passes
decimalSchema.parse("1.5"); // throws
검증 함수를 제공하지 않으면 Zod는 모든 값을 허용합니다. 위험할 수 있습니다.
z.custom<{ arg: string }>(); // performs no validation
두 번째 인수를 전달하여 오류 메시지와 기타 옵션을 사용자 지정할 수 있습니다. 이 매개변수는 .refine의 params 매개변수와 같은 방식으로 작동합니다.
z.custom<...>((val) => ..., "custom error message");
Apply
Zod의 메서드 체인에 외부 함수를 통합하려면 .apply()를 사용합니다.
- Zod
- Zod Mini
function setCommonNumberChecks<T extends z.ZodNumber>(schema: T) {
return schema
.min(0)
.max(100);
}
const schema = z.number()
.apply(setCommonNumberChecks)
.nullable();
schema.parse(0); // => 0
schema.parse(-1); // ❌ throws
schema.parse(101); // ❌ throws
schema.parse(null); // => null
function setCommonNumberChecks<T extends z.ZodMiniNumber>(schema: T) {
return schema.check(
z.minimum(0), z.maximum(100)
);
}
const schema = z.nullable(
z.number().apply(setCommonNumberChecks)
);
z.parse(schema, 0); // => 0
z.parse(schema, -1); // ❌ throws
z.parse(schema, 101); // ❌ throws
z.parse(schema, null); // => null
추가 인수는 함수에 그대로 전달됩니다.
function withDefault<T extends z.ZodType>(schema: T, value: z.output<T>) {
return schema.nullish().transform((val) => val ?? value);
}
const schema = z.string().apply(withDefault, "anonymous");
schema.parse(undefined); // => "anonymous"
schema.parse("sandwich"); // => "sandwich"