Tools mentioned in this article
Open the browser-based tool while you read and try the workflow immediately.
‘샘플에서 추론’만으로는 부족한 경우가 있다
이 사이트에는 이미 JSON to Zod 변환 이 있습니다. API 응답 같은 JSON을 붙여 넣으면 값에서 Zod 스키마를 추론해 주는 도구입니다.
다만 여기에는 원리적인 한계가 있습니다. 값 샘플만으로는 그 필드가 필수인지 알 수 없습니다. {"nickname": "alice"} 를 봐도 nickname이 항상 존재하는지, 이번에 우연히 들어 있었는지는 판단할 수 없습니다. 같은 이유로 "[email protected]" 이라는 값만으로 이메일 형식이어야 한다고 정할 수도 없습니다.
JSON Schema에는 그 정보가 처음부터 적혀 있습니다.
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"email": { "type": "string", "format": "email" },
"nickname": { "type": "string" }
},
"required": ["id", "email"]
}

JSON Schema ⇄ Zod 변환 은 이 정보를 버리지 않고 Zod로 옮기는 도구입니다. 반대로 코드에 이미 작성해 둔 Zod 스키마에서 JSON Schema를 뽑아낼 수도 있습니다. OpenAPI의 components.schemas 는 JSON Schema이므로 그대로 붙여 넣어 쓸 수 있습니다.
가장 큰 함정: required와 .optional()은 기본값이 반대
두 사양은 아무것도 쓰지 않았을 때의 의미가 정반대입니다.
| 필수를 나타내는 방법 | 아무것도 쓰지 않으면 | |
|---|---|---|
| JSON Schema | required 배열에 이름을 나열 | 선택(optional) |
| Zod | .optional() 을 붙이지 않음 | 필수 |
JSON Schema는 ‘명시적으로 필수라고 하지 않으면 선택’, Zod는 ‘명시적으로 선택이라고 하지 않으면 필수’입니다. 여기를 뒤집으면 필수여야 할 필드가 전부 optional이 된 타입이 만들어집니다.
변환기는 이 차이를 흡수합니다. JSON Schema→Zod 방향에서는 required 에 없는 프로퍼티에만 .optional() 을 붙입니다.
const required: string[] = Array.isArray(schema.required) ? schema.required : [];
const lines = Object.entries(properties).map(([key, value]) => {
let field = schemaToZod(value, depth + 1, warnings);
if (!required.includes(key)) field += '.optional()';
return `${indent}${formatKey(key)}: ${field},`;
});
반대 방향에서는 .optional() 이 없는 프로퍼티를 모아 required 배열을 만듭니다. 위 스키마를 변환하면 이렇게 됩니다.
import { z } from 'zod';
export const schema = z.object({
id: z.number().int(),
email: z.string().email(),
nickname: z.string().optional(),
});
export type Schema = z.infer<typeof schema>;
z.infer 타입 별칭도 함께 출력하므로, 검증과 타입 정의가 같은 곳에서 생깁니다.
제약 조건 대응표
| JSON Schema | Zod |
|---|---|
"type": "integer" | z.number().int() |
"format": "email" | z.string().email() |
"format": "date-time" | z.string().datetime() |
"minLength": 3 | z.string().min(3) |
"minimum": 0 | z.number().min(0) |
"enum": ["a","b"](모두 문자열) | z.enum(["a", "b"]) |
"enum"(다른 타입 포함) | z.union([z.literal(...), ...]) |
"type": ["string","null"] | z.string().nullable() |
"additionalProperties": false | .strict() |
oneOf / anyOf | z.union([...]) |
enum 만 분기가 필요합니다. Zod의 z.enum() 은 문자열 전용이라 숫자나 null이 섞인 enum은 그대로 넘길 수 없어, 리터럴의 union으로 떨어뜨립니다.
Zod→JSON Schema는 코드를 실행하지 않는다
반대 방향에는 구현상의 제약이 있습니다. zod-to-json-schema 같은 기존 라이브러리는 런타임의 Zod 객체를 받아 변환합니다. 그러나 이 사이트의 도구는 모두 브라우저 안에서 완결되는 방침이라, 붙여 넣은 코드를 eval 할 수는 없습니다.
그래서 Zod 코드를 텍스트로 해석합니다. z.string().min(1).optional() 같은 식을 호출 이름(string), 인자, 메서드 체인(min(1), optional())으로 분해합니다.
까다로운 부분은 괄호와 문자열 리터럴입니다. 단순히 ) 를 찾으면 중첩에서 무너지고, z.literal('a,b') 처럼 콤마를 포함한 문자열이 있으면 필드 분할도 실패합니다. 그래서 대응하는 괄호를 찾는 처리에서는 따옴표 안인지를 추적합니다.
const findClosing = (source: string, openIndex: number): number => {
let depth = 0;
let quote: string | null = null;
for (let i = openIndex; i < source.length; i += 1) {
const char = source[i];
if (quote) {
if (char === '\\') i += 1; // 이스케이프된 문자를 건너뜀
else if (char === quote) quote = null;
continue;
}
if (char === '"' || char === "'" || char === '`') { quote = char; continue; }
if (char === '(' || char === '[' || char === '{') depth += 1;
else if (char === ')' || char === ']' || char === '}') {
depth -= 1;
if (depth === 0) return i;
}
}
return -1;
};
같은 발상으로 객체의 필드를 나눌 때도 최상위 콤마만 구분자로 취급합니다. 덕분에 z.object({ label: z.literal('a,b') }) 같은 입력도 깨지지 않습니다.
import { z } from 'zod'; 나 export const user = 가 붙은 채 복사해도 동작하도록, z. 로 시작하는 위치부터 해석을 시작합니다.
변환할 수 없는 것을 조용히 버리지 않는다
$ref: 참조를 해결하지 않으므로 그 자리는z.any()가 됩니다allOf: 교차 타입으로 표현할 수 없어 첫 번째 서브스키마만 변환합니다if/then/not: 지원하지 않습니다
이것들을 조용히 z.any() 로 만들면 겉보기에는 깔끔하게 변환된 것 같지만 검증이 전부 통과되는 최악의 결과가 됩니다. 그래서 결과 아래에 경고로 표시합니다.
.refine() 같은 임의 함수도 마찬가지입니다. JSON Schema에는 표현 수단이 없어 반대 방향 변환에서 사라집니다. 생성물은 어디까지나 마이그레이션의 출발점으로 보고 반드시 내용을 확인하세요.
언제 쓰면 좋은가
OpenAPI 정의가 먼저 있는 프로젝트라면 components.schemas 의 내용을 붙여 넣는 것만으로 프런트엔드의 타입과 검증을 동시에 얻을 수 있습니다. 반대로 코드에서 Zod를 먼저 쓰고 있다면, 반대 방향 변환으로 API 문서용 JSON Schema를 만들 수 있습니다.
JSON 샘플만 있다면 지금까지처럼 JSON to Zod 변환 이 적합하고, OpenAPI 스키마 자체를 만들고 싶다면 JSON to OpenAPI 변환 도 있습니다. 모든 도구는 브라우저 안에서 처리가 끝나며 붙여 넣은 스키마가 외부로 전송되지 않습니다.
자주 묻는 질문
JSON to Zod와 어느 쪽을 써야 하나요?
JSON Schema(또는 OpenAPI 정의)가 이미 있다면 이쪽입니다. 필수 여부·enum·문자열 format 같은 제약이 보존되어 생성되는 Zod 스키마가 정확해집니다. 가진 것이 API 응답 같은 값 샘플뿐이라면 JSON to Zod 변환 을 사용하세요.
$ref가 들어 있는 스키마는 어떻게 하나요?
참조는 전개되지 않으므로 그 자리는 z.any() 가 되고 경고가 표시됩니다. 공통 정의를 $defs 로 빼둔 경우에는 붙여 넣기 전에 전개하거나, 생성 후 그 부분만 직접 고쳐 주세요.
생성한 Zod 스키마를 그대로 운영에 써도 되나요?
뼈대로는 쓸 수 있지만, 원래 JSON Schema에 없던 업무 규칙은 생성되지 않습니다. 또한 pattern 은 z.string().regex() 로 옮겨지므로 원래 정규식이 ECMAScript 호환이 아니면 동작이 달라질 수 있습니다. 반드시 확인 후 사용하세요.
붙여 넣은 스키마는 전송되나요?
아니요. JSON Schema ⇄ Zod 변환 은 브라우저 안에서 처리가 끝납니다. Zod 코드도 실행하지 않고 텍스트로 해석하므로, 사내 API 정의를 그대로 붙여 넣어도 외부로 나가지 않습니다.