Zod는 런타임에 데이터를 검증하면서 같은 스키마에서 TypeScript 타입을 추론하므로 구조를 한 번만 쓰면 됩니다. 큰 API 응답의 스키마를 손으로 쓰는 일은 번거롭습니다. 이 변환기는 실제 JSON 샘플로 z.object(...) 코드를 만들어 줍니다. 객체 타입마다 이름 붙은 스키마 하나, 목록에는 z.array(...), 마지막에는 z.infer로 만든 export type이 나옵니다.
출력은 모든 스키마가 그것을 쓰는 스키마보다 먼저 선언되는 순서라서 모듈에 붙여넣으면 바로 실행됩니다. 객체 배열은 스키마 하나로 병합되어, 일부 원소에 없는 키는 .optional(), null은 .nullable(), 섞인 원소 타입은 z.union이 됩니다.
샘플 하나로 API의 모든 것을 알 수는 없으므로 결과는 초안으로 보고 .email(), .datetime() 같은 검사는 직접 추가하세요. 샘플은 브라우저 탭 안에서만 처리되며 업로드되지 않습니다.
사용 방법
대표적인 JSON 샘플 붙여넣기
실제 응답을 쓰세요. 목록 안의 객체들이 서로 다른 키를 가진다면 선택 키가 드러나도록 여러 개를 넣습니다.
루트 이름 정하기
이름은 스키마(
RootSchema)와 타입(Root)의 PascalCase 기본형이 됩니다. 영문자·숫자가 아닌 문자는 단어 경계로 처리되고, 숫자로 시작하면Type접두사가 붙습니다.엄격한 객체와 정수 옵션 고르기
엄격한 객체를 켜면
z.object대신z.strictObject가 나옵니다. 정수에 .int() 사용은 같은 위치의 숫자가 모두 안전한 정수일 때만z.number().int()를 붙이며, 안전 범위 밖의 정수는z.number()로 남깁니다.변환하고 결과 읽기
잘못된 JSON이면 오류 메시지가 나옵니다. 그렇지 않으면
import { z } from "zod"한 줄, 중첩 스키마, 마지막으로 루트 스키마가 나옵니다.프로젝트에 복사
TypeScript 파일에 붙여넣거나
schema.ts로 내려받은 뒤 형식 검사와 enum으로 조여 갑니다.
작동 원리
JSON 값과 Zod의 대응
같은 위치에서 본 모든 값을 병합해 알맞은 Zod 빌더를 씁니다. 한 위치에 여러 종류의 값이 있었다면 빌더들을 z.union([...])으로 묶고, null이 있었다면 .nullable()을 붙입니다.
키에 .optional()이 붙는 것은 병합된 객체 중 일부에 그 키가 없을 때뿐이며, 한 배열의 원소들이 그 예입니다. 객체가 하나면 비교 대상이 없어 모든 키가 필수입니다.
| JSON 값 | Zod 스키마 | 비고 |
|---|---|---|
| "text" | z.string() | 형식을 감지하지 않아 날짜와 이메일도 일반 문자열입니다 |
| 42 | z.number() | 정수 옵션을 켜면 안전한 정수에만 z.number().int() |
| 9.5 | z.number() | 3과 9.5가 함께 나온 위치는 그냥 z.number() |
| true | z.boolean() | |
| 다른 타입과 함께 나온 null | T.nullable() | null만 나오면 z.null() |
| { ... } | z.object({ ... }) | 엄격 옵션이면 z.strictObject, 객체 타입마다 const 하나 |
| [1, "a"] | z.array(z.union([z.string(), z.number()])) | |
| [] | z.array(z.unknown()) | 빈 배열에는 정보가 없습니다 |
의존 순서와 이름
객체 타입마다 부모와 키로 이름을 지은 상수가 하나 만들어지며 user 키는 ApiResponseUserSchema가 됩니다. 상수는 하위 것이 먼저, 루트가 마지막에 출력됩니다. const는 초기화 전에 쓸 수 없으므로 이 순서가 모듈이 로드 시점에 죽지 않게 해 줍니다.
배열의 모든 원소는 키 이름의 단수형을 딴 상수 하나를 공유합니다(items는 ApiResponseItemSchema). display-name처럼 식별자가 아닌 키는 따옴표 붙은 속성 키로 남습니다. 루트는 RootSchema로 export되고 그 뒤에 export type Root = z.infer<typeof RootSchema>가 나옵니다.
const UserSchema = z.object({ id: z.number() });
export const RootSchema = z.object({
user: UserSchema,
});
export type Root = z.infer<typeof RootSchema>;Zod 버전
생성된 코드는 z.object, z.strictObject, z.array, z.union, z.string, z.number, z.boolean, z.null, z.unknown, .optional(), .nullable(), z.infer를 씁니다. 설치한 Zod 버전에서 지원하는지 확인하세요.
나중에 추가하는 검사는 버전마다 다릅니다. 예를 들어 Zod 3의 z.string().datetime()은 Zod 4에서 z.iso.datetime()입니다.
실제 예시
{"user":{"id":1,"name":"Ari"},"tags":["api","beta"]}import { z } from "zod";
const ApiResponseUserSchema = z.object({
id: z.number(),
name: z.string(),
});
export const ApiResponseSchema = z.object({
user: ApiResponseUserSchema,
tags: z.array(z.string()),
});
export type ApiResponse = z.infer<typeof ApiResponseSchema>;루트 이름 ApiResponse로 실행한 결과입니다. 중첩 스키마가 그것을 쓰는 루트보다 먼저 선언되므로 붙여넣은 그대로 실행됩니다.
{"items":[{"sku":"a","qty":2},{"sku":"b","qty":1,"note":"x"}]}import { z } from "zod";
const OrderItemSchema = z.object({
sku: z.string(),
qty: z.number(),
note: z.string().optional(),
});
export const OrderSchema = z.object({
items: z.array(OrderItemSchema),
});
export type Order = z.infer<typeof OrderSchema>;루트 이름 Order로 실행한 결과입니다. 두 원소가 OrderItemSchema를 공유하고, note는 한 원소에만 있어서 optional입니다.
{"users":[{"name":"Ari","age":30,"nick":null},{"name":"Bo","age":null}],"mixed":[1,"a"],"empty":[],"gone":null}import { z } from "zod";
const ProfileUserSchema = z.object({
name: z.string(),
age: z.number().nullable(),
nick: z.null().optional(),
});
export const ProfileSchema = z.object({
users: z.array(ProfileUserSchema),
mixed: z.array(z.union([z.string(), z.number()])),
empty: z.array(z.unknown()),
gone: z.null(),
});
export type Profile = z.infer<typeof ProfileSchema>;age는 숫자 또는 null, nick은 한 원소에서 null이고 다른 원소에는 없으며, mixed는 union, empty는 원소 타입이 없고, gone은 null만 나왔습니다.
{"id":7,"price":9.5,"qty":3}import { z } from "zod";
export const ItemSchema = z.strictObject({
id: z.number().int(),
price: z.number(),
qty: z.number().int(),
});
export type Item = z.infer<typeof ItemSchema>;엄격한 객체와 정수에 .int() 사용을 켠 결과입니다. 정수에는 .int()가 붙고, price는 소수부가 있어 z.number() 그대로입니다.
[{"id":1,"display-name":"A"},{"id":2,"display-name":"B"}]import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
"display-name": z.string(),
});
export const UsersSchema = z.array(UserSchema);
export type Users = z.infer<typeof UsersSchema>;루트 이름 Users로 실행한 결과입니다. 루트 배열은 UsersSchema를 z.array(UserSchema)로 export하고, 따옴표 붙은 display-name 키는 고칠 필요가 없습니다.
자주 하는 실수
optional 여부는 붙여넣은 객체 수에 달려 있습니다
병합된 객체 중 일부에 없는 키만 optional이 됩니다. 객체가 하나면 모두 필수이므로 배열의 레코드를 여러 개 붙여넣어야 어느 키가 선택인지 드러납니다.
문자열과 숫자는 일반형입니다
날짜, 이메일, UUID, 정수 ID도
z.string()이나z.number()로 나옵니다..datetime(),.email(),.uuid()를 붙이거나.int()옵션을 켜야 실제 검증이 됩니다.정수와 소수는 z.number()를 공유합니다
한 위치에 3과 9.5가 함께 있었다면
.int()옵션을 켜도 그냥z.number()입니다. 소수가.int()에서 실패하기 때문입니다. 안전 범위 밖의 정수도z.number()로 남겨 Zod 4의 안전한 정수 검사를 피합니다. JSON을 읽을 때 이미 반올림될 수 있으므로 큰 ID를 정확히 보존하려면 문자열을 쓰세요.엄격한 객체는 새 키를 거부합니다
z.strictObject는 API가 나중에 키를 추가하면 파싱이 실패합니다. 직접 관리하는 페이로드에만 쓰고 외부 응답에는 끄세요.enum과 다른 제약은 없습니다
샘플에 나오지 않은 필드나 열거형, 문자열 형식, 범위는 추론할 수 없습니다.
z.record나 판별 유니온도 만들지 않고 객체 변형을 선택적 필드가 있는 스키마 하나로 병합합니다. 필요한 제약은 직접 추가하세요.
자주 묻는 질문
JSON이 어딘가로 전송되나요?
아니요. 샘플은 브라우저 탭 안에서만 처리되며 업로드되지 않습니다.
Zod 3 코드인가요, Zod 4 코드인가요?
두 버전 모두에 있는 기본 빌더만 출력합니다. 직접 추가하는 날짜·이메일 검사 같은 refinement는 버전마다 조금 다릅니다.
붙여넣으면 바로 실행되나요?
선언 순서에 관해서는 그렇습니다. 중첩 스키마가 그것을 쓰는 스키마보다 먼저 출력되므로 초기화 전 사용 오류가 나지 않습니다.
필드를 optional이나 nullable로 만들려면 어떻게 하나요?
샘플에서 자동으로 처리됩니다. 일부 객체에 없는 키는 .optional(), null은 .nullable()이 붙습니다. 샘플에 그렇게 나오지 않은 필드는 직접 붙이거나 둘 다 필요하면 .nullish()를 쓰세요.
빈 배열은 무엇이 되나요?
z.array(z.unknown())입니다. 원소 타입을 추론할 근거가 없기 때문이며 z.unknown()을 실제 원소 스키마로 바꾸면 됩니다.
루트가 배열이면 무엇이 나오나요?
원소 스키마가 먼저 나오고, 이어서 export const UsersSchema = z.array(UserSchema)와 선택한 루트 이름의 타입이 나옵니다.
잘못된 JSON이나 특이한 키는 어떻게 되나요?
잘못된 JSON은 오류를 보여 주고 코드를 만들지 않습니다. display-name이나 1st처럼 식별자가 아닌 키는 따옴표 문자열로 쓰입니다.
마지막 업데이트:
