์ฝ˜ํ…์ธ ๋กœ ์ด๋™

[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 Error

unrepresentable ์˜ต์…˜์„ "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,
// }
// }
// }