Repository: colinhacks/zod
Pull request #4390 (2025-05-17 07:33:26)

Add `$schema` in `z.toJSONSchema`
Closes https://github.com/colinhacks/zod/issues/4299

coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

<details open="true">
<summary>🧪 Generate Unit Tests</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Create PR with Unit Tests
- [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Commit Unit Tests in branch `v4-jsonschema-target`
- [ ] <!-- {"checkboxId": "07f1e7d6-8a8e-4e23-9900-8731c2c87f58", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Post Copyable Unit Tests in Comment

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=colinhacks/zod&utm_content=4390):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai generate unit tests` to generate unit tests for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Patch:
diff --git a/packages/zod/src/v4/core/to-json-schema.ts b/packages/zod/src/v4/core/to-json-schema.ts
index 44a18be8..212adf2f 100644
--- a/packages/zod/src/v4/core/to-json-schema.ts
+++ b/packages/zod/src/v4/core/to-json-schema.ts
@@ -711,6 +711,14 @@ export class JSONSchemaGenerator {
       }
     }
 
+    if (this.target === "draft-2020-12") {
+      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 {
+      console.warn(`Invalid target: ${this.target}`);
+    }
+
     try {
       // this "finalizes" this schema and ensures all cycles are removed
       // each call to .emit() is functionally independent
diff --git a/play.ts b/play.ts
index f68ffda0..3ed27a13 100644
--- a/play.ts
+++ b/play.ts
@@ -2,4 +2,4 @@ import { z } from "zod/v4";
 
 z;
 
-z.instanceof(z.ZodType);
+z.number().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER);


Test patch:
diff --git a/packages/zod/src/v4/classic/tests/json-schema.test.ts b/packages/zod/src/v4/classic/tests/json-schema.test.ts
index 9d01baef..0ffd1575 100644
--- a/packages/zod/src/v4/classic/tests/json-schema.test.ts
+++ b/packages/zod/src/v4/classic/tests/json-schema.test.ts
@@ -6,38 +6,53 @@ describe("toJSONSchema", () => {
   test("primitive types", () => {
     expect(toJSONSchema(z.string())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "type": "string",
       }
     `);
     expect(toJSONSchema(z.number())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "type": "number",
       }
     `);
     expect(toJSONSchema(z.boolean())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "type": "boolean",
       }
     `);
     expect(toJSONSchema(z.null())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "type": "null",
       }
     `);
     expect(toJSONSchema(z.undefined())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "type": "null",
       }
     `);
-    expect(toJSONSchema(z.any())).toMatchInlineSnapshot(`{}`);
-    expect(toJSONSchema(z.unknown())).toMatchInlineSnapshot(`{}`);
+    expect(toJSONSchema(z.any())).toMatchInlineSnapshot(`
+      {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
+      }
+    `);
+    expect(toJSONSchema(z.unknown())).toMatchInlineSnapshot(`
+      {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
+      }
+    `);
     expect(toJSONSchema(z.never())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "not": {},
       }
     `);
     expect(toJSONSchema(z.email())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "email",
         "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
         "type": "string",
@@ -45,6 +60,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.iso.datetime())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "date-time",
         "pattern": "^((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z)$",
         "type": "string",
@@ -52,6 +68,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.iso.date())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "date",
         "pattern": "^((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))$",
         "type": "string",
@@ -59,6 +76,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.iso.time())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "time",
         "pattern": "^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?$",
         "type": "string",
@@ -66,6 +84,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.iso.duration())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "duration",
         "pattern": "^P(?:(\\d+W)|(?!.*W)(?=\\d|T\\d)(\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+([.,]\\d+)?S)?)?)$",
         "type": "string",
@@ -73,6 +92,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.ipv4())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "ipv4",
         "pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$",
         "type": "string",
@@ -80,6 +100,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.ipv6())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "ipv6",
         "pattern": "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})$",
         "type": "string",
@@ -87,6 +108,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.uuid())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "uuid",
         "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$",
         "type": "string",
@@ -94,6 +116,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.guid())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "uuid",
         "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$",
         "type": "string",
@@ -101,12 +124,14 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.url())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "uri",
         "type": "string",
       }
     `);
     expect(toJSONSchema(z.base64())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "contentEncoding": "base64",
         "format": "base64",
         "pattern": "^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$",
@@ -115,6 +140,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.cuid())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "cuid",
         "pattern": "^[cC][^\\s-]{8,}$",
         "type": "string",
@@ -123,6 +149,7 @@ describe("toJSONSchema", () => {
     // expect(toJSONSchema(z.regex(/asdf/))).toMatchInlineSnapshot();
     expect(toJSONSchema(z.emoji())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "emoji",
         "pattern": "^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$",
         "type": "string",
@@ -130,6 +157,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.nanoid())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "nanoid",
         "pattern": "^[a-zA-Z0-9_-]{21}$",
         "type": "string",
@@ -137,6 +165,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.cuid2())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "cuid2",
         "pattern": "^[0-9a-z]+$",
         "type": "string",
@@ -144,6 +173,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.ulid())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "ulid",
         "pattern": "^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$",
         "type": "string",
@@ -152,11 +182,13 @@ describe("toJSONSchema", () => {
     // expect(toJSONSchema(z.cidr())).toMatchInlineSnapshot();
     expect(toJSONSchema(z.number())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "type": "number",
       }
     `);
     expect(toJSONSchema(z.int())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "maximum": 9007199254740991,
         "minimum": -9007199254740991,
         "type": "integer",
@@ -164,6 +196,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.int32())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "maximum": 2147483647,
         "minimum": -2147483648,
         "type": "integer",
@@ -171,6 +204,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.float32())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "maximum": 3.4028234663852886e+38,
         "minimum": -3.4028234663852886e+38,
         "type": "number",
@@ -178,6 +212,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.float64())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "maximum": 1.7976931348623157e+308,
         "minimum": -1.7976931348623157e+308,
         "type": "number",
@@ -185,6 +220,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.jwt())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "jwt",
         "type": "string",
       }
@@ -213,6 +249,7 @@ describe("toJSONSchema", () => {
     const staticCatchSchema = z.string().catch(() => "sup");
     expect(toJSONSchema(staticCatchSchema)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "default": "sup",
         "type": "string",
       }
@@ -226,6 +263,7 @@ describe("toJSONSchema", () => {
   test("string formats", () => {
     expect(toJSONSchema(z.string().email())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "email",
         "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
         "type": "string",
@@ -233,6 +271,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.string().uuid())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "uuid",
         "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$",
         "type": "string",
@@ -240,6 +279,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.iso.datetime())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "date-time",
         "pattern": "^((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z)$",
         "type": "string",
@@ -248,6 +288,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(z.iso.date())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "date",
         "pattern": "^((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))$",
         "type": "string",
@@ -255,6 +296,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.iso.time())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "time",
         "pattern": "^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?$",
         "type": "string",
@@ -262,6 +304,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.iso.duration())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "duration",
         "pattern": "^P(?:(\\d+W)|(?!.*W)(?=\\d|T\\d)(\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+([.,]\\d+)?S)?)?)$",
         "type": "string",
@@ -275,6 +318,7 @@ describe("toJSONSchema", () => {
     // `);
     expect(toJSONSchema(z.ipv4())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "ipv4",
         "pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$",
         "type": "string",
@@ -283,6 +327,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(z.ipv6())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "ipv6",
         "pattern": "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})$",
         "type": "string",
@@ -291,6 +336,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(z.base64())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "contentEncoding": "base64",
         "format": "base64",
         "pattern": "^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$",
@@ -299,12 +345,14 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.url())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "uri",
         "type": "string",
       }
     `);
     expect(toJSONSchema(z.guid())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "uuid",
         "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$",
         "type": "string",
@@ -312,6 +360,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.string().regex(/asdf/))).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "format": "regex",
         "pattern": "asdf",
         "type": "string",
@@ -323,6 +372,7 @@ describe("toJSONSchema", () => {
     expect(toJSONSchema(z.number().min(5).max(10))).toMatchInlineSnapshot(
       `
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "maximum": 10,
         "minimum": 5,
         "type": "number",
