diff --git a/packages/core/src/json-schema.ts b/packages/core/src/json-schema.ts
index 98913a7f0a..486a3ac76e 100644
--- a/packages/core/src/json-schema.ts
+++ b/packages/core/src/json-schema.ts
@@ -9,6 +9,9 @@ export type Schema =
// | BaseSchema;
export interface BaseSchema {
+ /** A special key used as an intermediate representation of extends-style relationships. Removed in the omit stage. */
+ _ref?: BaseSchema;
+
type?: string | undefined;
$id?: string | undefined;
id?: string | undefined;
diff --git a/packages/core/src/registries.ts b/packages/core/src/registries.ts
index cacdbc439b..28c0eaaf28 100644
--- a/packages/core/src/registries.ts
+++ b/packages/core/src/registries.ts
@@ -61,28 +61,20 @@ export interface JSONSchemaMeta {
[k: string]: unknown;
}
-export class $ZodJSONSchemaRegistry<
- Meta extends JSONSchemaMeta = JSONSchemaMeta,
- Schema extends $ZodType = $ZodType,
-> extends $ZodRegistry {
- toJSONSchema(_schema: Schema): object {
- return {};
- }
-}
+// export class $ZodJSONSchemaRegistry<
+// Meta extends JSONSchemaMeta = JSONSchemaMeta,
+// Schema extends $ZodType = $ZodType,
+// > extends $ZodRegistry {
+// toJSONSchema(_schema: Schema): object {
+// return {};
+// }
+// }
export interface GlobalMeta extends JSONSchemaMeta {}
-export const globalRegistry: $ZodJSONSchemaRegistry =
- /*@__PURE__*/ new $ZodJSONSchemaRegistry();
-
// registries
export function registry(): $ZodRegistry {
return new $ZodRegistry();
}
-export function jsonSchemaRegistry<
- T extends JSONSchemaMeta = JSONSchemaMeta,
- S extends $ZodType = $ZodType,
->(): $ZodJSONSchemaRegistry {
- return new $ZodJSONSchemaRegistry();
-}
+export const globalRegistry: $ZodRegistry = /*@__PURE__*/ registry();
diff --git a/packages/core/src/schemas.ts b/packages/core/src/schemas.ts
index 176d0959b4..64e9215616 100644
--- a/packages/core/src/schemas.ts
+++ b/packages/core/src/schemas.ts
@@ -130,7 +130,7 @@ export interface $ZodTypeInternals extends $Zo
/** The constructor function of this schema. */
constr: new (
def: any
- ) => any;
+ ) => $ZodType;
/** A catchall object for computed metadata related to this schema. Commonly modified by checks using `onattach`. */
computed: Record;
@@ -140,6 +140,9 @@ export interface $ZodTypeInternals extends $Zo
/** An optional method used to override `toJSONSchema` logic. */
toJSONSchema?: () => object;
+
+ /** The parent of this schema. Only set during certain clone operations. */
+ parent?: $ZodType | undefined;
}
export interface $ZodType {
diff --git a/packages/core/src/to-json-schema.ts b/packages/core/src/to-json-schema.ts
index 2aea3a9865..19f5daa28b 100644
--- a/packages/core/src/to-json-schema.ts
+++ b/packages/core/src/to-json-schema.ts
@@ -61,11 +61,16 @@ const formatMap: Partial> = {
};
interface Seen {
+ /** JSON Schema result for this Zod schema */
schema: JSONSchema.BaseSchema;
- cached: JSONSchema.BaseSchema;
+ /** A cached version of the schema that doesn't get overwritten during ref resolution */
+ def?: JSONSchema.BaseSchema;
+ defId?: string | undefined;
+ /** Number of times this schema was encountered during traversal */
count: number;
+ /** Cycle path */
cycle?: (string | number)[] | undefined;
- defId?: string | undefined;
+
// external?: string | undefined;
}
@@ -118,34 +123,31 @@ export class JSONSchemaGenerator {
}
// initialize
- const result: Seen = { schema: {}, cached: {}, count: 1, cycle: undefined };
+ const result: Seen = { schema: {}, count: 1, cycle: undefined };
this.seen.set(schema, result);
+ // if(schema._zod.parent){
+ // // schema was cloned from another schema
+ // result.
+ // }
+
if (schema._zod.toJSONSchema) {
// custom method overrides default behavior
result.schema = schema._zod.toJSONSchema() as any;
// return
}
- const _json = result.schema;
-
// check if external
// const ext = this.external?.registry.get(schema)?.id;
// if (ext) {
// result.external = ext;
// }
- // metadata
- const meta = this.metadataRegistry.get(schema);
- if (meta) {
- Object.assign(_json, meta);
- // schema exists in registry. add to $defs.
- // if (meta.id) _json.id = meta.id;
- // if (meta.description) _json.description = meta.description;
- // if (meta.title) _json.title = meta.title;
- // if (meta.examples) _json.examples = meta.examples;
- // if (meta.example) _json.examples = [meta.example];
- }
+ // // metadata
+ // const meta = this.metadataRegistry.get(schema);
+ // if (meta) {
+ // Object.assign(_json, meta);
+ // }
// const def = (schema as schemas.$ZodTypes)._zod.def;
const params = {
@@ -154,373 +156,391 @@ export class JSONSchemaGenerator {
path: _params.path,
};
- switch (def.type) {
- case "string": {
- const json: JSONSchema.StringSchema = _json as any;
- json.type = "string";
- const { minimum, maximum, format, pattern, contentEncoding } = schema._zod.computed as {
- minimum?: number;
- maximum?: number;
- format?: checks.$ZodStringFormats;
- pattern?: RegExp;
- contentEncoding?: string;
- };
- if (minimum) json.minLength = minimum;
- if (maximum) json.maxLength = maximum;
- // custom pattern overrides format
- if (format) {
- json.format = formatMap[format] ?? format;
- }
- if (pattern) {
- json.pattern = pattern.source;
+ if (schema._zod.parent) {
+ // schema was cloned from another schema
+ result.schema._ref = this.process(schema._zod.parent, params);
+ } else {
+ const _json = result.schema;
+ switch (def.type) {
+ case "string": {
+ const json: JSONSchema.StringSchema = _json as any;
+ json.type = "string";
+ const { minimum, maximum, format, pattern, contentEncoding } = schema._zod.computed as {
+ minimum?: number;
+ maximum?: number;
+ format?: checks.$ZodStringFormats;
+ pattern?: RegExp;
+ contentEncoding?: string;
+ };
+ if (minimum) json.minLength = minimum;
+ if (maximum) json.maxLength = maximum;
+ // custom pattern overrides format
+ if (format) {
+ json.format = formatMap[format] ?? format;
+ }
+ if (pattern) {
+ json.pattern = pattern.source;
+ }
+ if (contentEncoding) json.contentEncoding = contentEncoding;
+
+ break;
}
- if (contentEncoding) json.contentEncoding = contentEncoding;
+ case "number": {
+ const json: JSONSchema.NumberSchema | JSONSchema.IntegerSchema = _json as any;
+ const { minimum, maximum, format, multipleOf, inclusive } = schema._zod.computed as {
+ minimum?: number;
+ maximum?: number;
+ format?: checks.$ZodNumberFormats;
+ multipleOf?: number;
+ inclusive?: boolean;
+ };
+ if (format?.includes("int")) json.type = "integer";
+ else json.type = "number";
+
+ if (minimum) {
+ if (inclusive) json.minimum = minimum;
+ else json.exclusiveMinimum = minimum;
+ }
+ if (maximum) {
+ if (inclusive) json.maximum = maximum;
+ else json.exclusiveMaximum = maximum;
+ }
+ if (multipleOf) json.multipleOf = multipleOf;
- break;
- }
- case "number": {
- const json: JSONSchema.NumberSchema | JSONSchema.IntegerSchema = _json as any;
- const { minimum, maximum, format, multipleOf, inclusive } = schema._zod.computed as {
- minimum?: number;
- maximum?: number;
- format?: checks.$ZodNumberFormats;
- multipleOf?: number;
- inclusive?: boolean;
- };
- if (format?.includes("int")) json.type = "integer";
- else json.type = "number";
-
- if (minimum) {
- if (inclusive) json.minimum = minimum;
- else json.exclusiveMinimum = minimum;
- }
- if (maximum) {
- if (inclusive) json.maximum = maximum;
- else json.exclusiveMaximum = maximum;
- }
- if (multipleOf) json.multipleOf = multipleOf;
-
- break;
- }
- case "boolean": {
- const json = _json as JSONSchema.BooleanSchema;
- json.type = "boolean";
- break;
- }
- case "bigint": {
- if (this.unrepresentable === "throw") {
- throw new Error("BigInt cannot be represented in JSON Schema");
+ break;
}
- break;
- }
- case "symbol": {
- if (this.unrepresentable === "throw") {
- throw new Error("Symbols cannot be represented in JSON Schema");
+ case "boolean": {
+ const json = _json as JSONSchema.BooleanSchema;
+ json.type = "boolean";
+ break;
}
- break;
- }
- case "undefined": {
- const json = _json as JSONSchema.NullSchema;
- json.type = "null";
- break;
- }
- case "null": {
- _json.type = "null";
- // const json = { type: "null" } as JSONSchema.NullSchema;
- break;
- }
- case "any": {
- break;
- }
- case "unknown": {
- break;
- }
- case "never": {
- _json.not = {};
- break;
- }
- case "void": {
- if (this.unrepresentable === "throw") {
- throw new Error("Void cannot be represented in JSON Schema");
+ case "bigint": {
+ if (this.unrepresentable === "throw") {
+ throw new Error("BigInt cannot be represented in JSON Schema");
+ }
+ break;
}
- break;
- }
- case "date": {
- if (this.unrepresentable === "throw") {
- throw new Error("Date cannot be represented in JSON Schema");
+ case "symbol": {
+ if (this.unrepresentable === "throw") {
+ throw new Error("Symbols cannot be represented in JSON Schema");
+ }
+ break;
}
- break;
- }
- case "array": {
- const json: JSONSchema.ArraySchema = _json as any;
- const { minimum, maximum } = schema._zod.computed as {
- minimum?: number;
- maximum?: number;
- };
- if (minimum) json.minItems = minimum;
- if (maximum) json.maxItems = maximum;
- json.type = "array";
- json.items = this.process(def.element, { ...params, path: [...params.path, "items"] });
- break;
- }
- case "object":
- case "interface": {
- const json: JSONSchema.ObjectSchema = _json as any;
- json.type = "object";
- json.properties = {};
- const shape = def.shape; // params.shapeCache.get(schema)!;
- // if (!shape) {
- // shape = def.shape;
- // params.shapeCache.set(schema, shape);
- // }
- for (const key in shape) {
- json.properties[key] = this.process(shape[key], {
- ...params,
- path: [...params.path, "properties", key],
- });
+ case "undefined": {
+ const json = _json as JSONSchema.NullSchema;
+ json.type = "null";
+ break;
+ }
+ case "null": {
+ _json.type = "null";
+ // const json = { type: "null" } as JSONSchema.NullSchema;
+ break;
+ }
+ case "any": {
+ break;
+ }
+ case "unknown": {
+ break;
+ }
+ case "never": {
+ _json.not = {};
+ break;
+ }
+ case "void": {
+ if (this.unrepresentable === "throw") {
+ throw new Error("Void cannot be represented in JSON Schema");
+ }
+ break;
}
+ case "date": {
+ if (this.unrepresentable === "throw") {
+ throw new Error("Date cannot be represented in JSON Schema");
+ }
+ break;
+ }
+ case "array": {
+ const json: JSONSchema.ArraySchema = _json as any;
+ const { minimum, maximum } = schema._zod.computed as {
+ minimum?: number;
+ maximum?: number;
+ };
+ if (minimum) json.minItems = minimum;
+ if (maximum) json.maxItems = maximum;
+ json.type = "array";
+ json.items = this.process(def.element, { ...params, path: [...params.path, "items"] });
+ break;
+ }
+ case "object":
+ case "interface": {
+ const json: JSONSchema.ObjectSchema = _json as any;
+ json.type = "object";
+ json.properties = {};
+ const shape = def.shape; // params.shapeCache.get(schema)!;
+ // if (!shape) {
+ // shape = def.shape;
+ // params.shapeCache.set(schema, shape);
+ // }
+ for (const key in shape) {
+ json.properties[key] = this.process(shape[key], {
+ ...params,
+ path: [...params.path, "properties", key],
+ });
+ }
- // required keys
- const allKeys = new Set(Object.keys(shape));
- const optionalKeys = new Set(def.optional);
- const requiredKeys = new Set([...allKeys].filter((key) => !optionalKeys.has(key)));
- json.required = Array.from(requiredKeys);
+ // required keys
+ const allKeys = new Set(Object.keys(shape));
+ const optionalKeys = new Set(def.optional);
+ const requiredKeys = new Set([...allKeys].filter((key) => !optionalKeys.has(key)));
+ json.required = Array.from(requiredKeys);
+
+ // catchall
+ if (def.catchall) {
+ json.additionalProperties = this.process(def.catchall, {
+ ...params,
+ path: [...params.path, "additionalProperties"],
+ });
+ }
- // catchall
- if (def.catchall) {
- json.additionalProperties = this.process(def.catchall, {
- ...params,
- path: [...params.path, "additionalProperties"],
- });
+ break;
}
-
- break;
- }
- case "union": {
- const json: JSONSchema.BaseSchema = _json as any;
- json.anyOf = def.options.map((x, i) =>
- this.process(x, {
- ...params,
- path: [...params.path, "anyOf", i],
- })
- );
- break;
- }
- case "intersection": {
- const json: JSONSchema.BaseSchema = _json as any;
- json.allOf = [
- this.process(def.left, {
- ...params,
- path: [...params.path, "allOf", 0],
- }),
- this.process(def.right, {
- ...params,
- path: [...params.path, "allOf", 1],
- }),
- ];
- break;
- }
- case "tuple": {
- const json: JSONSchema.ArraySchema = _json as any;
- json.type = "array";
- const prefixItems = def.items.map((x, i) =>
- this.process(x, { ...params, path: [...params.path, "prefixItems", i] })
- );
- if (this.target === "draft-2020-12") {
- json.prefixItems = prefixItems;
- } else {
- json.items = prefixItems;
- }
-
- if (def.rest) {
- const rest = this.process(def.rest, {
- ...params,
- path: [...params.path, "items"],
- });
+ case "union": {
+ const json: JSONSchema.BaseSchema = _json as any;
+ json.anyOf = def.options.map((x, i) =>
+ this.process(x, {
+ ...params,
+ path: [...params.path, "anyOf", i],
+ })
+ );
+ break;
+ }
+ case "intersection": {
+ const json: JSONSchema.BaseSchema = _json as any;
+ json.allOf = [
+ this.process(def.left, {
+ ...params,
+ path: [...params.path, "allOf", 0],
+ }),
+ this.process(def.right, {
+ ...params,
+ path: [...params.path, "allOf", 1],
+ }),
+ ];
+ break;
+ }
+ case "tuple": {
+ const json: JSONSchema.ArraySchema = _json as any;
+ json.type = "array";
+ const prefixItems = def.items.map((x, i) =>
+ this.process(x, { ...params, path: [...params.path, "prefixItems", i] })
+ );
if (this.target === "draft-2020-12") {
- json.items = rest;
+ json.prefixItems = prefixItems;
} else {
- json.additionalItems = rest;
+ json.items = prefixItems;
+ }
+
+ if (def.rest) {
+ const rest = this.process(def.rest, {
+ ...params,
+ path: [...params.path, "items"],
+ });
+ if (this.target === "draft-2020-12") {
+ json.items = rest;
+ } else {
+ json.additionalItems = rest;
+ }
+ }
+
+ // additionalItems
+ if (def.rest) {
+ json.items = this.process(def.rest, {
+ ...params,
+ path: [...params.path, "items"],
+ });
}
- }
- // additionalItems
- if (def.rest) {
- json.items = this.process(def.rest, {
+ // length
+ const { minimum, maximum } = schema._zod.computed as {
+ minimum?: number;
+ maximum?: number;
+ };
+ if (minimum) json.minItems = minimum;
+ if (maximum) json.maxItems = maximum;
+ break;
+ }
+ case "record": {
+ const json: JSONSchema.ObjectSchema = _json as any;
+ json.type = "object";
+ json.propertyNames = this.process(def.keyType, { ...params, path: [...params.path, "propertyNames"] });
+ json.additionalProperties = this.process(def.valueType, {
...params,
- path: [...params.path, "items"],
+ path: [...params.path, "additionalProperties"],
});
+ break;
}
-
- // length
- const { minimum, maximum } = schema._zod.computed as {
- minimum?: number;
- maximum?: number;
- };
- if (minimum) json.minItems = minimum;
- if (maximum) json.maxItems = maximum;
- break;
- }
- case "record": {
- const json: JSONSchema.ObjectSchema = _json as any;
- json.type = "object";
- json.propertyNames = this.process(def.keyType, { ...params, path: [...params.path, "propertyNames"] });
- json.additionalProperties = this.process(def.valueType, {
- ...params,
- path: [...params.path, "additionalProperties"],
- });
- break;
- }
- case "map": {
- if (this.unrepresentable === "throw") {
- throw new Error("Map cannot be represented in JSON Schema");
+ case "map": {
+ if (this.unrepresentable === "throw") {
+ throw new Error("Map cannot be represented in JSON Schema");
+ }
+ break;
}
- break;
- }
- case "set": {
- if (this.unrepresentable === "throw") {
- throw new Error("Set cannot be represented in JSON Schema");
+ case "set": {
+ if (this.unrepresentable === "throw") {
+ throw new Error("Set cannot be represented in JSON Schema");
+ }
+ break;
}
- break;
- }
- case "enum": {
- const json: JSONSchema.BaseSchema = _json as any;
- json.enum = Object.values(def.entries);
- break;
- }
- case "literal": {
- const json: JSONSchema.BaseSchema = _json as any;
- const vals: (string | number | boolean | null)[] = [];
- for (const val of def.values) {
- if (val === undefined) {
- if (this.unrepresentable === "throw") {
- throw new Error("Literal `undefined` cannot be represented in JSON Schema");
- } else {
- // do not add to vals
- }
- } else if (typeof val === "bigint") {
- if (this.unrepresentable === "throw") {
- throw new Error("BigInt literals cannot be represented in JSON Schema");
+ case "enum": {
+ const json: JSONSchema.BaseSchema = _json as any;
+ json.enum = Object.values(def.entries);
+ break;
+ }
+ case "literal": {
+ const json: JSONSchema.BaseSchema = _json as any;
+ const vals: (string | number | boolean | null)[] = [];
+ for (const val of def.values) {
+ if (val === undefined) {
+ if (this.unrepresentable === "throw") {
+ throw new Error("Literal `undefined` cannot be represented in JSON Schema");
+ } else {
+ // do not add to vals
+ }
+ } else if (typeof val === "bigint") {
+ if (this.unrepresentable === "throw") {
+ throw new Error("BigInt literals cannot be represented in JSON Schema");
+ } else {
+ vals.push(Number(val));
+ }
} else {
- vals.push(Number(val));
+ vals.push(val);
}
+ }
+ if (vals.length === 0) {
+ // do nothing (an undefined literal was stripped)
+ } else if (vals.length === 1) {
+ const val = vals[0];
+ json.const = val;
} else {
- vals.push(val);
+ json.enum = vals;
}
+ break;
}
- if (vals.length === 0) {
- // do nothing (an undefined literal was stripped)
- } else if (vals.length === 1) {
- const val = vals[0];
- json.const = val;
- } else {
- json.enum = vals;
- }
- break;
- }
- case "file": {
- if (this.unrepresentable === "throw") {
- throw new Error("File cannot be represented in JSON Schema");
+ case "file": {
+ if (this.unrepresentable === "throw") {
+ throw new Error("File cannot be represented in JSON Schema");
+ }
+ break;
}
- break;
- }
- case "transform": {
- if (this.unrepresentable === "throw") {
- throw new Error("Transforms cannot be represented in JSON Schema");
+ case "transform": {
+ if (this.unrepresentable === "throw") {
+ throw new Error("Transforms cannot be represented in JSON Schema");
+ }
+ break;
}
- break;
- }
- case "nullable": {
- const inner = this.process(def.innerType, params);
- _json.anyOf = [inner, { type: "null" }];
- break;
- }
- case "nonoptional": {
- const inner = this.process(def.innerType, params);
- Object.assign(_json, inner);
- break;
- }
- case "success": {
- const json = _json as JSONSchema.BooleanSchema;
- json.type = "boolean";
- break;
- }
- case "default": {
- const inner = this.process(def.innerType, params);
- Object.assign(_json, inner);
- _json.default = def.defaultValue();
- break;
- }
- case "catch": {
- // use conditionals
- const inner = this.process(def.innerType, params);
- Object.assign(_json, inner);
- let catchValue: any;
- try {
- catchValue = def.catchValue(undefined as any);
- } catch {
- throw new Error("Dynamic catch values are not supported in JSON Schema");
- }
- _json.default = catchValue;
- break;
- }
- case "nan": {
- if (this.unrepresentable === "throw") {
- throw new Error("NaN cannot be represented in JSON Schema");
+ case "nullable": {
+ const inner = this.process(def.innerType, params);
+ _json.anyOf = [inner, { type: "null" }];
+ break;
}
- break;
- }
- case "pipe": {
- const innerType = this.io === "input" ? def.in : def.out;
- const inner = this.process(innerType, params);
- result.schema = inner;
+ case "nonoptional": {
+ const inner = this.process(def.innerType, params);
+ Object.assign(_json, inner);
+ break;
+ }
+ case "success": {
+ const json = _json as JSONSchema.BooleanSchema;
+ json.type = "boolean";
+ break;
+ }
+ case "default": {
+ const inner = this.process(def.innerType, params);
+ Object.assign(_json, inner);
+ _json.default = def.defaultValue();
+ break;
+ }
+ case "catch": {
+ // use conditionals
+ const inner = this.process(def.innerType, params);
+ Object.assign(_json, inner);
+ let catchValue: any;
+ try {
+ catchValue = def.catchValue(undefined as any);
+ } catch {
+ throw new Error("Dynamic catch values are not supported in JSON Schema");
+ }
+ _json.default = catchValue;
+ break;
+ }
+ case "nan": {
+ if (this.unrepresentable === "throw") {
+ throw new Error("NaN cannot be represented in JSON Schema");
+ }
+ break;
+ }
+ case "pipe": {
+ const innerType = this.io === "input" ? def.in : def.out;
+ const inner = this.process(innerType, params);
+ // result.schema = inner;
+ _json._ref = inner;
- break;
- }
- case "readonly": {
- const inner = this.process(def.innerType, params);
- Object.assign(_json, inner);
- // _json.allOf = [inner];
- _json.readOnly = true;
- break;
- }
- case "template_literal": {
- const json = _json as JSONSchema.StringSchema;
- const pattern = schema._zod.pattern;
- if (!pattern) throw new Error("Pattern not found in template literal");
- json.type = "string";
- json.pattern = pattern.source;
- break;
- }
- case "promise": {
- const inner = this.process(def.innerType, params);
- result.schema = inner;
- break;
- }
- // passthrough types
- case "optional": {
- const inner = this.process(def.innerType, params);
- result.schema = inner;
- break;
- }
- case "lazy": {
- const innerType = (schema as schemas.$ZodLazy)._zod._getter;
- const inner = this.process(innerType, params);
- result.schema = inner;
- break;
- }
- case "custom": {
- if (this.unrepresentable === "throw") {
- throw new Error("Custom types cannot be represented in JSON Schema");
+ break;
+ }
+ case "readonly": {
+ _json._ref = this.process(def.innerType, params);
+ _json.readOnly = true;
+ // const inner = this.process(def.innerType, params);
+ // Object.assign(_json, inner);
+ // _json.allOf = [inner];
+ // result.extra.readOnly = true;
+ // _json.readOnly = true;
+ break;
+ }
+ case "template_literal": {
+ const json = _json as JSONSchema.StringSchema;
+ const pattern = schema._zod.pattern;
+ if (!pattern) throw new Error("Pattern not found in template literal");
+ json.type = "string";
+ json.pattern = pattern.source;
+ break;
+ }
+ case "promise": {
+ const inner = this.process(def.innerType, params);
+ // result.schema = inner;
+ _json._ref = inner;
+ break;
+ }
+ // passthrough types
+ case "optional": {
+ // result.schema = this.process(def.innerType, params);
+ _json._ref = this.process(def.innerType, params);
+ break;
+ }
+ case "lazy": {
+ const innerType = (schema as schemas.$ZodLazy)._zod._getter;
+ const inner = this.process(innerType, params);
+ // result.schema = inner;
+ _json._ref = inner;
+ break;
+ }
+ case "custom": {
+ if (this.unrepresentable === "throw") {
+ throw new Error("Custom types cannot be represented in JSON Schema");
+ }
+ break;
+ }
+ default: {
+ def satisfies never;
}
- break;
- }
- default: {
- def satisfies never;
}
}
+ // metadata
+ const meta = this.metadataRegistry.get(schema);
+ if (meta) {
+ Object.assign(result.schema, meta);
+ }
+
// pulling fresh from this.seen in case it was overwritten
const _result = this.seen.get(schema)!;
@@ -529,7 +549,7 @@ export class JSONSchemaGenerator {
jsonSchema: _result.schema,
});
- Object.assign(_result.cached, _result.schema);
+ // Object.assign(_result.cached, _result.schema);
return _result.schema;
}
@@ -543,76 +563,90 @@ export class JSONSchemaGenerator {
} satisfies EmitParams;
// iterate over seen map
- const result: JSONSchema.BaseSchema = {};
- const defs: JSONSchema.BaseSchema["$defs"] = params.external?.defs ?? {};
- const seen = this.seen.get(schema);
- if (!seen) throw new Error("Unprocessed schema. This is a bug in Zod.");
- Object.assign(result, seen.cached);
+ // const result: JSONSchema.BaseSchema = {};
+ // const defs: JSONSchema.BaseSchema["$defs"] = params.external?.defs ?? {};
+ const root = this.seen.get(schema);
+
+ if (!root) throw new Error("Unprocessed schema. This is a bug in Zod.");
+
+ // initialize result with root schema fields
+ // Object.assign(result, seen.cached);
const makeURI = (entry: [schemas.$ZodType, Seen]): { ref: string; defId?: string } => {
// comparing the seen objects because sometimes
// multiple schemas map to the same seen object.
// e.g. lazy
- if (entry[1] === seen) {
- return { ref: "#" };
- }
// external is configured
const defsSegment = this.target === "draft-2020-12" ? "$defs" : "definitions";
if (params.external) {
const externalId = params.external.registry.get(entry[0])?.id; // ?? "__shared";// `__schema${this.counter++}`;
+
+ // check if schema is in the external registry
if (externalId) return { ref: params.external.uri(externalId) };
+
+ // otherwise, add to __shared
const id = entry[1].defId ?? entry[1].schema.id ?? `schema${this.counter++}`;
entry[1].defId = id;
return { defId: id, ref: `${params.external.uri("__shared")}#/${defsSegment}/${id}` };
}
+ if (entry[1] === root) {
+ return { ref: "#" };
+ }
+
// self-contained schema
const uriPrefix = `#`;
const defUriPrefix = `${uriPrefix}/${defsSegment}/`;
const defId = entry[1].schema.id ?? `__schema${this.counter++}`;
-
return { defId, ref: defUriPrefix + defId };
};
+ const extractToDef = (entry: [schemas.$ZodType, Seen]): void => {
+ if (entry[1].schema.$ref) {
+ // throw new Error("Already extracted");
+ return;
+ }
+ const seen = entry[1];
+ const { ref, defId } = makeURI(entry);
+ // defId won't be set if the schema is a reference to an external schema
+
+ seen.def = { ...seen.schema };
+ if (defId) seen.defId = defId;
+ // wipe away all properties except $ref
+ schemaToRef(seen, ref);
+ };
+
+ // extract schemas into $defs
for (const entry of this.seen.entries()) {
const seen = entry[1];
+
+ // convert root schema to # $ref
+ // also prevents root schema from being extracted
if (schema === entry[0]) {
- schemaToRef({
- schema: seen.schema,
- ref: "#",
- });
+ // do not copy to defs...this is the root schema
+ extractToDef(entry);
continue;
}
- // external
+ // extract schemas that are in the external registry
if (params.external) {
const ext = params.external.registry.get(entry[0])?.id;
if (schema !== entry[0] && ext) {
- // does not write to defs
- const { ref, defId } = makeURI(entry); // params.external.uri(ext);
- if (defId) defs[defId] = { ...seen.cached };
- schemaToRef({
- schema: seen.schema,
- ref,
- });
+ extractToDef(entry);
continue;
}
}
- // handle schemas with `id`
+ // extract schemas with `id` meta
const id = this.metadataRegistry.get(entry[0])?.id;
if (id) {
- const { ref, defId } = makeURI(entry);
- if (defId) defs[defId] = { ...seen.cached };
- schemaToRef({
- schema: seen.schema,
- ref,
- });
+ extractToDef(entry);
+
continue;
}
- // handle cycles
+ // break cycles
if (seen.cycle) {
if (params.cycles === "throw") {
throw new Error(
@@ -621,31 +655,37 @@ export class JSONSchemaGenerator {
'\n\nSet the `cycles` parameter to `"ref"` to resolve cyclical schemas with defs.'
);
} else if (params.cycles === "ref") {
- const { ref, defId } = makeURI(entry); // schema === entry[0] ? "#" : defUriPrefix + id;
- if (defId) defs[defId] = { ...seen.cached };
- schemaToRef({
- schema: seen.schema,
- ref,
- });
+ extractToDef(entry);
}
continue;
}
- // handle reused schemas
+ // extract reused schemas
if (seen.count > 1) {
if (params.reused === "ref") {
- const { ref, defId } = makeURI(entry);
- if (defId) defs[defId] = { ...seen.cached };
- schemaToRef({
- schema: seen.schema,
- ref,
- });
+ extractToDef(entry);
// biome-ignore lint:
continue;
}
}
}
+ for (const entry of this.seen.entries()) {
+ const seen = entry[1];
+ flattenRef(seen.schema);
+ if (seen.def) flattenRef(seen.def);
+ }
+
+ const result = { ...root.def };
+ const defs: JSONSchema.BaseSchema["$defs"] = params.external?.defs ?? {};
+ for (const entry of this.seen.entries()) {
+ const seen = entry[1];
+ if (seen.def && seen.defId) {
+ defs[seen.defId] = seen.def;
+ }
+ }
+
+ // set definitions in result
if (!params.external && Object.keys(defs).length > 0) {
if (this.target === "draft-2020-12") {
result.$defs = defs;
@@ -660,17 +700,23 @@ export class JSONSchemaGenerator {
// though the seen map is shared
return JSON.parse(JSON.stringify(result));
} catch (_err) {
- console.log(_err);
throw new Error("Error converting schema to JSON.");
}
}
}
-function schemaToRef(params: {
- schema: JSONSchema.BaseSchema;
- ref: string;
-}) {
- const { schema, ref } = params;
+// flatten _refs
+const flattenRef = (schema: JSONSchema.BaseSchema) => {
+ const _schema = { ...schema };
+ if (schema._ref) flattenRef(schema._ref);
+ const ref = schema._ref;
+ Object.assign(schema, ref);
+ Object.assign(schema, _schema); // this is to prevent mutation of the original schema
+ delete schema._ref;
+};
+
+function schemaToRef(seen: Seen, ref: string) {
+ const schema = seen.schema;
for (const key in schema) {
delete schema[key];
schema.$ref = ref;
@@ -726,6 +772,5 @@ export function toJSONSchema(
const gen = new JSONSchemaGenerator(_params);
gen.process(input);
- // console.log(gen.seen);
return gen.emit(input, _params);
}
diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts
index 2e34755ea2..3d7e5d83a9 100644
--- a/packages/core/src/util.ts
+++ b/packages/core/src/util.ts
@@ -395,8 +395,10 @@ export function escapeRegex(str: string): string {
}
// zod-specific utils
-export function clone(inst: T, def: T["_zod"]["def"]): T {
- return new inst._zod.constr(def);
+export function clone(inst: T, def?: T["_zod"]["def"]): T {
+ const cl = new inst._zod.constr(def ?? inst._zod.def);
+ if (!def) cl._zod.parent = inst;
+ return cl as any;
}
export type Params<
diff --git a/packages/docs/content/error-customization.mdx b/packages/docs/content/error-customization.mdx
index f13e683ea4..5795950067 100644
--- a/packages/docs/content/error-customization.mdx
+++ b/packages/docs/content/error-customization.mdx
@@ -382,6 +382,7 @@ Below is a quick reference for determining error precedence: if multiple error c
z.config({
customError: (iss) => "My custom error"
});
+ ```
4. **Locale error map** — A custom error map passed into `z.config()`.
diff --git a/packages/mini/src/schemas.ts b/packages/mini/src/schemas.ts
index 4f5cfe63e4..ad829375f9 100644
--- a/packages/mini/src/schemas.ts
+++ b/packages/mini/src/schemas.ts
@@ -55,7 +55,7 @@ export const ZodMiniType: core.$constructor = /*@__PURE__*/ core.$c
],
});
};
- inst.clone = (_def) => core.clone(inst, _def ?? def);
+ inst.clone = (_def) => core.clone(inst, _def);
inst.brand = () => inst as any;
inst.register = ((reg: any, meta: any) => {
reg.add(inst, meta);
diff --git a/packages/zod/src/schemas.ts b/packages/zod/src/schemas.ts
index 9e2854ff8e..1811bfc9cf 100644
--- a/packages/zod/src/schemas.ts
+++ b/packages/zod/src/schemas.ts
@@ -113,7 +113,7 @@ export const ZodType: core.$constructor = /*@__PURE__*/ core.$construct
],
});
};
- inst.clone = (_def) => core.clone(inst, _def ?? def);
+ inst.clone = (_def) => core.clone(inst, _def);
inst.brand = () => inst as any;
inst.register = ((reg: any, meta: any) => {
reg.add(inst, meta);
@@ -150,8 +150,8 @@ export const ZodType: core.$constructor = /*@__PURE__*/ core.$construct
// meta
inst.describe = (description) => {
const cl = inst.clone();
- const meta = core.globalRegistry.get(inst) ?? {};
- meta.description = description;
+ const meta = { ...(core.globalRegistry.get(inst) ?? {}), description };
+ delete meta.id; // do not inherit
core.globalRegistry.add(cl, meta);
return cl;
};
diff --git a/packages/zod/tests/json-schema.test.ts b/packages/zod/tests/json-schema.test.ts
index 67b3fe7179..103473a9ad 100644
--- a/packages/zod/tests/json-schema.test.ts
+++ b/packages/zod/tests/json-schema.test.ts
@@ -1029,3 +1029,301 @@ test("unrepresentable literal values are ignored", () => {
const c = z.toJSONSchema(z.literal([undefined]), { unrepresentable: "any" });
expect(c).toMatchInlineSnapshot(`{}`);
});
+
+test("describe with id", () => {
+ const jobId = z.string().meta({ id: "jobId" });
+
+ const a = z.toJSONSchema(
+ z.object({
+ current: jobId.describe("Current job"),
+ previous: jobId.describe("Previous job"),
+ })
+ );
+ expect(a).toMatchInlineSnapshot(`
+ {
+ "$defs": {
+ "jobId": {
+ "id": "jobId",
+ "type": "string",
+ },
+ },
+ "properties": {
+ "current": {
+ "$ref": "#/$defs/jobId",
+ "description": "Current job",
+ },
+ "previous": {
+ "$ref": "#/$defs/jobId",
+ "description": "Previous job",
+ },
+ },
+ "required": [
+ "current",
+ "previous",
+ ],
+ "type": "object",
+ }
+ `);
+});
+
+test("overwrite id", () => {
+ const jobId = z.string().meta({ id: "aaa" });
+
+ const a = z.toJSONSchema(
+ z.object({
+ current: jobId,
+ previous: jobId.meta({ id: "bbb" }),
+ })
+ );
+ expect(a).toMatchInlineSnapshot(`
+ {
+ "$defs": {
+ "aaa": {
+ "id": "aaa",
+ "type": "string",
+ },
+ "bbb": {
+ "$ref": "#/$defs/aaa",
+ "id": "bbb",
+ },
+ },
+ "properties": {
+ "current": {
+ "$ref": "#/$defs/aaa",
+ },
+ "previous": {
+ "$ref": "#/$defs/bbb",
+ },
+ },
+ "required": [
+ "current",
+ "previous",
+ ],
+ "type": "object",
+ }
+ `);
+
+ const b = z.toJSONSchema(
+ z.object({
+ current: jobId,
+ previous: jobId.meta({ id: "ccc" }),
+ }),
+ {
+ reused: "ref",
+ }
+ );
+ expect(b).toMatchInlineSnapshot(`
+ {
+ "$defs": {
+ "aaa": {
+ "id": "aaa",
+ "type": "string",
+ },
+ "ccc": {
+ "$ref": "#/$defs/aaa",
+ "id": "ccc",
+ },
+ },
+ "properties": {
+ "current": {
+ "$ref": "#/$defs/aaa",
+ },
+ "previous": {
+ "$ref": "#/$defs/ccc",
+ },
+ },
+ "required": [
+ "current",
+ "previous",
+ ],
+ "type": "object",
+ }
+ `);
+});
+
+test("overwrite descriptions", () => {
+ const field = z.string().describe("a").describe("b").describe("c");
+
+ const a = z.toJSONSchema(
+ z.object({
+ d: field.describe("d"),
+ e: field.describe("e"),
+ })
+ );
+ expect(a).toMatchInlineSnapshot(`
+ {
+ "properties": {
+ "d": {
+ "description": "d",
+ "type": "string",
+ },
+ "e": {
+ "description": "e",
+ "type": "string",
+ },
+ },
+ "required": [
+ "d",
+ "e",
+ ],
+ "type": "object",
+ }
+ `);
+
+ const b = z.toJSONSchema(
+ z.object({
+ d: field.describe("d"),
+ e: field.describe("e"),
+ }),
+ {
+ reused: "ref",
+ }
+ );
+ expect(a).toMatchInlineSnapshot(`
+ {
+ "properties": {
+ "d": {
+ "description": "d",
+ "type": "string",
+ },
+ "e": {
+ "description": "e",
+ "type": "string",
+ },
+ },
+ "required": [
+ "d",
+ "e",
+ ],
+ "type": "object",
+ }
+ `);
+});
+
+test("top-level readonly", () => {
+ const A = z
+ .interface({
+ name: z.string(),
+ get b() {
+ return B;
+ },
+ })
+ .readonly()
+ .meta({ id: "A" });
+
+ const B = z
+ .interface({
+ name: z.string(),
+ get a() {
+ return A;
+ },
+ })
+ .readonly()
+ .meta({ id: "B" });
+
+ const result = z.toJSONSchema(A);
+ expect(result).toMatchInlineSnapshot(`
+ {
+ "$defs": {
+ "B": {
+ "id": "B",
+ "properties": {
+ "a": {
+ "$ref": "#",
+ },
+ "name": {
+ "type": "string",
+ },
+ },
+ "readOnly": true,
+ "required": [
+ "name",
+ "a",
+ ],
+ "type": "object",
+ },
+ },
+ "id": "A",
+ "properties": {
+ "b": {
+ "$ref": "#/$defs/B",
+ },
+ "name": {
+ "type": "string",
+ },
+ },
+ "readOnly": true,
+ "required": [
+ "name",
+ "b",
+ ],
+ "type": "object",
+ }
+ `);
+});
+
+test("basic registry", () => {
+ const myRegistry = z.registry<{ id: string }>();
+ const User = z.interface({
+ name: z.string(),
+ get posts() {
+ return z.array(Post);
+ },
+ });
+
+ const Post = z.interface({
+ title: z.string(),
+ content: z.string(),
+ get author() {
+ return User;
+ },
+ });
+
+ myRegistry.add(User, { id: "User" });
+ myRegistry.add(Post, { id: "Post" });
+
+ const result = z.toJSONSchema(myRegistry);
+ expect(result).toMatchInlineSnapshot(`
+ {
+ "schemas": {
+ "Post": {
+ "properties": {
+ "author": {
+ "$ref": "User",
+ },
+ "content": {
+ "type": "string",
+ },
+ "title": {
+ "type": "string",
+ },
+ },
+ "required": [
+ "title",
+ "content",
+ "author",
+ ],
+ "type": "object",
+ },
+ "User": {
+ "properties": {
+ "name": {
+ "type": "string",
+ },
+ "posts": {
+ "items": {
+ "$ref": "Post",
+ },
+ "type": "array",
+ },
+ },
+ "required": [
+ "name",
+ "posts",
+ ],
+ "type": "object",
+ },
+ },
+ }
+ `);
+});
diff --git a/play.ts b/play.ts
index 8e4f967000..d508ee4467 100644
--- a/play.ts
+++ b/play.ts
@@ -1,2 +1,41 @@
import * as z from "zod";
z;
+
+// const a = z.string().meta({ id: "jobId", description: "a" });
+// const b = a.describe("b");
+// const c = b.describe("c");
+// const d = c.describe("d");
+
+// const result = z.toJSONSchema(
+// z.object({
+// a,
+// b,
+// b2: b,
+// c,
+// d,
+// })
+// );
+// console.log(JSON.stringify(result, null, 2));
+
+// import * as z from "zod";
+
+const User = z.interface({
+ name: z.string(),
+ get posts() {
+ return z.array(Post);
+ },
+});
+
+const Post = z.interface({
+ title: z.string(),
+ content: z.string(),
+ get author() {
+ return User;
+ },
+});
+
+z.globalRegistry.add(User, { id: "User" });
+z.globalRegistry.add(Post, { id: "Post" });
+
+const reg = z.toJSONSchema(z.globalRegistry);
+console.dir(reg, { depth: 10 });
diff --git a/vitest.root.mts b/vitest.root.mts
index ecb336114d..d9cf889a8f 100644
--- a/vitest.root.mts
+++ b/vitest.root.mts
@@ -6,7 +6,7 @@ export default defineConfig({
},
test: {
watch: false,
- isolate: false,
+ isolate: true,
typecheck: {
include: ["**/*.test.ts"],
enabled: true,