Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b8db8a7
br(deps): Making zod plugin an optional peer dependency.
RobinTail Apr 10, 2026
f3175e9
upd lockfile.
RobinTail Apr 10, 2026
38b28c2
add plugin to dev deps as well.
RobinTail Apr 10, 2026
2b50d27
rm importing the plugin in vitest setup.
RobinTail Apr 10, 2026
11e224f
fix(ez): prevent brand override.
RobinTail Apr 10, 2026
848208d
fix(deep): shorter syntax.
RobinTail Apr 10, 2026
451672b
fix(raw): simpler type.
RobinTail Apr 10, 2026
3fd3515
fix(test): better assertion.
RobinTail Apr 10, 2026
0826316
BR(plugin): Moving getBrand() to the framework.
RobinTail Apr 11, 2026
1a0bdb1
fix(test): using getBrand again in deep checks test.
RobinTail Apr 11, 2026
100b768
fix(test): reducing diff.
RobinTail Apr 11, 2026
7dba992
Merge branch 'make-v28' into opt-zod-plugin
RobinTail Apr 11, 2026
eb8e12f
fix(example): mv plugin import into routing.
RobinTail Apr 11, 2026
11c287c
README: reflecting the breaking change (draft, AI).
RobinTail Apr 11, 2026
67d6467
Merge branch 'make-v28' into opt-zod-plugin
RobinTail Apr 11, 2026
612689b
fix(docs): Plugin readme, changing 'from' to 'for'.
RobinTail Apr 11, 2026
9ba1822
Changelog: reflecting on v28.
RobinTail Apr 11, 2026
320c2aa
Readme: polishing zod plugin section.
RobinTail Apr 11, 2026
ef69524
fix(docs): Adjusting documentation section.
RobinTail Apr 11, 2026
3434846
fix(docs): Adjusting the deprecation article.
RobinTail Apr 11, 2026
c00dfa2
fix(docs): Improving the brands handling article.
RobinTail Apr 11, 2026
93f06f3
fix(docs): typo
RobinTail Apr 11, 2026
2cfd4c9
fix(test): better assertion for no exports from the plugin.
RobinTail Apr 11, 2026
f68afc1
feat: getExamples() helper and general metadata file.
RobinTail Apr 11, 2026
a2566c6
ref: using getExamples by pullResponseExamples.
RobinTail Apr 11, 2026
7d842e0
ref: using getExamples by defaultResultHandler.
RobinTail Apr 11, 2026
17ac7e5
fix(import): consistent import of the new file.
RobinTail Apr 11, 2026
e3ea8e3
fix(test): rm plugin import from integration test.
RobinTail Apr 11, 2026
7c51d96
fix(test): rm plugin import in documentation test.
RobinTail Apr 11, 2026
924be39
fix(deps): rm the plugin from the framework devDependencies.
RobinTail Apr 11, 2026
2f707a0
Revert "fix(deps): rm the plugin from the framework devDependencies."
RobinTail Apr 11, 2026
88232a8
fix(docs): Add import statement for the plugin demo.
RobinTail Apr 11, 2026
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
2 changes: 1 addition & 1 deletion cjs-test/zod-plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);

require("express-zod-api"); // side effect here via Zod Plugin
require("@express-zod-api/zod-plugin"); // side effect here
const z = require("zod"); // ensure CJS version of Zod is used

