Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
### v28.0.0

- Supported Node.js versions: `^22.19.0 || ^24.0.0`;
- Zod compatibility: `^4.3.4` (supports Zod 4.4+ without upper limit);
- The Zod plugin is no longer installed automatically — it's an optional peer dependency now:
- To keep using `.example()`, `.label()`, `.remap()`, `.deprecated()` and `.brand()` methods on schemas
install the `@express-zod-api/zod-plugin` manually and import it (ideally at the top of a file declaring `Routing`);
- To keep using `.example()`, `.label()`, `.remap()`, `.deprecated()` and methods on schemas, as well as runtime
Comment thread
pullfrog[bot] marked this conversation as resolved.
distinguishable brands, install the `@express-zod-api/zod-plugin` manually and import it (ideally at the top of a
file declaring your `Routing`);
- Breaking change: `ZodType::brand()` method is no longer patched by the plugin:
- Use `.xBrand()` method instead — alias for `.meta({ "x-brand": ... })` and does not conflict with Zod 4.4;
- Breaking changes to the `createConfig()` argument (object):
- property `wrongMethodBehavior` (number) changed to `hintAllowedMethods` (boolean);
- property `methodLikeRouteBehavior` (string literal) changed to `recognizeMethodDependentRoutes` (boolean);
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1102,7 +1102,7 @@ expect(output).toEqual({ collectedContext: ["prev"], testLength: 9 });
The [@express-zod-api/zod-plugin](https://www.npmjs.com/package/@express-zod-api/zod-plugin) is an optional package
that extends Zod with convenience methods:

- `.brand(name)` — enhanced with a shorthand for `.meta({ "x-brand": name })`;
- `.xBrand(name)` — shorthand for `.meta({ "x-brand": name })`;
- `.example(value)` — shorthand for `.meta({ examples: [value] })`;
- `.deprecated()` — shorthand for `.meta({ deprecated: true })`;
- `.label(text)` — shorthand for `.meta({ default: text })` on `ZodDefault`;
Expand Down Expand Up @@ -1251,9 +1251,9 @@ const routing: Routing = {

You can customize handling rules for your schemas in Documentation and Integration. The framework treats your schema
specially based on its `x-brand` metadata. When the [Zod Plugin](#zod-plugin) is installed you can conveniently use
the `.brand()` enhanced method of the Zod schema, preferably with a symbol argument for its branding.
After that use the `brandHandling` feature of both constructors to declare your custom implementation. In case you need
to reuse a handling rule for multiple brands, use the exposed types `Depicter` and `Producer`.
the `.xBrand()` method on Zod schema, preferably with a symbol argument for its branding. After that use the
`brandHandling` feature of both constructors to declare your custom implementation. In case you need to reuse a
handling rule for multiple brands, use the exposed types `Depicter` and `Producer`.

```ts
import ts from "typescript";
Expand All @@ -1266,7 +1266,7 @@ import {
} from "express-zod-api";

const myBrand = Symbol("MamaToldMeImSpecial"); // I recommend to use symbols for this purpose
const myBrandedSchema = z.string().brand(myBrand); // requires Zod Plugin, or .meta({ "x-brand": myBrand })
const myBrandedSchema = z.string().xBrand(myBrand); // requires Zod Plugin, or .meta({ "x-brand": myBrand })

const ruleForDocs: Depicter = (
{ zodSchema, jsonSchema }, // jsonSchema is the default depiction
Expand Down
1 change: 0 additions & 1 deletion example/example.documentation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,6 @@ components:
required:
- title
additionalProperties: false
id: Feature
responses: {}
parameters: {}
examples: {}
Expand Down
4 changes: 3 additions & 1 deletion express-zod-api/src/documentation-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,8 @@ const fixReferences = (
ctx: OpenAPIContext,
) => {
const stack: unknown[] = [subject, defs];
const filterNaming = (name: string) =>
/schema\d+$/.test(name) ? undefined : name;
for (let idx = 0; idx < stack.length; idx++) {
const entry = stack[idx];
if (R.is(Object, entry)) {
Expand All @@ -380,7 +382,7 @@ const fixReferences = (
entry.$ref = ctx.makeRef(
depiction.id || depiction, // avoiding serialization, because changing $ref
asOAS(depiction),
depiction.id,
depiction.id || filterNaming(actualName),
).$ref;
}
continue;
Expand Down
5 changes: 3 additions & 2 deletions express-zod-api/src/documentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,11 @@ interface DocumentationParams {
/** @default inline */
composition?: "inline" | "components";
/**
* @desc Handling rules for your own branded schemas.
* @desc Handling rules for your own schemas branded with `x-brand` metadata.
* @desc Keys: brands (recommended to use unique symbols).
* @desc Values: functions having Zod context as first argument, second one is the framework context.
* @example { MyBrand: ( { zodSchema, jsonSchema } ) => ({ type: "object" })
* @example { MyBrand: ({ zodSchema, jsonSchema }) => ({ type: "object" })
* @link https://www.npmjs.com/package/@express-zod-api/zod-plugin
*/
brandHandling?: BrandHandling;
/**
Expand Down
5 changes: 3 additions & 2 deletions express-zod-api/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@ interface IntegrationParams {
* */
hasHeadMethod?: boolean;
/**
* @desc Handling rules for your own branded schemas.
* @desc Handling rules for your own schemas branded with `x-brand` metadata.
* @desc Keys: brands (recommended to use unique symbols).
* @desc Values: functions having schema as first argument that you should assign type to, second one is a context.
* @example { MyBrand: ( schema: typeof myBrandSchema, { next } ) => createKeywordTypeNode(SyntaxKind.AnyKeyword)
* @example { MyBrand: (schema: typeof myBrandSchema, { next }) => createKeywordTypeNode(SyntaxKind.AnyKeyword)
* @link https://www.npmjs.com/package/@express-zod-api/zod-plugin
*/
brandHandling?: HandlingRules<ts.TypeNode, ZTSContext>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1804,7 +1804,7 @@ paths:
cuid:
type: string
format: cuid
pattern: ^[cC][^\\s-]{8,}$
pattern: ^[cC][0-9a-z]{6,}$
cuid2:
type: string
format: cuid2
Expand Down Expand Up @@ -5042,7 +5042,6 @@ components:
const: John
- type: string
const: Jane
id: NameParam
responses: {}
parameters: {}
examples: {}
Expand Down
8 changes: 0 additions & 8 deletions express-zod-api/tests/__snapshots__/env.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,6 @@ exports[`Environment checks > Zod new features > input examples of transformatio
}
`;

exports[`Environment checks > Zod new features > meta id goes directly to depiction 1`] = `
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"id": "uniq",
"type": "string",
}
`;

exports[`Environment checks > Zod new features > meta() merge, not just overrides 1`] = `
{
"description": "some",
Expand Down
8 changes: 4 additions & 4 deletions express-zod-api/tests/__snapshots__/zts.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ exports[`zod-to-ts > Example > should produce the expected results 1`] = `
enum: "hi" | "bye";
intersectionWithTransform: (number & bigint) & (number & string);
date: any;
undefined?: undefined;
undefined: undefined;
null: null;
void: undefined;
any: any;
Expand Down Expand Up @@ -153,7 +153,7 @@ exports[`zod-to-ts > PrimitiveSchema (isResponse=false) > outputs correct typesc
number: number;
boolean: boolean;
date: any;
undefined?: undefined;
undefined: undefined;
null: null;
void: undefined;
any: any;
Expand All @@ -168,7 +168,7 @@ exports[`zod-to-ts > PrimitiveSchema (isResponse=true) > outputs correct typescr
number: number;
boolean: boolean;
date: unknown;
undefined?: undefined;
undefined: undefined;
null: null;
void: undefined;
any: any;
Expand Down Expand Up @@ -242,7 +242,7 @@ exports[`zod-to-ts > z.object() > escapes correctly 1`] = `
$e: any;
"4t": any;
_r: any;
"-r"?: undefined;
"-r": undefined;
}"
`;

Expand Down
6 changes: 4 additions & 2 deletions express-zod-api/tests/env.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,10 @@ describe("Environment checks", () => {
},
);

test("meta id goes directly to depiction", () => {
expect(z.toJSONSchema(z.string().meta({ id: "uniq" }))).toMatchSnapshot();
test("meta id does NOT go into depiction", () => {
expect(
Comment thread
pullfrog[bot] marked this conversation as resolved.
z.toJSONSchema(z.string().meta({ id: "uniq" })),
).not.toHaveProperty("id");
});
});

Expand Down
22 changes: 11 additions & 11 deletions pnpm-lock.yaml

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

4 changes: 2 additions & 2 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ catalogs:
prod:
"ramda": "^0.32.0"
peer:
"zod": "~4.3.4"
"zod": "^4.3.4"
dev:
"@types/compression": "^1.8.1"
"@types/express": "^5.0.6"
Expand All @@ -64,7 +64,7 @@ catalogs:
"http-errors": "^2.0.1"
"typescript": "^6.0.2"
"typescript-eslint": "^8.59.0"
"zod": "~4.3.4"
"zod": "^4.4.1"
overrides:
"@scarf/scarf": "-"
"lightningcss": "-"
5 changes: 5 additions & 0 deletions zod-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
- Supported Node.js versions: `^22.19.0 || ^24.0.0`;
- `getBrand()` removed:
- use `schema.meta()?.["x-brand"]` instead.
- Added `xBrand()` method to all Zod schemas:
- shorthand for `.meta({ "x-brand": ... })`;
- This method does not conflict with Zod 4.4+ internal mechanisms;
- Use `xBrand()` instead of `brand()` for setting runtime-distinguishable brands;
- Zod compatibility updated to `^4.3.4` (supports Zod 4.4+).

## Version 4

Expand Down
10 changes: 5 additions & 5 deletions zod-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,26 @@ This module extends Zod functionality when it's imported:
- shorthand for `.meta({ examples: [...] })`;
- Adds `.deprecated()` method to all Zod schemas:
- shorthand for `.meta({ deprecated: true })`;
- Adds `.xBrand()` method to all Zod schemas:
- shorthand for `.meta({ "x-brand": ... })` making the brand available in runtime;
- This method does not conflict with Zod 4.4+ internal mechanisms;
- Adds `.label()` method to `ZodDefault`:
- shorthand for `.meta({ default: ... })`;
- Adds `.remap()` method to `ZodObject` for renaming object properties:
- Supports a mapping object or an object transforming function as an argument;
- Relies on `R.renameKeys()` from the `ramda` library;
- Alters the `.brand()` method on all Zod schemas:
- shorthand for `.meta({ "x-brand": ... })` making the brand available in runtime;

## Requirements

- Compatible with Zod versions `~4.3.4` (<4.4.0);
- Zod 4.4+ support will be available in v5 (the next major version).
- Compatible with Zod versions `^4.3.4` (including 4.4+);

## Basic usage

```ts
import { z } from "zod";
import "@express-zod-api/zod-plugin";

const schema = z.string().example("test").example("another").brand("custom");
const schema = z.string().example("test").example("another").xBrand("custom");

schema.meta(); // { examples: ["test", "another"], "x-brand": "custom" }
```
5 changes: 4 additions & 1 deletion zod-plugin/src/augmentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ declare module "zod/v4/core" {
* @see https://github.com/colinhacks/zod/blob/90efe7fa6135119224412c7081bd12ef0bccef26/plugin/effect/src/index.ts#L21-L31
* @desc This code modifies and extends zod's functionality immediately when importing the plugin.
* @desc Enables .example() and .deprecated() on all schemas (ZodType)
* @desc Enables .xBrand() on all schemas as an alternative to .brand() that doesn't conflict with Zod 4.4+
* @desc Enables .label() on ZodDefault
* @desc Enables .remap() on ZodObject
* @desc Stores the argument supplied to .brand() on all schemas (runtime distinguishable branded types)
* @desc Stores the argument supplied to .xBrand() on all schemas (runtime distinguishable)
* */
declare module "zod" {
interface ZodType<
Expand All @@ -29,6 +30,8 @@ declare module "zod" {
/** @desc Shorthand for .meta({ examples }) */
example(example: z.output<this>): this;
deprecated(): this;
/** @desc Shorthand for .meta({ "x-brand": ... }) */
xBrand(brand?: PropertyKey): this;
}
interface ZodDefault<T extends z.core.SomeType = z.core.$ZodType>
extends z._ZodType<z.core.$ZodDefaultInternals<T>>, z.core.$ZodDefault<T> {
Expand Down
8 changes: 3 additions & 5 deletions zod-plugin/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,9 @@ if (!(pluginFlag in globalThis)) {
value: deprecationSetter,
writable: false,
},
["brand" satisfies keyof z.ZodType]: {
set() {}, // this is required to override the existing method
get() {
return brandSetter.bind(this) as z.ZodType["brand"];
},
["xBrand" satisfies keyof z.ZodType]: {
value: brandSetter,
writable: false,
},
});
}
Expand Down
8 changes: 4 additions & 4 deletions zod-plugin/tests/runtime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,23 @@ describe.each<{ variant: string; z: typeof zESM }>([
});
});

describe(".brand()", () => {
describe(".xBrand()", () => {
test("should set the brand", () => {
const subject = z.string().brand("test");
const subject = z.string().xBrand("test");
expect(subject.meta()).toEqual({ "x-brand": "test" });
});

test("should withstand refinements", () => {
const schema = z.string();
const schemaWithMeta = schema.brand("test");
const schemaWithMeta = schema.xBrand("test");
expect(schemaWithMeta.meta()).toEqual({ "x-brand": "test" });
expect(schemaWithMeta.regex(/@example.com$/).meta()).toEqual({
"x-brand": "test",
});
});

test("should withstand describing", () => {
const schema = z.string().brand("test").describe("something");
const schema = z.string().xBrand("test").describe("something");
expect(schema.meta()).toEqual({
"x-brand": "test",
description: "something",
Expand Down
Loading