agent_patch
runs/2026-02-27__21-30-28__gpt-5-4/flux-pr-4811/flux-pr-4811.1-of-1.2026-02-27__21-30-28__gpt-5-4/agent-logs/agent.patch
15956 bytes
diff --git a/app/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json b/app/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
new file mode 100644
index 0000000..1fe4d70
--- /dev/null
+++ b/app/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
@@ -0,0 +1 @@
+{"version":"4.0.18","results":[[":packages/zod/src/v4/classic/tests/to-json-schema.test.ts",{"duration":0,"failed":true}]]}
\ No newline at end of file
diff --git a/tmp/agent-patch-flux-pr-4811.1-of-1.2026-02-27__21-30-28__gpt-5-4/app/packages/docs/content/json-schema.mdx b/app/packages/docs/content/json-schema.mdx
index 958aa6d..192c26e 100644
--- a/tmp/agent-patch-flux-pr-4811.1-of-1.2026-02-27__21-30-28__gpt-5-4/app/packages/docs/content/json-schema.mdx
+++ b/app/packages/docs/content/json-schema.mdx
@@ -214,8 +214,9 @@ Below is a quick reference for each supported parameter. Each one is explained i
interface ToJSONSchemaParams {
/** The JSON Schema version to target.
* - `"draft-2020-12"` — Default. JSON Schema Draft 2020-12
- * - `"draft-7"` — JSON Schema Draft 7 */
- target?: "draft-7" | "draft-2020-12";
+ * - `"draft-7"` — JSON Schema Draft 7
+ * - `"draft-4"` — JSON Schema Draft 4 */
+ target?: "draft-4" | "draft-7" | "draft-2020-12";
/** A registry used to look up metadata for each schema.
* Any schema with an `id` property will be extracted as a $def. */
@@ -248,10 +249,13 @@ interface ToJSONSchemaParams {
To set the target JSON Schema version, use the `target` parameter. By default, Zod will target Draft 2020-12.
```ts
+z.toJSONSchema(schema, { target: "draft-4" });
z.toJSONSchema(schema, { target: "draft-7" });
z.toJSONSchema(schema, { target: "draft-2020-12" });
```
+When targeting `"draft-4"`, Zod adjusts output for draft-04's older keyword set. For example, exclusive numeric bounds are emitted using boolean `exclusiveMinimum`/`exclusiveMaximum`, single-value literals use `enum` instead of `const`, and record key constraints fall back to the closest draft-04-compatible representation.
+
### `metadata`
> If you haven't already, read through the [Metadata and registries](/metadata) page for context on storing metadata in Zod.
diff --git a/tmp/agent-patch-flux-pr-4811.1-of-1.2026-02-27__21-30-28__gpt-5-4/app/packages/zod/src/v4/classic/tests/to-json-schema.test.ts b/app/packages/zod/src/v4/classic/tests/to-json-schema.test.ts
index f6e18bb..f7f6987 100644
--- a/tmp/agent-patch-flux-pr-4811.1-of-1.2026-02-27__21-30-28__gpt-5-4/app/packages/zod/src/v4/classic/tests/to-json-schema.test.ts
+++ b/app/packages/zod/src/v4/classic/tests/to-json-schema.test.ts
@@ -439,6 +439,30 @@ describe("toJSONSchema", () => {
"type": "string",
}
`);
+
+ expect(
+ z.toJSONSchema(
+ z.string().startsWith("hello").endsWith("world"),
+ {
+ target: "draft-4",
+ }
+ )
+ ).toMatchInlineSnapshot(`
+ {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "allOf": [
+ {
+ "pattern": "^hello.*",
+ "type": "string",
+ },
+ {
+ "pattern": ".*world$",
+ "type": "string",
+ },
+ ],
+ "type": "string",
+ }
+ `);
});
test("number constraints", () => {
@@ -537,6 +561,25 @@ describe("toJSONSchema", () => {
"type": "number",
}
`);
+
+ expect(z.toJSONSchema(z.number().gt(5).lt(10), { target: "draft-4" })).toMatchInlineSnapshot(`
+ {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "exclusiveMaximum": true,
+ "exclusiveMinimum": true,
+ "maximum": 10,
+ "minimum": 5,
+ "type": "number",
+ }
+ `);
+
+ expect(z.toJSONSchema(z.number().gt(5).gte(10), { target: "draft-4" })).toMatchInlineSnapshot(`
+ {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "minimum": 10,
+ "type": "number",
+ }
+ `);
});
test("arrays", () => {
@@ -618,6 +661,19 @@ describe("toJSONSchema", () => {
"type": "object",
}
`);
+
+ expect(z.toJSONSchema(z.record(z.string().regex(/^foo/), z.boolean()), { target: "draft-4" })).toMatchInlineSnapshot(`
+ {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^foo": {
+ "type": "boolean",
+ },
+ },
+ "type": "object",
+ }
+ `);
});
test("tuple", () => {
@@ -706,6 +762,15 @@ describe("toJSONSchema", () => {
"type": "string",
}
`);
+ expect(z.toJSONSchema(a, { target: "draft-4" })).toMatchInlineSnapshot(`
+ {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "enum": [
+ "hello",
+ ],
+ "type": "string",
+ }
+ `);
const b = z.literal(7);
expect(z.toJSONSchema(b)).toMatchInlineSnapshot(`
diff --git a/tmp/agent-patch-flux-pr-4811.1-of-1.2026-02-27__21-30-28__gpt-5-4/app/packages/zod/src/v4/core/json-schema.ts b/app/packages/zod/src/v4/core/json-schema.ts
index 4b3abfb..55d7cfd 100644
--- a/tmp/agent-patch-flux-pr-4811.1-of-1.2026-02-27__21-30-28__gpt-5-4/app/packages/zod/src/v4/core/json-schema.ts
+++ b/app/packages/zod/src/v4/core/json-schema.ts
@@ -45,7 +45,10 @@ export type Schema =
export type _JSONSchema = boolean | JSONSchema;
export type JSONSchema = {
[k: string]: unknown;
- $schema?: "https://json-schema.org/draft/2020-12/schema" | "http://json-schema.org/draft-07/schema#";
+ $schema?:
+ | "https://json-schema.org/draft/2020-12/schema"
+ | "http://json-schema.org/draft-07/schema#"
+ | "http://json-schema.org/draft-04/schema#";
$id?: string;
$anchor?: string;
$ref?: string;
@@ -54,6 +57,7 @@ export type JSONSchema = {
$vocabulary?: Record<string, boolean>;
$comment?: string;
$defs?: Record<string, JSONSchema>;
+ definitions?: Record<string, JSONSchema>;
type?: "object" | "array" | "string" | "number" | "boolean" | "null" | "integer";
additionalItems?: _JSONSchema;
unevaluatedItems?: _JSONSchema;
@@ -75,9 +79,9 @@ export type JSONSchema = {
not?: _JSONSchema;
multipleOf?: number;
maximum?: number;
- exclusiveMaximum?: number;
+ exclusiveMaximum?: number | boolean;
minimum?: number;
- exclusiveMinimum?: number;
+ exclusiveMinimum?: number | boolean;
maxLength?: number;
minLength?: number;
pattern?: string;
diff --git a/tmp/agent-patch-flux-pr-4811.1-of-1.2026-02-27__21-30-28__gpt-5-4/app/packages/zod/src/v4/core/to-json-schema.ts b/app/packages/zod/src/v4/core/to-json-schema.ts
index 001f195..d61716e 100644
--- a/tmp/agent-patch-flux-pr-4811.1-of-1.2026-02-27__21-30-28__gpt-5-4/app/packages/zod/src/v4/core/to-json-schema.ts
+++ b/app/packages/zod/src/v4/core/to-json-schema.ts
@@ -10,8 +10,9 @@ interface JSONSchemaGeneratorParams {
metadata?: $ZodRegistry<Record<string, any>>;
/** The JSON Schema version to target.
* - `"draft-2020-12"` — Default. JSON Schema Draft 2020-12
- * - `"draft-7"` — JSON Schema Draft 7 */
- target?: "draft-7" | "draft-2020-12";
+ * - `"draft-7"` — JSON Schema Draft 7
+ * - `"draft-4"` — JSON Schema Draft 4 */
+ target?: "draft-4" | "draft-7" | "draft-2020-12";
/** How to handle unrepresentable types.
* - `"throw"` — Default. Unrepresentable types throw an error
* - `"any"` — Unrepresentable types become `{}` */
@@ -71,7 +72,7 @@ interface Seen {
export class JSONSchemaGenerator {
metadataRegistry: $ZodRegistry<Record<string, any>>;
- target: "draft-7" | "draft-2020-12";
+ target: "draft-4" | "draft-7" | "draft-2020-12";
unrepresentable: "throw" | "any";
override: (ctx: {
zodSchema: schemas.$ZodTypes;
@@ -93,6 +94,66 @@ export class JSONSchemaGenerator {
this.seen = new Map();
}
+ private targetUsesDefs() {
+ return this.target === "draft-2020-12";
+ }
+
+ private isLegacyTarget() {
+ return this.target !== "draft-2020-12";
+ }
+
+ private setNumericBound(
+ json: JSONSchema.NumberSchema | JSONSchema.IntegerSchema,
+ params:
+ | { kind: "minimum"; minimum?: number; exclusiveMinimum?: number }
+ | { kind: "maximum"; maximum?: number; exclusiveMaximum?: number }
+ ) {
+ if (params.kind === "minimum") {
+ if (this.target === "draft-4") {
+ let minimum = params.minimum;
+ let exclusive = false;
+ if (typeof params.exclusiveMinimum === "number" && (minimum === undefined || params.exclusiveMinimum >= minimum)) {
+ minimum = params.exclusiveMinimum;
+ exclusive = true;
+ }
+ if (typeof minimum === "number") json.minimum = minimum;
+ if (exclusive) json.exclusiveMinimum = true;
+ return;
+ }
+
+ if (typeof params.exclusiveMinimum === "number") json.exclusiveMinimum = params.exclusiveMinimum;
+ if (typeof params.minimum === "number") {
+ json.minimum = params.minimum;
+ if (typeof params.exclusiveMinimum === "number") {
+ if (params.exclusiveMinimum >= params.minimum) delete json.minimum;
+ else delete json.exclusiveMinimum;
+ }
+ }
+ return;
+ }
+
+ if (this.target === "draft-4") {
+ let maximum = params.maximum;
+ let exclusive = false;
+ if (typeof params.exclusiveMaximum === "number" && (maximum === undefined || params.exclusiveMaximum <= maximum)) {
+ maximum = params.exclusiveMaximum;
+ exclusive = true;
+ }
+ if (typeof maximum === "number") json.maximum = maximum;
+ if (exclusive) json.exclusiveMaximum = true;
+ return;
+ }
+
+ if (typeof params.exclusiveMaximum === "number") json.exclusiveMaximum = params.exclusiveMaximum;
+ if (typeof params.maximum === "number") {
+ json.maximum = params.maximum;
+ if (typeof params.exclusiveMaximum === "number") {
+ if (params.exclusiveMaximum <= params.maximum) delete json.maximum;
+ else delete json.exclusiveMaximum;
+ }
+ }
+ }
+
process(schema: schemas.$ZodType, _params: ProcessParams = { path: [], schemaPath: [] }): JSONSchema.BaseSchema {
const def = (schema as schemas.$ZodTypes)._zod.def;
@@ -163,7 +224,7 @@ export class JSONSchemaGenerator {
else if (regexes.length > 1) {
result.schema.allOf = [
...regexes.map((regex) => ({
- ...(this.target === "draft-7" ? ({ type: "string" } as const) : {}),
+ ...(this.isLegacyTarget() ? ({ type: "string" } as const) : {}),
pattern: regex.source,
})),
];
@@ -178,23 +239,8 @@ export class JSONSchemaGenerator {
if (typeof format === "string" && format.includes("int")) json.type = "integer";
else json.type = "number";
- if (typeof exclusiveMinimum === "number") json.exclusiveMinimum = exclusiveMinimum;
- if (typeof minimum === "number") {
- json.minimum = minimum;
- if (typeof exclusiveMinimum === "number") {
- if (exclusiveMinimum >= minimum) delete json.minimum;
- else delete json.exclusiveMinimum;
- }
- }
-
- if (typeof exclusiveMaximum === "number") json.exclusiveMaximum = exclusiveMaximum;
- if (typeof maximum === "number") {
- json.maximum = maximum;
- if (typeof exclusiveMaximum === "number") {
- if (exclusiveMaximum <= maximum) delete json.maximum;
- else delete json.exclusiveMaximum;
- }
- }
+ this.setNumericBound(json, { kind: "minimum", minimum, exclusiveMinimum });
+ this.setNumericBound(json, { kind: "maximum", maximum, exclusiveMaximum });
if (typeof multipleOf === "number") json.multipleOf = multipleOf;
@@ -378,12 +424,39 @@ export class JSONSchemaGenerator {
}
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, {
+ const valueSchema = this.process(def.valueType, {
...params,
path: [...params.path, "additionalProperties"],
});
+ json.type = "object";
+ const keySchema = this.process(def.keyType, {
+ ...params,
+ path: [...params.path, "propertyNames"],
+ });
+
+ if (this.target !== "draft-4") {
+ json.propertyNames = keySchema;
+ json.additionalProperties = valueSchema;
+ break;
+ }
+
+ const keyEnum = Array.isArray(keySchema.enum) && keySchema.enum.every((v) => typeof v === "string") ? keySchema.enum : undefined;
+ const keySchemaKeys = Object.keys(keySchema);
+
+ if (
+ typeof keySchema.pattern === "string" &&
+ keySchemaKeys.every((key) => key === "type" || key === "pattern")
+ ) {
+ json.patternProperties = {
+ [keySchema.pattern]: valueSchema,
+ };
+ json.additionalProperties = false;
+ } else if (keyEnum) {
+ json.properties = Object.fromEntries(keyEnum.map((key) => [key, valueSchema]));
+ json.additionalProperties = false;
+ } else {
+ json.additionalProperties = valueSchema;
+ }
break;
}
case "map": {
@@ -432,7 +505,8 @@ export class JSONSchemaGenerator {
} else if (vals.length === 1) {
const val = vals[0]!;
json.type = val === null ? ("null" as const) : (typeof val as any);
- json.const = val;
+ if (this.target === "draft-4") json.enum = [val];
+ else json.const = val;
} else {
if (vals.every((v) => typeof v === "number")) json.type = "number";
if (vals.every((v) => typeof v === "string")) json.type = "string";
@@ -622,7 +696,7 @@ export class JSONSchemaGenerator {
// e.g. lazy
// external is configured
- const defsSegment = this.target === "draft-2020-12" ? "$defs" : "definitions";
+ const defsSegment = this.targetUsesDefs() ? "$defs" : "definitions";
if (params.external) {
const externalId = params.external.registry.get(entry[0])?.id; // ?? "__shared";// `__schema${this.counter++}`;
@@ -749,7 +823,7 @@ export class JSONSchemaGenerator {
// merge referenced schema into current
const refSchema = this.seen.get(ref)!.schema;
- if (refSchema.$ref && params.target === "draft-7") {
+ if (refSchema.$ref && params.target !== "draft-2020-12") {
schema.allOf = schema.allOf ?? [];
schema.allOf.push(refSchema);
} else {
@@ -776,6 +850,8 @@ export class JSONSchemaGenerator {
result.$schema = "https://json-schema.org/draft/2020-12/schema";
} else if (this.target === "draft-7") {
result.$schema = "http://json-schema.org/draft-07/schema#";
+ } else if (this.target === "draft-4") {
+ result.$schema = "http://json-schema.org/draft-04/schema#";
} else {
// @ts-ignore
console.warn(`Invalid target: ${this.target}`);
@@ -802,7 +878,7 @@ export class JSONSchemaGenerator {
if (params.external) {
} else {
if (Object.keys(defs).length > 0) {
- if (this.target === "draft-2020-12") {
+ if (this.targetUsesDefs()) {
result.$defs = defs;
} else {
result.definitions = defs;