[z.fromJSONSchema()](https://zod.dev/json-schema?id=zfromjsonschema)
๐ Zod 4๋ ์๋ก์ด ๊ธฐ๋ฅ์ธ ๋ค์ดํฐ๋ธ JSON Schema ๋ณํ์ ๋์ ํฉ๋๋ค. JSON Schema๋ JSON์ ๊ตฌ์กฐ๋ฅผ ๊ธฐ์ ํ๋ ํ์ค์ผ๋ก, OpenAPI ์ ์๋ AI์ฉ ๊ตฌ์กฐํ๋ ์ถ๋ ฅ ์ ์์ ๋๋ฆฌ ์ฌ์ฉ๋ฉ๋๋ค.
์คํ์ โ z.fromJSONSchema() ํจ์๋ ์คํ ๋จ๊ณ์ด๋ฉฐ Zod์ ์์ ๋ API์ ์ผ๋ถ๋ก ๊ฐ์ฃผ๋์ง ์์ต๋๋ค. ํฅํ ๋ฆด๋ฆฌ์ค์์ ๊ตฌํ์ด ๋ณ๊ฒฝ๋ ๊ฐ๋ฅ์ฑ์ด ๋์ต๋๋ค.
Zod๋ z.fromJSONSchema()๋ฅผ ์ ๊ณตํ์ฌ JSON Schema๋ฅผ Zod ์คํค๋ง๋ก ๋ณํํฉ๋๋ค.
import * as z from "zod";
const jsonSchema = { type: "object", properties: { name: { type: "string" }, age: { type: "number" }, }, required: ["name", "age"], };
const zodSchema = z.fromJSONSchema(jsonSchema);Zod ์คํค๋ง๋ฅผ JSON Schema๋ก ๋ณํํ๋ ค๋ฉด z.toJSONSchema() ํจ์๋ฅผ ์ฌ์ฉํ์ธ์.
import * as z from "zod";
const schema = z.object({ name: z.string(), age: z.number(), });
z.toJSONSchema(schema) // => { // type: 'object', // properties: { name: { type: 'string' }, age: { type: 'number' } }, // required: [ 'name', 'age' ], // additionalProperties: false, // }๋ชจ๋ ์คํค๋ง์ ๊ฒ์ฌ ์กฐ๊ฑด์ ๊ฐ๋ฅํ ๊ฐ์ฅ ๊ฐ๊น์ด JSON Schema ๋์์ผ๋ก ๋ณํ๋ฉ๋๋ค. ์ผ๋ถ ํ์
์ ๋์ํ ์ ์์ผ๋ฉฐ ํฉ๋ฆฌ์ ์ผ๋ก ํํํ ์ ์์ต๋๋ค. ์๋ unrepresentable ์น์
์์ ์ด๋ฌํ ๊ฒฝ์ฐ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ํ์ธํ์ธ์.
z.bigint(); // โ z.int64(); // โ z.symbol(); // โ z.undefined(); // โ z.void(); // โ z.date(); // โ z.map(); // โ z.set(); // โ z.transform(); // โ z.nan(); // โ z.custom(); // โ๋ ๋ฒ์งธ ์ธ์๋ก ๋ณํ ๋ก์ง์ ์ฌ์ฉ์ ์ ์ํ ์ ์์ต๋๋ค.
z.toJSONSchema(schema, { // ...params })์๋๋ ์ง์๋๋ ๋งค๊ฐ๋ณ์๋ณ ๊ฐ๋จํ ์ฐธ๊ณ ์ ๋๋ค. ๊ฐ ํญ๋ชฉ์ ์๋์์ ๋ ์์ธํ ์ค๋ช ํฉ๋๋ค.
interface ToJSONSchemaParams { /** The JSON Schema version to target. * - `"draft-2020-12"` โ Default. JSON Schema Draft 2020-12 * - `"draft-07"` โ JSON Schema Draft 7 * - `"draft-04"` โ JSON Schema Draft 4 * - `"openapi-3.0"` โ OpenAPI 3.0 Schema Object */ target?: | "draft-04" | "draft-4" | "draft-07" | "draft-7" | "draft-2020-12" | "openapi-3.0" | ({} & string) | undefined;
/** A registry used to look up metadata for each schema. * Any schema with an `id` property will be extracted as a $def. */ metadata?: $ZodRegistry<Record<string, any>>;
/** How to handle unrepresentable types. * - `"throw"` โ Default. Unrepresentable types throw an error * - `"any"` โ Unrepresentable types become `{}` */ unrepresentable?: "throw" | "any";
/** How to handle cycles. * - `"ref"` โ Default. Cycles will be broken using $defs * - `"throw"` โ Cycles will throw an error if encountered */ cycles?: "ref" | "throw";
/* How to handle reused schemas. * - `"inline"` โ Default. Reused schemas will be inlined * - `"ref"` โ Reused schemas will be extracted as $defs */ reused?: "ref" | "inline";
/** A function used to convert `id` values to URIs to be used in *external* $refs. * * Default is `(id) => id`. */ uri?: (id: string) => string; }์ผ๋ถ ์คํค๋ง ํ์
์ ์
๋ ฅ ํ์
๊ณผ ์ถ๋ ฅ ํ์
์ด ๋ค๋ฆ
๋๋ค(์: ZodPipe, ZodDefault, ๊ฐ์ ๋ณํ๋ ๊ธฐ๋ณธ ํ์
). ๊ธฐ๋ณธ์ ์ผ๋ก z.toJSONSchema์ ๊ฒฐ๊ณผ๋ _์ถ๋ ฅ ํ์
_์ ๋ํ๋
๋๋ค; ์
๋ ฅ ํ์
์ ์ถ์ถํ๋ ค๋ฉด "io": "input"์ ์ฌ์ฉํ์ธ์.
const mySchema = z.string().transform(val => val.length).pipe(z.number()); // ZodPipe
const jsonSchema = z.toJSONSchema(mySchema); // => { type: "number" }
const jsonSchema = z.toJSONSchema(mySchema, { io: "input" }); // => { type: "string" }๋์ JSON Schema ๋ฒ์ ์ ์ค์ ํ๋ ค๋ฉด target ๋งค๊ฐ๋ณ์๋ฅผ ์ฌ์ฉํ์ธ์. ๊ธฐ๋ณธ๊ฐ์ Draft 2020-12์
๋๋ค.
z.toJSONSchema(schema, { target: "draft-07" }); z.toJSONSchema(schema, { target: "draft-2020-12" }); z.toJSONSchema(schema, { target: "draft-04" }); z.toJSONSchema(schema, { target: "openapi-3.0" });์์ง ์ฝ์ง ์์๋ค๋ฉด Zod์์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋ฐฉ๋ฒ์ ๋ํ ๋งฅ๋ฝ์ผ๋ก Metadata and registries ํ์ด์ง๋ฅผ ๋จผ์ ์ฐธ๊ณ ํ์ธ์.
Zod์์๋ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๋ ์ง์คํธ๋ฆฌ์ ์ ์ฅํฉ๋๋ค. Zod๋ id, title, description, examples ๊ฐ์ ๊ณตํต ๋ฉํ๋ฐ์ดํฐ ํ๋๋ฅผ ์ ์ฅํ ์ ์๋ ์ ์ญ ๋ ์ง์คํธ๋ฆฌ z.globalRegistry๋ฅผ ๋ด๋ณด๋
๋๋ค.
ZodZod Mini
import * as z from "zod";
// `.meta()`๋ `z.globalRegistry`์ ์คํค๋ง๋ฅผ ๋ฑ๋กํ๋ ํธ์ ๋ฉ์๋์
๋๋ค. const emailSchema = z.string().meta({ title: "Email address", description: "Your email address", });
z.toJSONSchema(emailSchema); // => { type: "string", title: "Email address", description: "Your email address", ... }๋ชจ๋ ๋ฉํ๋ฐ์ดํฐ ํ๋๋ ๊ฒฐ๊ณผ JSON Schema์ ๋ณต์ฌ๋ฉ๋๋ค.
const schema = z.string().meta({ whatever: 1234 });
z.toJSONSchema(schema); // => { type: "string", whatever: 1234 }๋ค์ API๋ JSON Schema๋ก ํํํ ์ ์์ต๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก Zod๋ ์ด๋ค์ ๋ง๋๋ฉด ์ค๋ฅ๋ฅผ ๋์ง๋๋ค. JSON์์ ๋์๋๋ ๊ฒ์ด ์์ผ๋ฏ๋ก ๋ณํ์ ์๋ํ๋ ๊ฒ์ ๋ถ์ ์ ํฉ๋๋ค. ๋ค์ ์ค ํ๋๋ฅผ ๋ง๋๋ฉด ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค.
z.bigint(); // โ z.int64(); // โ z.symbol(); // โ z.undefined(); // โ z.void(); // โ z.date(); // โ z.map(); // โ z.set(); // โ z.transform(); // โ z.nan(); // โ z.custom(); // โ๊ธฐ๋ณธ์ ์ผ๋ก Zod๋ ์ด๋ฌํ ํ์ ์ ๋ง๋๋ฉด ์ค๋ฅ๋ฅผ ๋์ง๋๋ค.
z.toJSONSchema(z.bigint()); // => throws Errorunrepresentable ์ต์
์ "any"๋ก ์ค์ ํ๋ฉด ์ด ๋์์ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค. ๊ทธ๋ฌ๋ฉด ํํํ ์ ์๋ ๋ชจ๋ ํ์
์ JSON Schema์์ {}(unknown๊ณผ ๋๋ฑํจ)๋ก ๋ณํํฉ๋๋ค.
z.toJSONSchema(z.bigint(), { unrepresentable: "any" }); // => {}์ํ์ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์
๋๋ค. z.toJSONSchema()๊ฐ ์คํค๋ง๋ฅผ ์ํํ๋ฉด์ ์ํ์ ๋ง๋๋ฉด $ref๋ก ํํ๋ฉ๋๋ค.
const User = z.object({ name: z.string(), get friend() { return User; }, });
z.toJSONSchema(User); // => { // type: 'object', // properties: { name: { type: 'string' }, friend: { '$ref': '#' } }, // required: [ 'name', 'friend' ], // additionalProperties: false, // }๋์ ์ค๋ฅ๋ฅผ ๋ฐ์์ํค๊ณ ์ถ๋ค๋ฉด cycles ์ต์
์ "throw"๋ก ์ค์ ํ์ธ์.
z.toJSONSchema(User, { cycles: "throw" }); // => throws Error๋์ผํ ์คํค๋ง๊ฐ ํ ์คํค๋ง ๋ด์์ ์ฌ๋ฌ ๋ฒ ๋ฑ์ฅํ ๋์ ์ฒ๋ฆฌ ๋ฐฉ์์ ๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก Zod๋ ์ด๋ฌํ ์คํค๋ง๋ฅผ ์ธ๋ผ์ธํฉ๋๋ค.
const name = z.string(); const User = z.object({ firstName: name, lastName: name, });
z.toJSONSchema(User); // => { // type: 'object', // properties: { // firstName: { type: 'string' }, // lastName: { type: 'string' } // }, // required: [ 'firstName', 'lastName' ], // additionalProperties: false, // }reused ์ต์
์ "ref"๋ก ์ค์ ํ์ฌ ์ด ์คํค๋ง๋ค์ $defs๋ก ์ถ์ถํ ์ ์์ต๋๋ค.
z.toJSONSchema(User, { reused: "ref" }); // => { // type: 'object', // properties: { // firstName: { '$ref': '#/$defs/__schema0' }, // lastName: { '$ref': '#/$defs/__schema0' } // }, // required: [ 'firstName', 'lastName' ], // additionalProperties: false, // '$defs': { __schema0: { type: 'string' } } // }override๋ฅผ ์ฌ์ฉํด ๋ง์ถค ๋ฎ์ด์ฐ๊ธฐ ๋ก์ง์ ์ ์ํ ์ ์์ต๋๋ค. ์ ๊ณต๋ ์ฝ๋ฐฑ์ ์๋ Zod ์คํค๋ง์ ๊ธฐ๋ณธ JSON Schema์ ์ ๊ทผํ ์ ์์ต๋๋ค. ์ด ํจ์๋ ctx.jsonSchema๋ฅผ ์ง์ ์์ ํด์ผ ํฉ๋๋ค.
const mySchema = /* ... */ z.toJSONSchema(mySchema, { override: (ctx)=>{ ctx.zodSchema; // the original Zod schema ctx.jsonSchema; // the default JSON Schema
// directly modify ctx.jsonSchema.whatever = "sup"; } });ํํํ ์ ์๋ ํ์
์ ์ด ํจ์๊ฐ ํธ์ถ๋๊ธฐ ์ ์ Error๋ฅผ ๋์ง๋๋ค. ํํํ ์ ์๋ ํ์
์ ๋ํด ์ฌ์ฉ์ ์ ์ ๋์์ ์ ์ํ๋ ค๋ฉด override์ ํจ๊ป unrepresentable: "any"๋ฅผ ์ค์ ํด์ผ ํฉ๋๋ค.
// support z.date() as ISO datetime strings const result = z.toJSONSchema(z.date(), { unrepresentable: "any", override: (ctx) => { const def = ctx.zodSchema._zod.def; if(def.type ==="date"){ ctx.jsonSchema.type = "string"; ctx.jsonSchema.format = "date-time"; } }, });๋ค์์ Zod์ JSON Schema ๋ณํ ๋ก์ง์ ๋ํ ์ถ๊ฐ ์ค๋ช ์ ๋๋ค.
Zod๋ ๋ค์ ์คํค๋ง ํ์
๋ค์ ๋๋ฑํ JSON Schema format์ผ๋ก ๋ณํํฉ๋๋ค.
// Supported via `format` z.email(); // => { type: "string", format: "email" } z.iso.datetime(); // => { type: "string", format: "date-time" } z.iso.date(); // => { type: "string", format: "date" } z.iso.time(); // => { type: "string", format: "time" } z.iso.duration(); // => { type: "string", format: "duration" } z.ipv4(); // => { type: "string", format: "ipv4" } z.ipv6(); // => { type: "string", format: "ipv6" } z.uuid(); // => { type: "string", format: "uuid" } z.guid(); // => { type: "string", format: "uuid" } z.url(); // => { type: "string", format: "uri" }์ด ์คํค๋ง๋ค์ contentEncoding์ ํตํด ์ง์๋ฉ๋๋ค.
z.base64(); // => { type: "string", contentEncoding: "base64" }๋ค๋ฅธ ๋ชจ๋ ๋ฌธ์์ด ํ์์ pattern์ ํตํด ์ง์๋ฉ๋๋ค.
z.base64url(); z.cuid(); z.emoji(); z.nanoid(); z.cuid2(); z.ulid(); z.cidrv4(); z.cidrv6(); z.mac();Zod๋ ๋ค์ ์ซ์ ํ์ ์ JSON Schema๋ก ๋ณํํฉ๋๋ค.
// number z.number(); // => { type: "number" } z.float32(); // => { type: "number", exclusiveMinimum: ..., exclusiveMaximum: ... } z.float64(); // => { type: "number", exclusiveMinimum: ..., exclusiveMaximum: ... }
// integer z.int(); // => { type: "integer" } z.int32(); // => { type: "integer", exclusiveMinimum: ..., exclusiveMaximum: ... }๊ธฐ๋ณธ์ ์ผ๋ก z.object() ์คํค๋ง๋ additionalProperties: "false"๋ฅผ ํฌํจํฉ๋๋ค. ์ด๋ ํ๋ฒํ z.object() ์คํค๋ง๊ฐ ์ถ๊ฐ ์์ฑ์ ์ ๊ฑฐํ๋ Zod์ ๊ธฐ๋ณธ ๋์์ ์ ํํ ๋ฐ์ํ ๊ฒ์
๋๋ค.
import * as z from "zod";
const schema = z.object({ name: z.string(), age: z.number(), });
z.toJSONSchema(schema) // => { // type: 'object', // properties: { name: { type: 'string' }, age: { type: 'number' } }, // required: [ 'name', 'age' ], // additionalProperties: false, // }"input" ๋ชจ๋๋ก JSON Schema๋ก ๋ณํํ ๋๋ additionalProperties๊ฐ ์ค์ ๋์ง ์์ต๋๋ค. ์์ธํ ๋ด์ฉ์ io ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ธ์.
import * as z from "zod";
const schema = z.object({ name: z.string(), age: z.number(), });
z.toJSONSchema(schema, { io: "input" }); // => { // type: 'object', // properties: { name: { type: 'string' }, age: { type: 'number' } }, // required: [ 'name', 'age' ], // }๋ฐ๋ฉด:
z.looseObject()๋additionalProperties: false๋ฅผ ์ ๋ ์ค์ ํ์ง ์์ต๋๋ค.z.strictObject()๋ ํญ์additionalProperties: false๋ฅผ ์ค์ ํฉ๋๋ค.
Zod๋ z.file()์ ๋ค์ OpenAPI ์นํ์ ์ธ ์คํค๋ง๋ก ๋ณํํฉ๋๋ค.
z.file(); // => { type: "string", format: "binary", contentEncoding: "binary" }ํฌ๊ธฐ ๋ฐ MIME ๊ฒ์ฌ๋ ํํ๋ฉ๋๋ค.
z.file().min(1).max(1024 * 1024).mime("image/png"); // => { // type: "string", // format: "binary", // contentEncoding: "binary", // contentMediaType: "image/png", // minLength: 1, // maxLength: 1048576, // }Zod๋ z.null()์ JSON Schema์์ { type: "null" }๋ก ๋ณํํฉ๋๋ค.
z.null(); // => { type: "null" }z.undefined()๋ JSON Schema์์ ํํํ ์ ์์์ ์ฐธ๊ณ ํ์ธ์ (์๋ ์ฐธ์กฐ).
๋น์ทํ๊ฒ, nullable์ null๊ณผ์ ์ ๋์ธ์ผ๋ก ํํ๋ฉ๋๋ค.
z.nullable(z.string()); // => { oneOf: [{ type: "string" }, { type: "null" }] }optional ์คํค๋ง๋ ๊ทธ๋๋ก ํํ๋์ง๋ง optional ์ฃผ์์ด ๋ถ์ต๋๋ค.
z.optional(z.string()); // => { type: "string" }์คํค๋ง๋ฅผ z.toJSONSchema()์ ์ ๋ฌํ๋ฉด ์์ฒด ํฌํจ JSON Schema๊ฐ ๋ฐํ๋ฉ๋๋ค.
๋ ๋ค๋ฅธ ๊ฒฝ์ฐ์๋ ์ฌ๋ฌ ์ฐ๊ฒฐ๋ JSON Schema๋ก ํํํ๋ ค๋ Zod ์คํค๋ง ์ธํธ๋ฅผ .json ํ์ผ๋ก ์์ฑํ์ฌ ์น ์๋ฒ์์ ์ ๊ณตํ๋ ค๋ ๊ฒฝ์ฐ๊ฐ ์์ ์ ์์ต๋๋ค.
import * as z from "zod";
const User = z.object({ name: z.string(), get posts(){ return z.array(Post); } });
const Post = z.object({ title: z.string(), content: z.string(), get author(){ return User; } });
z.globalRegistry.add(User, {id: "User"}); z.globalRegistry.add(Post, {id: "Post"});์ด๋ฅผ ์ํด z.toJSONSchema()์ registry๋ฅผ ์ ๋ฌํ ์ ์์ต๋๋ค.
์ค์ โ ๋ชจ๋ ์คํค๋ง๋ ๋ ์ง์คํธ๋ฆฌ์ ๋ฑ๋ก๋ id ์์ฑ์ ๊ฐ์ ธ์ผ ํฉ๋๋ค! id๊ฐ ์๋ ์คํค๋ง๋ ๋ฌด์๋ฉ๋๋ค.
z.toJSONSchema(z.globalRegistry); // => { // schemas: { // User: { // id: 'User', // type: 'object', // properties: { // name: { type: 'string' }, // posts: { type: 'array', items: { '$ref': 'Post' } } // }, // required: [ 'name', 'posts' ], // additionalProperties: false, // }, // Post: { // id: 'Post', // type: 'object', // properties: { // title: { type: 'string' }, // content: { type: 'string' }, // author: { '$ref': 'User' } // }, // required: [ 'title', 'content', 'author' ], // additionalProperties: false, // } // } // }๊ธฐ๋ณธ์ ์ผ๋ก $ref URI๋ "User"์ฒ๋ผ ๊ฐ๋จํ ์๋ ๊ฒฝ๋ก์
๋๋ค. ์ด๋ฌํ ๊ฒฝ๋ก๋ฅผ ์ ๋ URI๋ก ๋ง๋ค๋ ค๋ฉด uri ์ต์
์ ์ฌ์ฉํ์ธ์. ์ด ์ต์
์ id๋ฅผ ์์ ํ URI๋ก ๋ณํํ๋ ํจ์๋ฅผ ๊ธฐ๋ํฉ๋๋ค.
z.toJSONSchema(z.globalRegistry, { uri: (id) => `https://example.com/${id}.json` }); // => { // schemas: { // User: { // id: 'User', // type: 'object', // properties: { // name: { type: 'string' }, // posts: { // type: 'array', // items: { '$ref': 'https://example.com/Post.json' } // } // }, // required: [ 'name', 'posts' ], // additionalProperties: false, // }, // Post: { // id: 'Post', // type: 'object', // properties: { // title: { type: 'string' }, // content: { type: 'string' }, // author: { '$ref': 'https://example.com/User.json' } // }, // required: [ 'title', 'content', 'author' ], // additionalProperties: false, // } // } // }