Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: StefanTerdell/zod-to-json-schema
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: c8961db5c3d81d8b533a75d0f6bf89e47f9550b4
Choose a base ref
...
head repository: StefanTerdell/zod-to-json-schema
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: d29866e9d63cf1fd205294a6831550d796f65c97
Choose a head ref
  • 3 commits
  • 9 files changed
  • 2 contributors

Commits on Feb 23, 2025

  1. 🚀 3.24.3

    StefanTerdell committed Feb 23, 2025
    Copy the full SHA
    92c5b60 View commit details

Commits on Mar 14, 2025

  1. ✨ Extend options regarding additionalProperties

    Stefan Terdell committed Mar 14, 2025
    Copy the full SHA
    07937f6 View commit details

Commits on Mar 15, 2025

  1. Copy the full SHA
    d29866e View commit details
Showing with 265 additions and 86 deletions.
  1. +34 −7 README.md
  2. +1 −0 changelog.md
  3. +2 −2 package-lock.json
  4. +3 −15 package.json
  5. +4 −0 src/Options.ts
  6. +84 −59 src/parsers/object.ts
  7. +3 −3 src/parsers/record.ts
  8. +83 −0 test/issues.test.ts
  9. +51 −0 test/parsers/object.test.ts
41 changes: 34 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ Does what it says on the tin; converts [Zod schemas](https://github.com/colinhac
## Sponsors

A great big thank you to our amazing sponsors! Please consider joining them through my [GitHub Sponsors page](https://github.com/sponsors/StefanTerdell). Every cent helps, but these fellas have really gone above and beyond 💚:

<table align="center" style="justify-content: center;align-items: center;display: flex;">
<tr>
<td align="center">
@@ -59,8 +59,6 @@ A great big thank you to our amazing sponsors! Please consider joining them thro
</tr>
</table>



## Usage

### Basic example
@@ -135,7 +133,9 @@ Instead of the schema name (or nothing), you can pass an options object as the s
| **patternStrategy**?: "escape" \| "preserve" | The Zod string validations `.includes()`, `.startsWith()`, and `.endsWith()` must be converted to regex to be compatible with JSON Schema's `pattern`. For safety, all non-alphanumeric characters are `escape`d by default (consider `z.string().includes(".")`), but this can occasionally cause problems with Unicode-flagged regex parsers. Use `preserve` to prevent this escaping behaviour and preserve the exact string written, even if it results in an inaccurate regex. |
| **applyRegexFlags**?: boolean | JSON Schema's `pattern` doesn't support RegExp flags, but Zod's `z.string().regex()` does. When this option is true (default false), a best-effort is made to transform regexes into a flag-independent form (e.g. `/x/i => /[xX]/` ). Supported flags: `i` (basic Latin only), `m`, `s`. |
| **pipeStrategy**?: "all" \| "input" \| "output" | Decide which types should be included when using `z.pipe`, for example `z.string().pipe(z.number())` would return both `string` and `number` by default, only `string` for "input" and only `number` for "output". |
| **removeAdditionalStrategy**?: "passthrough" \| "strict" | Decide when `additionalProperties ` should be false - whether according to strict or to passthrough. Since most parsers would retain properties given that `additionalProperties = false` while zod strips them, the default is to strip them unless `passthrough` is explicitly in the schema. On the other hand, it is useful to retain all fields unless `strict` is explicit in the schema which is the second option for the removeAdditional |
| **removeAdditionalStrategy**?: "passthrough" \| "strict" | Decide when `additionalProperties` should be allowed. See the section on additional properties for details. |
| **allowedAdditionalProperties**?: `true` \| `undefined` | What value to give `additionalProperties` when allowed. See the section on additional properties for details. |
| **rejectedAdditionalProperties**?: `false` \| `undefined` | What value to give `additionalProperties` when rejected. See the section on additional properties for details. |
| **override**?: callback | See section |
| **postProcess**?: callback | See section |

@@ -217,7 +217,34 @@ This allows for field specific, validation step specific error messages which ca
- ZodArray
- min, max

## `override` option
### Additional properties

By default, Zod removes undeclared properties when parsing object schemas. In order to replicate the expected output of this behaviour, the default for behaviour of zodToJsonSchema is to set `"additionalProperties"` to `false` (although the correctness of this can be debated). If you wish to allow undeclared properties you can either:

- Set `removeAdditionalStrategy` to `"strict"`. This will allow additional properties for any object schema that is not declared with `.strict()`.
- Leave `removeAdditionalStrategy` set to its default value of `"passthrough"`, and add `.passtrough()` to your object schema.

#### Removing the `additionalProperties` keyword using the `allowedAdditionalProperties` and/or `rejectedAdditionalProperties` options.

Some schema definitions (like Googles Gen AI API for instance) does not allow the `additionalProperties` keyword at all. Luckily the JSON Schema spec allows for this: leaving the keyword undefined _should_ have the same effect as setting it to true (as per usual YMMV). To enable this behaviour, set the option `allowedAdditionalProperties` to `undefined`.

To exclude the keyword even when additional properties are _not_ allowed, set the `rejectedAdditionalProperties` to `undefined` as well.

_Heads up ⚠️: Both of these options will be ignored if your schema is declared with `.catchall(...)` as the provided schema will be used instead (if valid)._

#### Expected outputs

| `z.object({})` + option | `"additionalProperties"` value |
| ------------------------- | ----------------------------------------------------------- |
| `.strip()` (default) | `false` if strategy is `"passtrough"`, `true` if `"strict"` |
| `.passtrough()` | `true` |
| `.strict()` | `false` |
| `.catchall(z.string())` | `{ "type": "string" }` |
| `.catchall(z.function())` | `undefined` (function schemas are not currently parseable) |

Substitute `true` and `false` for `undefined` according to `allowedAdditionalProperties` and/or `rejectedAdditionalProperties` respectively.

### `override`

This options takes a callback receiving a Zod schema definition, the current reference object (containing the current ref path and other options), an argument containing inforation about wether or not the schema has been encountered before, and a forceResolution argument.

@@ -271,7 +298,7 @@ Expected output:
}
```

## `postProcess` option
### `postProcess`

Besided receiving all arguments of the `override` callback, the `postProcess` callback also receives the generated schema. It should always return a JSON Schema, or `undefined` if you wish to filter it out. Unlike the `override` callback you do not have to return `ignoreOverride` if you are happy with the produced schema; simply return it unchanged.

@@ -315,7 +342,7 @@ const postProcess: PostProcessCallback = (
const jsonSchema = zodToJsonSchema(zodSchema, { postProcess });
```

### Using `postProcess` for including examples and other meta
#### Using `postProcess` for including examples and other meta

Adding support for examples and other JSON Schema meta keys are among the most commonly requested features for this project. Unfortunately the current Zod major (3) has pretty anemic support for this, so some userland hacking is required. Since this is such a common usecase I've included a helper function that simply tries to parse any description as JSON and expand it into the resulting schema.

1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

| Version | Change |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 3.24.3 | Adds postProcess callback option |
| 3.24.2 | Restructured internals to remove circular dependencies which apparently might cause some build systems to whine a bit. Big thanks to [Víctor Hernández](https://github.com/NanezX) for the fix. |
| 3.24.1 | Adds OpenAI target |
| 3.24.0 | Implements new string checks (jwt, base64url, cidr ipv4/v6), matching the new Zod version |
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 3 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zod-to-json-schema",
"version": "3.24.2",
"version": "3.24.3",
"description": "Converts Zod schemas to Json Schemas",
"types": "./dist/types/index.d.ts",
"main": "./dist/cjs/index.js",
@@ -28,21 +28,9 @@
"gen": "tsx createIndex.ts"
},
"c8": {
"exclude": [
"createIndex.ts",
"postcjs.ts",
"postesm.ts",
"test"
]
"exclude": ["createIndex.ts", "postcjs.ts", "postesm.ts", "test"]
},
"keywords": [
"zod",
"json",
"schema",
"open",
"api",
"conversion"
],
"keywords": ["zod", "json", "schema", "open", "api", "conversion"],
"author": "Stefan Terdell",
"contributors": [
"Hammad Asif (https://github.com/mrhammadasif)",
4 changes: 4 additions & 0 deletions src/Options.ts
Original file line number Diff line number Diff line change
@@ -53,6 +53,8 @@ export type Options<Target extends Targets = "jsonSchema7"> = {
dateStrategy: DateStrategy | DateStrategy[];
mapStrategy: "entries" | "record";
removeAdditionalStrategy: "passthrough" | "strict";
allowedAdditionalProperties: true | undefined;
rejectedAdditionalProperties: false | undefined;
target: Target;
strictUnions: boolean;
definitionPath: string;
@@ -77,6 +79,8 @@ export const defaultOptions: Options = {
dateStrategy: "format:date-time",
mapStrategy: "entries",
removeAdditionalStrategy: "passthrough",
allowedAdditionalProperties: true,
rejectedAdditionalProperties: false,
definitionPath: "definitions",
target: "jsonSchema7",
strictUnions: false,
143 changes: 84 additions & 59 deletions src/parsers/object.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
import { ZodObjectDef, ZodOptional } from "zod";
import { ZodObjectDef, ZodOptional, ZodTypeAny } from "zod";
import { parseDef } from "../parseDef.js";
import { JsonSchema7Type } from "../parseTypes.js";
import { Refs } from "../Refs.js";

function decideAdditionalProperties(def: ZodObjectDef, refs: Refs) {
if (refs.removeAdditionalStrategy === "strict") {
return def.catchall._def.typeName === "ZodNever"
? def.unknownKeys !== "strict"
: parseDef(def.catchall._def, {
...refs,
currentPath: [...refs.currentPath, "additionalProperties"],
}) ?? true;
} else {
return def.catchall._def.typeName === "ZodNever"
? def.unknownKeys === "passthrough"
: parseDef(def.catchall._def, {
...refs,
currentPath: [...refs.currentPath, "additionalProperties"],
}) ?? true;
}
}

export type JsonSchema7ObjectType = {
type: "object";
properties: Record<string, JsonSchema7Type>;
additionalProperties: boolean | JsonSchema7Type;
additionalProperties?: boolean | JsonSchema7Type;
required?: string[];
};

@@ -33,45 +15,88 @@ export function parseObjectDef(def: ZodObjectDef, refs: Refs) {

const result: JsonSchema7ObjectType = {
type: "object",
...Object.entries(def.shape()).reduce(
(
acc: {
properties: Record<string, JsonSchema7Type>;
required: string[];
},
[propName, propDef],
) => {
if (propDef === undefined || propDef._def === undefined) return acc;

let propOptional = propDef.isOptional();

if (propOptional && forceOptionalIntoNullable) {
if (propDef instanceof ZodOptional) {
propDef = propDef._def.innerType;
}

if (!propDef.isNullable()) {
propDef = propDef.nullable();
}

propOptional = false;
}

const parsedDef = parseDef(propDef._def, {
...refs,
currentPath: [...refs.currentPath, "properties", propName],
propertyPath: [...refs.currentPath, "properties", propName],
});
if (parsedDef === undefined) return acc;
return {
properties: { ...acc.properties, [propName]: parsedDef },
required: propOptional ? acc.required : [...acc.required, propName],
};
},
{ properties: {}, required: [] },
),
additionalProperties: decideAdditionalProperties(def, refs),
properties: {},
};
if (!result.required!.length) delete result.required;

const required: string[] = [];

const shape = def.shape();

for (const propName in shape) {
let propDef = shape[propName];

if (propDef === undefined || propDef._def === undefined) {
continue;
}

let propOptional = safeIsOptional(propDef);

if (propOptional && forceOptionalIntoNullable) {
if (propDef instanceof ZodOptional) {
propDef = propDef._def.innerType;
}

if (!propDef.isNullable()) {
propDef = propDef.nullable();
}

propOptional = false;
}

const parsedDef = parseDef(propDef._def, {
...refs,
currentPath: [...refs.currentPath, "properties", propName],
propertyPath: [...refs.currentPath, "properties", propName],
});

if (parsedDef === undefined) {
continue;
}

result.properties[propName] = parsedDef;

if (!propOptional) {
required.push(propName);
}
}

if (required.length) {
result.required = required;
}

const additionalProperties = decideAdditionalProperties(def, refs);

if (additionalProperties !== undefined) {
result.additionalProperties = additionalProperties;
}

return result;
}

function decideAdditionalProperties(def: ZodObjectDef, refs: Refs) {
if (def.catchall._def.typeName !== "ZodNever") {
return parseDef(def.catchall._def, {
...refs,
currentPath: [...refs.currentPath, "additionalProperties"],
});
}

switch (def.unknownKeys) {
case "passthrough":
return refs.allowedAdditionalProperties;
case "strict":
return refs.rejectedAdditionalProperties;
case "strip":
return refs.removeAdditionalStrategy === "strict"
? refs.allowedAdditionalProperties
: refs.rejectedAdditionalProperties;
}
}

function safeIsOptional(schema: ZodTypeAny): boolean {
try {
return schema.isOptional();
} catch {
return true;
}
}
Loading