describe("Zod plugin in CJS environment", () => {
Expand Down
8 changes: 0 additions & 8 deletions compat-test/dts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@ import { describe, test, expect } from "vitest";
import { readFile } from "node:fs/promises";

describe("DTS", () => {
test("Framework must import Zod plugin", async () => {
const fwDts = await readFile(
"./node_modules/express-zod-api/dist/index.d.ts",
"utf-8",
);
expect(fwDts).toMatch(`import "@express-zod-api/zod-plugin";`);
});

test("Zod plugin must import augmentation", async () => {
const pluginDts = await readFile(
"./node_modules/express-zod-api/node_modules/@express-zod-api/zod-plugin/dist/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions example/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "@express-zod-api/zod-plugin"; // adds .example() method
import { BuiltinLogger, createConfig } from "express-zod-api";
import ui from "swagger-ui-express";
import createHttpError from "http-errors";
Expand Down
2 changes: 1 addition & 1 deletion example/generate-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { config } from "./config.ts";
import { writeFile } from "node:fs/promises";
import { Integration } from "express-zod-api";
import { routing } from "./routing.ts";
import { config } from "./config.ts";
import typescript from "typescript";

await writeFile(
Expand Down
2 changes: 1 addition & 1 deletion example/generate-documentation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { config } from "./config.ts";
import { writeFile } from "node:fs/promises";
import { Documentation } from "express-zod-api";
import { config } from "./config.ts";
import { routing } from "./routing.ts";
import manifest from "./package.json" with { type: "json" };

Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"test": "vitest run index.spec.ts"
},
"devDependencies": {
"@express-zod-api/zod-plugin": "workspace:*",
"@types/http-errors": "catalog:dev",
"@types/swagger-ui-express": "^4.1.8",
"express-zod-api": "workspace:*",
Expand Down
7 changes: 5 additions & 2 deletions express-zod-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"prepack": "cp ../*.md ../LICENSE ./"
},
"type": "module",
"sideEffects": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"module": "dist/index.js",
Expand All @@ -42,13 +41,13 @@
"node": "^22.19.0 || ^24.0.0"
},
"dependencies": {
"@express-zod-api/zod-plugin": "workspace:^",
"ansis": "^4.2.0",
"node-mocks-http": "^1.17.2",
"openapi3-ts": "^4.5.0",
"ramda": "catalog:prod"
},
"peerDependencies": {
"@express-zod-api/zod-plugin": "workspace:^",
"@types/compression": "^1.7.5",
"@types/express": "^5.0.0",
"@types/express-fileupload": "^1.5.0",
Expand All @@ -61,6 +60,9 @@
"zod": "catalog:peer"
},
"peerDependenciesMeta": {
"@express-zod-api/zod-plugin": {
"optional": true
},
"@types/compression": {
"optional": true
},
Expand All @@ -84,6 +86,7 @@
}
},
"devDependencies": {
"@express-zod-api/zod-plugin": "workspace:^",
"@types/compression": "catalog:dev",
"@types/cors": "^2.8.19",
"@types/depd": "^1.1.37",
Expand Down
15 changes: 15 additions & 0 deletions express-zod-api/src/brand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { brandProperty as brandProp } from "../../zod-plugin/src/brand.ts";
import { globalRegistry, type z } from "zod";

export const brandProperty = "x-brand" satisfies typeof brandProp;

export const getBrand = (subject: z.core.$ZodType) => {
const { [brandProperty]: brand } = globalRegistry.get(subject) || {};
if (
typeof brand === "symbol" ||
typeof brand === "string" ||
typeof brand === "number"
)
return brand;
return undefined;
};
3 changes: 2 additions & 1 deletion express-zod-api/src/buffer-schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { brandProperty } from "./brand";

export const ezBufferBrand = Symbol("Buffer");

Expand All @@ -7,4 +8,4 @@ export const buffer = () =>
.custom<Buffer>((subject) => Buffer.isBuffer(subject), {
error: "Expected Buffer",
})
.brand(ezBufferBrand as symbol);
.meta({ [brandProperty]: ezBufferBrand });
4 changes: 2 additions & 2 deletions express-zod-api/src/date-in-schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { brandProperty } from "./brand";

export const ezDateInBrand = Symbol("DateIn");

Expand All @@ -20,6 +21,5 @@ export const dateIn = ({ examples, ...rest }: DateInParams = {}) => {
.meta({ examples })
.transform((str) => new Date(str))
.pipe(z.date())
.brand(ezDateInBrand as symbol)
.meta(rest);
.meta({ ...rest, [brandProperty]: ezDateInBrand });
};
4 changes: 2 additions & 2 deletions express-zod-api/src/date-out-schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { brandProperty } from "./brand";

export const ezDateOutBrand = Symbol("DateOut");

Expand All @@ -13,5 +14,4 @@ export const dateOut = (meta: DateOutParams = {}) =>
z
.date()
.transform((date) => date.toISOString())
.brand(ezDateOutBrand as symbol)
.meta(meta);
.meta({ ...meta, [brandProperty]: ezDateOutBrand });
2 changes: 1 addition & 1 deletion express-zod-api/src/deep-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ezDateOutBrand } from "./date-out-schema";
import { DeepCheckError } from "./errors";
import { ezFormBrand } from "./form-schema";
import type { IOSchema } from "./io-schema";
import { getBrand } from "@express-zod-api/zod-plugin";
import { getBrand } from "./brand";
import type { FirstPartyKind } from "./schema-walker";
import { ezUploadBrand } from "./upload-schema";
import { ezRawBrand } from "./raw-schema";
Expand Down
2 changes: 1 addition & 1 deletion express-zod-api/src/documentation-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { DocumentationError } from "./errors";
import type { IOSchema } from "./io-schema";
import { flattenIO } from "./json-schema-helpers";
import type { Alternatives } from "./logical-container";
import { getBrand } from "@express-zod-api/zod-plugin";
import { getBrand } from "./brand";
import type { ClientMethod } from "./method";
import type { ProprietaryBrand } from "./proprietary-schemas";
import { ezRawBrand } from "./raw-schema";
Expand Down
8 changes: 5 additions & 3 deletions express-zod-api/src/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type { IOSchema } from "./io-schema";
import { lastResortHandler } from "./last-resort";
import type { ActualLogger } from "./logger-helpers";
import type { LogicalContainer } from "./logical-container";
import { getBrand } from "@express-zod-api/zod-plugin";
import { getBrand } from "./brand";
import type { ClientMethod, CORSMethod, Method, SomeMethod } from "./method";
import { AbstractMiddleware, ExpressMiddleware } from "./middleware";
import type { ContentType } from "./content-type";
Expand Down Expand Up @@ -92,9 +92,11 @@ export class Endpoint<
> extends AbstractEndpoint {
readonly #def: ConstructorParameters<typeof Endpoint<IN, OUT, CTX>>[0];

/** considered expensive operation, only required for generators */
/** considered an expensive operation, only required for generators */
#ensureOutputExamples = R.once(() => {
if (globalRegistry.get(this.#def.outputSchema)?.examples?.length) return; // examples on output schema, or pull up:
const { examples: existing } =
globalRegistry.get(this.#def.outputSchema) || {};
if (Array.isArray(existing) && existing.length) return; // examples on output schema, or pull up:
if (!isSchema<z.core.$ZodObject>(this.#def.outputSchema, "object")) return;
const examples = pullResponseExamples(this.#def.outputSchema);
if (!examples.length) return;
Expand Down
7 changes: 4 additions & 3 deletions express-zod-api/src/form-schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { z } from "zod";
import { brandProperty } from "./brand";

export const ezFormBrand = Symbol("Form");

/** @desc Accepts an object shape or a custom object schema */
export const form = <S extends z.core.$ZodShape>(base: S | z.ZodObject<S>) =>
(base instanceof z.ZodObject ? base : z.object(base)).brand(
ezFormBrand as symbol,
);
(base instanceof z.ZodObject ? base : z.object(base)).meta({
[brandProperty]: ezFormBrand,
});
1 change: 0 additions & 1 deletion express-zod-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import "@express-zod-api/zod-plugin"; // side effects here
export { createConfig } from "./config-type";
export {
EndpointsFactory,
Expand Down
7 changes: 4 additions & 3 deletions express-zod-api/src/raw-schema.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { z } from "zod";
import { buffer } from "./buffer-schema";
import { brandProperty } from "./brand";

export const ezRawBrand = Symbol("Raw");

const base = z.object({ raw: buffer() });
type Base = ReturnType<typeof base.brand<symbol>>;
type Base = typeof base;

const extended = <S extends z.core.$ZodShape>(extra: S) =>
base.extend(extra).brand(ezRawBrand as symbol);
base.extend(extra).meta({ [brandProperty]: ezRawBrand });

export function raw(): Base;
export function raw<S extends z.core.$ZodShape>(
extra: S,
): ReturnType<typeof extended<S>>;
export function raw(extra?: z.core.$ZodShape) {
return extra ? extended(extra) : base.brand(ezRawBrand as symbol);
return extra ? extended(extra) : base.meta({ [brandProperty]: ezRawBrand });
}

export type RawSchema = Base;
12 changes: 6 additions & 6 deletions express-zod-api/src/result-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const defaultResultHandler = new ResultHandler({
data: output,
});
const { examples } = globalRegistry.get(output) || {}; // pulling down:
if (examples?.length) {
if (Array.isArray(examples) && examples.length) {
Comment thread
pullfrog[bot] marked this conversation as resolved.
Outdated
globalRegistry.add(responseSchema, {
examples: examples.map((data) => ({
status: "success" as const,
Expand Down Expand Up @@ -160,11 +160,11 @@ export const arrayResultHandler = new ResultHandler({
output.shape.items instanceof z.ZodArray
? output.shape.items
: z.array(z.any());
if (globalRegistry.get(responseSchema)?.examples?.length)
return responseSchema; // has examples on the items, or pull down:
const examples = globalRegistry
.get(output)
?.examples?.filter(
const { examples: existing } = globalRegistry.get(responseSchema) || {};
if (Array.isArray(existing) && existing.length) return responseSchema; // has examples on the items, or pull down:
const { examples: pulled } = globalRegistry.get(output) || {};
const examples = (Array.isArray(pulled) ? pulled : [])
.filter(
(example): example is { items: unknown[] } =>
isObject(example) &&
"items" in example &&
Expand Down
11 changes: 6 additions & 5 deletions express-zod-api/src/result-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,12 @@ export const getPublicErrorMessage = (error: HttpError): string =>
export const pullResponseExamples = <T extends z.core.$ZodObject>(subject: T) =>
Object.entries(subject._zod.def.shape).reduce<FlatObject[]>(
(acc, [key, schema]) => {
const { examples = [] } = globalRegistry.get(schema) || {};
return combinations(acc, examples.map(R.objOf(key)), ([left, right]) => ({
...left,
...right,
}));
const { examples } = globalRegistry.get(schema) || {};
return combinations(
acc,
(Array.isArray(examples) ? examples : []).map(R.objOf(key)),
([left, right]) => ({ ...left, ...right }),
);
},
[],
);
2 changes: 1 addition & 1 deletion express-zod-api/src/schema-walker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { EmptyObject, FlatObject } from "./common-helpers";
import { getBrand } from "@express-zod-api/zod-plugin";
import { getBrand } from "./brand";
import type { z } from "zod";

export type FirstPartyKind = z.core.$ZodTypeDef["type"];
Expand Down
3 changes: 2 additions & 1 deletion express-zod-api/src/upload-schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { UploadedFile } from "express-fileupload";
import { z } from "zod";
import { brandProperty } from "./brand";

export const ezUploadBrand = Symbol("Upload");

Expand Down Expand Up @@ -33,4 +34,4 @@ export const upload = () =>
}),
},
)
.brand(ezUploadBrand as symbol);
.meta({ [brandProperty]: ezUploadBrand });
16 changes: 8 additions & 8 deletions express-zod-api/tests/__snapshots__/zts.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -252,14 +252,7 @@ exports[`zod-to-ts > z.object() > specially handles coercive schema in response
}"
`;

exports[`zod-to-ts > z.object() > supports string literal properties 1`] = `
"{
"5": number;
"string-literal": string;
}"
`;

exports[`zod-to-ts > z.object() > supports zod.deprecated() 1`] = `
exports[`zod-to-ts > z.object() > supports deprecated metadata 1`] = `
"{
/** @deprecated */
one: string;
Expand All @@ -268,6 +261,13 @@ exports[`zod-to-ts > z.object() > supports zod.deprecated() 1`] = `
}"
`;

exports[`zod-to-ts > z.object() > supports string literal properties 1`] = `
"{
"5": number;
"string-literal": string;
}"
`;

exports[`zod-to-ts > z.object() > supports zod.describe() 1`] = `
"{
/** The name of the item */
Expand Down
13 changes: 13 additions & 0 deletions express-zod-api/tests/brand.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { globalRegistry, z } from "zod";
import { brandProperty, getBrand } from "../src/brand";

describe("getBrand", () => {
test.each([{ [brandProperty]: "test" }, {}, undefined])(
"should take it from metadata in globalRegistry %#",
(metadata) => {
const subject = z.string();
if (metadata) globalRegistry.add(subject, metadata);
expect(getBrand(subject)).toBe(metadata?.[brandProperty]);
},
);
});
4 changes: 2 additions & 2 deletions express-zod-api/tests/buffer-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { readFile } from "node:fs/promises";
import { z, $brand } from "zod";
import { z } from "zod";
import { ez } from "../src";

describe("ez.buffer()", () => {
describe("creation", () => {
test("should create a Buffer", () => {
const schema = ez.buffer();
expect(schema).toBeInstanceOf(z.ZodCustom);
expectTypeOf(schema._zod.output).toEqualTypeOf<Buffer & $brand<symbol>>();
expectTypeOf(schema._zod.output).toEqualTypeOf<Buffer>();
});
});

Expand Down
2 changes: 1 addition & 1 deletion express-zod-api/tests/date-in-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from "zod";
import { ezDateInBrand } from "../src/date-in-schema";
import { ez } from "../src";
import { getBrand } from "@express-zod-api/zod-plugin";
import { getBrand } from "../src/brand";

describe("ez.dateIn()", () => {
describe("creation", () => {
Expand Down
2 changes: 1 addition & 1 deletion express-zod-api/tests/date-out-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from "zod";
import { ezDateOutBrand } from "../src/date-out-schema";
import { ez } from "../src";
import { getBrand } from "@express-zod-api/zod-plugin";
import { getBrand } from "../src/brand";

describe("ez.dateOut()", () => {
describe("creation", () => {
Expand Down
4 changes: 2 additions & 2 deletions express-zod-api/tests/deep-checks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { UploadedFile } from "express-fileupload";
import { z } from "zod";
import { ez } from "../src";
import { findNestedSchema, hasCycle } from "../src/deep-checks";
import { getBrand } from "@express-zod-api/zod-plugin";
import { getBrand } from "../src/brand";
import { ezUploadBrand } from "../src/upload-schema";

describe("Checks", () => {
Expand All @@ -22,7 +22,7 @@ describe("Checks", () => {
z.object({ test: z.boolean() }).and(z.object({ test2: ez.upload() })),
z.optional(ez.upload()),
ez.upload().nullable(),
ez.upload().default({} as UploadedFile & z.core.$brand<symbol>),
ez.upload().default({} as UploadedFile),
z.record(z.string(), ez.upload()),
ez.upload().refine(() => true),
z.array(ez.upload()),
Expand Down
1 change: 1 addition & 0 deletions express-zod-api/tests/documentation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "@express-zod-api/zod-plugin";
import camelize from "camelize-ts";
import snakify from "snakify-ts";
import {
Expand Down
Loading
Loading