@@ -332,6 +382,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(z.number().gt(5).gt(10))).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "exclusiveMinimum": 10,
         "type": "number",
       }
@@ -339,6 +390,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(z.number().gt(5).gte(10))).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "minimum": 10,
         "type": "number",
       }
@@ -346,6 +398,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(z.number().lt(5).lt(3))).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "exclusiveMaximum": 3,
         "type": "number",
       }
@@ -353,6 +406,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(z.number().lt(5).lt(3).lte(2))).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "maximum": 2,
         "type": "number",
       }
@@ -360,6 +414,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(z.number().lt(5).lte(3))).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "maximum": 3,
         "type": "number",
       }
@@ -367,6 +422,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(z.number().gt(5).lt(10))).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "exclusiveMaximum": 10,
         "exclusiveMinimum": 5,
         "type": "number",
@@ -374,6 +430,7 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.number().gte(5).lte(10))).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "maximum": 10,
         "minimum": 5,
         "type": "number",
@@ -381,24 +438,28 @@ describe("toJSONSchema", () => {
     `);
     expect(toJSONSchema(z.number().positive())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "exclusiveMinimum": 0,
         "type": "number",
       }
     `);
     expect(toJSONSchema(z.number().negative())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "exclusiveMaximum": 0,
         "type": "number",
       }
     `);
     expect(toJSONSchema(z.number().nonpositive())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "maximum": 0,
         "type": "number",
       }
     `);
     expect(toJSONSchema(z.number().nonnegative())).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "minimum": 0,
         "type": "number",
       }
