diff --git a/CHANGELOG.md b/CHANGELOG.md
index b86301423..8c8a6d099 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,36 @@
## Version 24
+### v24.4.2
+
+- Improved the type of the `input` argument for `Endpoint::handler()`:
+ - For `z.object()`-based schema removed `never` for unknown properties;
+ - Fixed incorrect typing of excessive properties when using a `z.looseObject()`-based schema;
+ - The issue reported by [@ThomasKientz](https://github.com/ThomasKientz).
+
+```ts
+import { defaultEndpointsFactory } from "express-zod-api";
+import { z } from "zod/v4";
+
+const endpoint1 = defaultEndpointsFactory.buildVoid({
+ input: z.object({
+ foo: z.string(),
+ }),
+ handler: async ({ input: { bar } }) => {
+ console.log(bar); // before: never, after: TypeScript Error
+ },
+});
+
+const endpoint2 = defaultEndpointsFactory.buildVoid({
+ input: z.looseObject({
+ foo: z.string(),
+ }),
+ handler: async ({ input: { bar } }) => {
+ console.log(bar); // before: never, after: unknown
+ },
+});
+```
+
### v24.4.1
- Compatibility fix for Zod 3.25.67.
diff --git a/README.md b/README.md
index bb6249841..c70f0ab8f 100644
--- a/README.md
+++ b/README.md
@@ -84,6 +84,7 @@ Therefore, many basic tasks can be accomplished faster and easier, in particular
These people contributed to the improvement of the framework by reporting bugs, making changes and suggesting ideas:
+[
](https://github.com/ThomasKientz)
[
](https://github.com/james10424)
[
](https://github.com/HeikoOsigus)
[
](https://github.com/crgeary)
diff --git a/express-zod-api/src/endpoints-factory.ts b/express-zod-api/src/endpoints-factory.ts
index a82854adf..d7fa5b957 100644
--- a/express-zod-api/src/endpoints-factory.ts
+++ b/express-zod-api/src/endpoints-factory.ts
@@ -2,7 +2,11 @@ import { Request, Response } from "express";
import { z } from "zod/v4";
import { EmptyObject, EmptySchema, FlatObject, Tag } from "./common-helpers";
import { Endpoint, Handler } from "./endpoint";
-import { IOSchema, getFinalEndpointInputSchema } from "./io-schema";
+import {
+ IOSchema,
+ getFinalEndpointInputSchema,
+ ConditionalIntersection,
+} from "./io-schema";
import { Method } from "./method";
import {
AbstractMiddleware,
@@ -18,7 +22,7 @@ import {
interface BuildProps<
IN extends IOSchema,
OUT extends IOSchema | z.ZodVoid,
- MIN extends IOSchema,
+ MIN extends IOSchema | undefined,
OPT extends FlatObject,
SCO extends string,
> {
@@ -31,7 +35,11 @@ interface BuildProps<
/** @desc The schema by which the returns of the Endpoint handler is validated */
output: OUT;
/** @desc The Endpoint handler receiving the validated inputs, returns of added Middlewares (options) and a logger */
- handler: Handler>, z.input, OPT>;
+ handler: Handler<
+ z.output>,
+ z.input,
+ OPT
+ >;
/** @desc The operation description for the generated Documentation */
description?: string;
/** @desc The operation summary for the generated Documentation (50 symbols max) */
@@ -59,7 +67,7 @@ interface BuildProps<
}
export class EndpointsFactory<
- IN extends IOSchema = EmptySchema,
+ IN extends IOSchema | undefined = undefined,
OUT extends FlatObject = EmptyObject,
SCO extends string = string,
> {
@@ -67,7 +75,7 @@ export class EndpointsFactory<
constructor(protected resultHandler: AbstractResultHandler) {}
static #create<
- CIN extends IOSchema,
+ CIN extends IOSchema | undefined,
COUT extends FlatObject,
CSCO extends string,
>(middlewares: AbstractMiddleware[], resultHandler: AbstractResultHandler) {
@@ -86,7 +94,7 @@ export class EndpointsFactory<
| ConstructorParameters>[0],
) {
return EndpointsFactory.#create<
- z.ZodIntersection,
+ ConditionalIntersection,
OUT & AOUT,
SCO & ASCO
>(
diff --git a/express-zod-api/src/io-schema.ts b/express-zod-api/src/io-schema.ts
index cabadf807..66a2032d8 100644
--- a/express-zod-api/src/io-schema.ts
+++ b/express-zod-api/src/io-schema.ts
@@ -7,6 +7,11 @@ type Base = object & { [Symbol.iterator]?: never };
/** @desc The type allowed on the top level of Middlewares and Endpoints */
export type IOSchema = z.ZodType;
+export type ConditionalIntersection<
+ Current extends IOSchema | undefined,
+ Inc extends IOSchema,
+> = z.ZodIntersection;
+
/**
* @description intersects input schemas of middlewares and the endpoint
* @since 07.03.2022 former combineEndpointAndMiddlewareInputSchemas()
@@ -15,7 +20,7 @@ export type IOSchema = z.ZodType;
* @since 22.05.2025 does not mix examples in after switching to Zod 4
*/
export const getFinalEndpointInputSchema = <
- MIN extends IOSchema,
+ MIN extends IOSchema | undefined,
IN extends IOSchema,
>(
middlewares: AbstractMiddleware[],
@@ -23,4 +28,7 @@ export const getFinalEndpointInputSchema = <
) =>
R.pluck("schema", middlewares)
.concat(input)
- .reduce((acc, schema) => acc.and(schema)) as z.ZodIntersection;
+ .reduce((acc, schema) => acc.and(schema)) as ConditionalIntersection<
+ MIN,
+ IN
+ >;
diff --git a/express-zod-api/src/sse.ts b/express-zod-api/src/sse.ts
index 587967fe3..0f9692031 100644
--- a/express-zod-api/src/sse.ts
+++ b/express-zod-api/src/sse.ts
@@ -1,6 +1,6 @@
import { Response } from "express";
import { z } from "zod/v4";
-import { EmptySchema, FlatObject } from "./common-helpers";
+import { FlatObject } from "./common-helpers";
import { contentTypes } from "./content-type";
import { EndpointsFactory } from "./endpoints-factory";
import { Middleware } from "./middleware";
@@ -100,7 +100,7 @@ export const makeResultHandler = (events: E) =>
});
export class EventStreamFactory extends EndpointsFactory<
- EmptySchema,
+ undefined,
Emitter
> {
constructor(events: E) {
diff --git a/express-zod-api/tests/endpoints-factory.spec.ts b/express-zod-api/tests/endpoints-factory.spec.ts
index 8780588b7..b04ac3bce 100644
--- a/express-zod-api/tests/endpoints-factory.spec.ts
+++ b/express-zod-api/tests/endpoints-factory.spec.ts
@@ -1,5 +1,6 @@
import { RequestHandler } from "express";
import createHttpError from "http-errors";
+import { expectTypeOf } from "vitest";
import {
EndpointsFactory,
Middleware,
@@ -81,6 +82,28 @@ describe("EndpointsFactory", () => {
>
>();
});
+
+ test("Issue #2760: should strip excessive props by default", () => {
+ defaultEndpointsFactory.build({
+ input: z.object({ foo: z.string() }),
+ output: z.object({ foo: z.string() }),
+ handler: async ({ input }) => {
+ expectTypeOf(input).not.toHaveProperty("bar");
+ return input;
+ },
+ });
+ });
+
+ test("Issue #2760: should allow excessive props when using loose object schema", () => {
+ defaultEndpointsFactory.build({
+ input: z.looseObject({ foo: z.string() }),
+ output: z.object({ foo: z.string() }),
+ handler: async ({ input }) => {
+ expectTypeOf(input).toHaveProperty("bar").toEqualTypeOf();
+ return input;
+ },
+ });
+ });
});
describe(".addOptions()", () => {
@@ -92,7 +115,7 @@ describe("EndpointsFactory", () => {
}));
expectTypeOf(newFactory).toEqualTypeOf<
EndpointsFactory<
- EmptySchema,
+ undefined,
EmptyObject & { option1: string; option2: string }
>
>();
@@ -249,10 +272,9 @@ describe("EndpointsFactory", () => {
expect(endpoint.methods).toBeUndefined();
expect(endpoint.inputSchema).toMatchSnapshot();
expect(endpoint.outputSchema).toMatchSnapshot();
- expectTypeOf(endpoint.inputSchema._zod.output).toExtend<{
- n: number;
- s: string;
- }>();
+ expectTypeOf(endpoint.inputSchema._zod.output).toEqualTypeOf<
+ { n: number } & { s: string }
+ >();
});
test("Should create an endpoint with refined object middleware", () => {
@@ -277,11 +299,9 @@ describe("EndpointsFactory", () => {
});
expect(endpoint.inputSchema).toMatchSnapshot();
expect(endpoint.outputSchema).toMatchSnapshot();
- expectTypeOf(endpoint.inputSchema._zod.output).toExtend<{
- a?: number;
- b?: string;
- i: string;
- }>();
+ expectTypeOf(endpoint.inputSchema._zod.output).toEqualTypeOf<
+ { a?: number; b?: string } & { i: string }
+ >();
});
test("Should create an endpoint with intersection middleware", () => {
@@ -302,11 +322,9 @@ describe("EndpointsFactory", () => {
expect(endpoint.methods).toBeUndefined();
expect(endpoint.inputSchema).toMatchSnapshot();
expect(endpoint.outputSchema).toMatchSnapshot();
- expectTypeOf(endpoint.inputSchema._zod.output).toExtend<{
- n1: number;
- n2: number;
- s: string;
- }>();
+ expectTypeOf(endpoint.inputSchema._zod.output).toEqualTypeOf<
+ { n1: number } & { n2: number } & { s: string }
+ >();
});
test("Should create an endpoint with union middleware", () => {
@@ -330,7 +348,7 @@ describe("EndpointsFactory", () => {
expect(endpoint.methods).toBeUndefined();
expect(endpoint.inputSchema).toMatchSnapshot();
expect(endpoint.outputSchema).toMatchSnapshot();
- expectTypeOf(endpoint.inputSchema._zod.output).toExtend<
+ expectTypeOf(endpoint.inputSchema._zod.output).toEqualTypeOf<
{ s: string } & ({ n1: number } | { n2: number })
>();
});