STET

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

Back to adjudication
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;