@@ -408,6 +469,7 @@ describe("toJSONSchema", () => {
   test("arrays", () => {
     expect(toJSONSchema(z.array(z.string()))).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "items": {
           "type": "string",
         },
@@ -420,6 +482,7 @@ describe("toJSONSchema", () => {
     const schema = z.union([z.string(), z.number()]);
     expect(toJSONSchema(schema)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "anyOf": [
           {
             "type": "string",
@@ -437,6 +500,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(schema)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "allOf": [
           {
             "properties": {
@@ -469,6 +533,7 @@ describe("toJSONSchema", () => {
     const schema = z.record(z.string(), z.boolean());
     expect(toJSONSchema(schema)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "additionalProperties": {
           "type": "boolean",
         },
@@ -484,6 +549,7 @@ describe("toJSONSchema", () => {
     const schema = z.tuple([z.string(), z.number()]).rest(z.boolean());
     expect(toJSONSchema(schema)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "items": {
           "type": "boolean",
         },
@@ -504,6 +570,7 @@ describe("toJSONSchema", () => {
     const schema = z.promise(z.string());
     expect(toJSONSchema(schema)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "type": "string",
       }
     `);
@@ -513,6 +580,7 @@ describe("toJSONSchema", () => {
     const schema = z.lazy(() => z.string());
     expect(toJSONSchema(schema)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "type": "string",
       }
     `);
@@ -523,6 +591,7 @@ describe("toJSONSchema", () => {
     const schema = z.enum(["a", "b", "c"]);
     expect(toJSONSchema(schema)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "enum": [
           "a",
           "b",
@@ -537,6 +606,7 @@ describe("toJSONSchema", () => {
     const a = z.literal("hello");
     expect(toJSONSchema(a)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "const": "hello",
       }
     `);
@@ -547,6 +617,7 @@ describe("toJSONSchema", () => {
     const c = z.literal(["hello", null, 5]);
     expect(toJSONSchema(c)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "enum": [
           "hello",
           null,
@@ -564,6 +635,7 @@ describe("toJSONSchema", () => {
       .pipe(z.number());
     expect(toJSONSchema(schema)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "type": "number",
       }
     `);
@@ -578,6 +650,7 @@ describe("toJSONSchema", () => {
     expect(toJSONSchema(schema)).toMatchInlineSnapshot(
       `
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "properties": {
           "age": {
             "type": "number",
@@ -604,6 +677,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(a)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "additionalProperties": false,
         "properties": {
           "age": {
@@ -629,6 +703,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(b)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "additionalProperties": {
           "type": "string",
         },
@@ -650,6 +725,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(c)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "additionalProperties": {},
         "properties": {
           "name": {
@@ -675,6 +751,7 @@ describe("toJSONSchema", () => {
 
     expect(result).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "properties": {
           "nonoptional": {
             "type": "string",
@@ -709,6 +786,7 @@ describe("toJSONSchema", () => {
     const result = toJSONSchema(categorySchema);
     expect(result).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "properties": {
           "name": {
             "type": "string",
@@ -738,6 +816,7 @@ describe("toJSONSchema", () => {
     const result = toJSONSchema(userSchema);
     expect(result).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "properties": {
           "age": {
             "type": "number",
@@ -762,6 +841,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(a)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "additionalProperties": false,
         "properties": {
           "age": {
@@ -787,6 +867,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(b)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "additionalProperties": {
           "type": "string",
         },
@@ -808,6 +889,7 @@ describe("toJSONSchema", () => {
 
     expect(toJSONSchema(c)).toMatchInlineSnapshot(`
       {
+        "$schema": "https://json-schema.org/draft-2020-12/schema",
         "additionalProperties": {},
         "properties": {
           "name": {
@@ -848,7 +930,8 @@ describe("toJSONSchema", () => {
         "required": [
           "id",
           "children"
-        ]
+        ],
+        "$schema": "https://json-schema.org/draft-2020-12/schema"
       }"
     `
     );
@@ -902,7 +985,8 @@ describe("toJSONSchema", () => {
         "required": [
           "name",
           "files"
-        ]
+        ],
+        "$schema": "https://json-schema.org/draft-2020-12/schema"
       }"
     `
     );
@@ -919,6 +1003,7 @@ test("override", () => {
   });
   expect(schema).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "type": "string",
       "whatever": "sup",
     }
@@ -956,6 +1041,7 @@ test("pipe", () => {
   const a = z.toJSONSchema(mySchema);
   expect(a).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "type": "number",
     }
   `);
@@ -964,6 +1050,7 @@ test("pipe", () => {
   const b = z.toJSONSchema(mySchema, { io: "input" });
   expect(b).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "type": "string",
     }
   `);
@@ -1007,6 +1094,7 @@ test("passthrough schemas", () => {
           "type": "object",
         },
       },
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "properties": {
         "a": {
           "$ref": "#/$defs/__schema0",
@@ -1057,6 +1145,7 @@ test("extract schemas with id", () => {
           "type": "string",
         },
       },
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "properties": {
         "age": {
           "$ref": "#/$defs/age",
@@ -1092,6 +1181,7 @@ test("unrepresentable literal values are ignored", () => {
   const a = z.toJSONSchema(z.literal(["hello", null, 5, BigInt(1324), undefined]), { unrepresentable: "any" });
   expect(a).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "enum": [
         "hello",
         null,
@@ -1104,6 +1194,7 @@ test("unrepresentable literal values are ignored", () => {
   const b = z.toJSONSchema(z.literal([undefined, null, 5, BigInt(1324)]), { unrepresentable: "any" });
   expect(b).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "enum": [
         null,
         5,
@@ -1113,7 +1204,11 @@ test("unrepresentable literal values are ignored", () => {
   `);
 
   const c = z.toJSONSchema(z.literal([undefined]), { unrepresentable: "any" });
-  expect(c).toMatchInlineSnapshot(`{}`);
+  expect(c).toMatchInlineSnapshot(`
+    {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
+    }
+  `);
 });
 
 test("describe with id", () => {
@@ -1133,6 +1228,7 @@ test("describe with id", () => {
           "type": "string",
         },
       },
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "properties": {
         "current": {
           "$ref": "#/$defs/jobId",
@@ -1173,6 +1269,7 @@ test("overwrite id", () => {
           "id": "bbb",
         },
       },
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "properties": {
         "current": {
           "$ref": "#/$defs/aaa",
@@ -1210,6 +1307,7 @@ test("overwrite id", () => {
           "id": "ccc",
         },
       },
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "properties": {
         "current": {
           "$ref": "#/$defs/aaa",
@@ -1238,6 +1336,7 @@ test("overwrite descriptions", () => {
   );
   expect(a).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "properties": {
         "d": {
           "description": "d",
@@ -1273,6 +1372,7 @@ test("overwrite descriptions", () => {
           "type": "string",
         },
       },
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "properties": {
         "d": {
           "$ref": "#/$defs/__schema0",
@@ -1335,6 +1435,7 @@ test("top-level readonly", () => {
           "type": "object",
         },
       },
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "id": "A",
       "properties": {
         "b": {
@@ -1379,6 +1480,7 @@ test("basic registry", () => {
     {
       "schemas": {
         "Post": {
+          "$schema": "https://json-schema.org/draft-2020-12/schema",
           "properties": {
             "author": {
               "$ref": "User",
@@ -1398,6 +1500,7 @@ test("basic registry", () => {
           "type": "object",
         },
         "User": {
+          "$schema": "https://json-schema.org/draft-2020-12/schema",
           "properties": {
             "name": {
               "type": "string",
@@ -1425,6 +1528,7 @@ test("_ref", () => {
   const a = z.toJSONSchema(z.promise(z.string().describe("a")));
   expect(a).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "description": "a",
       "type": "string",
     }
@@ -1433,6 +1537,7 @@ test("_ref", () => {
   const b = z.toJSONSchema(z.lazy(() => z.string().describe("a")));
   expect(b).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "description": "a",
       "type": "string",
     }
@@ -1441,6 +1546,7 @@ test("_ref", () => {
   const c = z.toJSONSchema(z.optional(z.string().describe("a")));
   expect(c).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "description": "a",
       "type": "string",
     }
@@ -1458,11 +1564,13 @@ test("defaults/prefaults", () => {
   // a
   expect(toJSONSchema(a)).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "type": "number",
     }
   `);
   expect(toJSONSchema(a, { io: "input" })).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "type": "string",
     }
   `);
@@ -1470,11 +1578,13 @@ test("defaults/prefaults", () => {
   // b
   expect(toJSONSchema(b)).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "type": "number",
     }
   `);
   expect(toJSONSchema(b, { io: "input" })).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "default": "hello",
       "type": "string",
     }
@@ -1482,12 +1592,14 @@ test("defaults/prefaults", () => {
   // c
   expect(toJSONSchema(c)).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "default": 1234,
       "type": "number",
     }
   `);
   expect(toJSONSchema(c, { io: "input" })).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "default": 1234,
       "type": "string",
     }
@@ -1504,6 +1616,7 @@ test("input type", () => {
   });
   expect(toJSONSchema(schema, { io: "input" })).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "properties": {
         "a": {
           "type": "string",
@@ -1539,6 +1652,7 @@ test("input type", () => {
   `);
   expect(toJSONSchema(schema, { io: "output" })).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "properties": {
         "a": {
           "type": "string",
@@ -1586,6 +1700,7 @@ test("examples on pipe", () => {
   const i = z.toJSONSchema(schema, { io: "input", unrepresentable: "any" });
   expect(i).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "examples": [
         "test",
       ],
@@ -1595,6 +1710,7 @@ test("examples on pipe", () => {
   const o = z.toJSONSchema(schema, { io: "output", unrepresentable: "any" });
   expect(o).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "examples": [
         4,
       ],
@@ -1614,6 +1730,7 @@ test("override with refs", () => {
 
   expect(result).toMatchInlineSnapshot(`
     {
+      "$schema": "https://json-schema.org/draft-2020-12/schema",
       "type": "STRING",
     }
   `);