express-zod-api 23.6.1 → 24.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,77 @@
1
1
  # Changelog
2
2
 
3
+ ## Version 24
4
+
5
+ ### v24.0.0
6
+
7
+ - Switched to Zod 4:
8
+ - Minimum supported version of `zod` is 3.25.1, BUT imports MUST be from `zod/v4`;
9
+ - Read the [Explanation of the versioning strategy](https://github.com/colinhacks/zod/issues/4371);
10
+ - Express Zod API, however, is not aiming to support both Zod 3 and Zod 4 simultaneously due to:
11
+ - incompatibility of data structures;
12
+ - operating composite schemas (need to avoid mixing schemas of different versions);
13
+ - the temporary nature of this transition;
14
+ - the advantages of Zod 4 that provide opportunities to simplifications and corrections of known issues.
15
+ - `IOSchema` type had to be simplified down to a schema resulting to an `object`, but not an `array`;
16
+ - Refer to [Migration guide on Zod 4](https://v4.zod.dev/v4/changelog) for adjusting your schemas;
17
+ - Changes to `ZodType::example()` (Zod plugin method):
18
+ - Now acts as an alias for `ZodType::meta({ examples })`;
19
+ - The argument has to be the output type of the schema (used to be the opposite):
20
+ - This change is only breaking for transforming schemas;
21
+ - In order to specify an example for an input schema the `.example()` method must be called before `.transform()`;
22
+ - The transforming proprietary schemas `ez.dateIn()` and `ez.dateOut()` now accept metadata as its argument:
23
+ - This allows to set examples before transformation (`ez.dateIn()`) and to avoid the examples "branding";
24
+ - Changes to `Documentation`:
25
+ - Generating Documentation is mostly delegated to Zod 4 `z.toJSONSchema()`;
26
+ - Express Zod API implements some overrides and improvements to fit it into OpenAPI 3.1 that extends JSON Schema;
27
+ - The `numericRange` option removed from `Documentation` class constructor argument;
28
+ - The `Depicter` type signature changed: became a postprocessing function returning an overridden JSON Schema;
29
+ - Changes to `Integration`:
30
+ - The `optionalPropStyle` option removed from `Integration` class constructor:
31
+ - Use `.optional()` to add question mark to the object property as well as `undefined` to its type;
32
+ - Use `.or(z.undefined())` to add `undefined` to the type of the object property;
33
+ - See the [reasoning](https://x.com/colinhacks/status/1919292504861491252);
34
+ - `z.any()` and `z.unknown()` are required: [details](https://v4.zod.dev/v4/changelog#changes-zunknown-optionality);
35
+ - Added types generation for `z.never()`, `z.void()` and `z.unknown()` schemas;
36
+ - The fallback type for unsupported schemas and unclear transformations in response changed from `any` to `unknown`;
37
+ - The argument of `ResultHandler::handler` is now discriminated: either `output` or `error` is `null`, not both;
38
+ - The `getExamples()` public helper removed — use `.meta()?.examples` instead;
39
+ - Added the new proprietary schema `ez.buffer()`;
40
+ - The `ez.file()` schema removed: use `z.string()`, `z.base64()`, `ez.buffer()` or their union;
41
+ - Consider the automated migration using the built-in ESLint rule.
42
+
43
+ ```js
44
+ // eslint.config.mjs — minimal ESLint 9 config to apply migrations automatically using "eslint --fix"
45
+ import parser from "@typescript-eslint/parser";
46
+ import migration from "express-zod-api/migration";
47
+
48
+ export default [
49
+ { languageOptions: { parser }, plugins: { migration } },
50
+ { files: ["**/*.ts"], rules: { "migration/v24": "error" } },
51
+ ];
52
+ ```
53
+
54
+ ```diff
55
+ - import { z } from "zod";
56
+ + import { z } from "zod/v4";
57
+ ```
58
+
59
+ ```diff
60
+ input: z.string()
61
+ + .example("123")
62
+ .transform(Number)
63
+ - .example("123")
64
+ ```
65
+
66
+ ```diff
67
+ - ez.dateIn().example("2021-12-31");
68
+ + ez.dateIn({ examples: ["2021-12-31"] });
69
+ - ez.file("base64");
70
+ + z.base64();
71
+ - ez.file("buffer");
72
+ + ez.buffer();
73
+ ```
74
+
3
75
  ## Version 23
4
76
 
5
77
  ### v23.6.1
package/README.md CHANGED
@@ -36,9 +36,9 @@ Start your API server with I/O schema validation and custom middlewares in minut
36
36
  2. [Headers as input source](#headers-as-input-source)
37
37
  3. [Response customization](#response-customization)
38
38
  4. [Empty response](#empty-response)
39
- 5. [Error handling](#error-handling)
40
- 6. [Production mode](#production-mode)
41
- 7. [Non-object response](#non-object-response) including file downloads
39
+ 5. [Non-JSON response](#non-json-response) including file downloads
40
+ 6. [Error handling](#error-handling)
41
+ 7. [Production mode](#production-mode)
42
42
  8. [HTML Forms (URL encoded)](#html-forms-url-encoded)
43
43
  9. [File uploads](#file-uploads)
44
44
  10. [Connect to your own express app](#connect-to-your-own-express-app)
@@ -58,8 +58,7 @@ Start your API server with I/O schema validation and custom middlewares in minut
58
58
  5. [Deprecated schemas and routes](#deprecated-schemas-and-routes)
59
59
  6. [Customizable brands handling](#customizable-brands-handling)
60
60
  8. [Caveats](#caveats)
61
- 1. [Coercive schema of Zod](#coercive-schema-of-zod)
62
- 2. [Excessive properties in endpoint output](#excessive-properties-in-endpoint-output)
61
+ 1. [Excessive properties in endpoint output](#excessive-properties-in-endpoint-output)
63
62
  9. [Your input to my output](#your-input-to-my-output)
64
63
 
65
64
  You can find the release notes and migration guides in [Changelog](CHANGELOG.md).
@@ -151,7 +150,8 @@ Much can be customized to fit your needs.
151
150
 
152
151
  - [Typescript](https://www.typescriptlang.org/) first.
153
152
  - Web server — [Express.js](https://expressjs.com/) v5.
154
- - Schema validation — [Zod 3.x](https://github.com/colinhacks/zod) including [Zod Plugin](#zod-plugin).
153
+ - Schema validation — [Zod 4.x](https://github.com/colinhacks/zod) including [Zod Plugin](#zod-plugin):
154
+ - For using with Zod 3.x install the framework versions below 24.0.0.
155
155
  - Supports any logger having `info()`, `debug()`, `error()` and `warn()` methods;
156
156
  - Built-in console logger with colorful and pretty inspections by default.
157
157
  - Generators:
@@ -169,7 +169,7 @@ Install the framework, its peer dependencies and type assistance packages using
169
169
 
170
170
  ```shell
171
171
  # example for yarn:
172
- yarn add express-zod-api express zod@3 typescript http-errors
172
+ yarn add express-zod-api express zod typescript http-errors
173
173
  yarn add -D @types/express @types/node @types/http-errors
174
174
  ```
175
175
 
@@ -214,7 +214,7 @@ import { defaultEndpointsFactory } from "express-zod-api";
214
214
  The endpoint responds with "Hello, World" or "Hello, {name}" if the name is supplied within `GET` request payload.
215
215
 
216
216
  ```typescript
217
- import { z } from "zod";
217
+ import { z } from "zod/v4";
218
218
 
219
219
  const helloWorldEndpoint = defaultEndpointsFactory.build({
220
220
  // method: "get" (default) or array ["get", "post", ...]
@@ -326,7 +326,7 @@ Inputs of middlewares are also available to endpoint handlers within `input`.
326
326
  Here is an example of the authentication middleware, that checks a `key` from input and `token` from headers:
327
327
 
328
328
  ```typescript
329
- import { z } from "zod";
329
+ import { z } from "zod/v4";
330
330
  import createHttpError from "http-errors";
331
331
  import { Middleware } from "express-zod-api";
332
332
 
@@ -456,7 +456,7 @@ You can implement additional validations within schemas using refinements.
456
456
  Validation errors are reported in a response with a status code `400`.
457
457
 
458
458
  ```typescript
459
- import { z } from "zod";
459
+ import { z } from "zod/v4";
460
460
  import { Middleware } from "express-zod-api";
461
461
 
462
462
  const nicknameConstraintMiddleware = new Middleware({
@@ -479,7 +479,7 @@ By the way, you can also refine the whole I/O object, for example in case you ne
479
479
  const endpoint = endpointsFactory.build({
480
480
  input: z
481
481
  .object({
482
- email: z.string().email().optional(),
482
+ email: z.email().optional(),
483
483
  id: z.string().optional(),
484
484
  otherThing: z.string().optional(),
485
485
  })
@@ -497,7 +497,7 @@ Since parameters of GET requests come in the form of strings, there is often a n
497
497
  arrays of numbers.
498
498
 
499
499
  ```typescript
500
- import { z } from "zod";
500
+ import { z } from "zod/v4";
501
501
 
502
502
  const getUserEndpoint = endpointsFactory.build({
503
503
  input: z.object({
@@ -528,7 +528,7 @@ Here is a recommended solution: it is important to use shallow transformations o
528
528
  ```ts
529
529
  import camelize from "camelize-ts";
530
530
  import snakify from "snakify-ts";
531
- import { z } from "zod";
531
+ import { z } from "zod/v4";
532
532
 
533
533
  const endpoint = endpointsFactory.build({
534
534
  input: z
@@ -562,7 +562,7 @@ in actual response by calling
562
562
  which in turn calls
563
563
  [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString).
564
564
  It is also impossible to transmit the `Date` in its original form to your endpoints within JSON. Therefore, there is
565
- confusion with original method ~~z.date()~~ that should not be used within IO schemas of your API.
565
+ confusion with original method ~~z.date()~~ that is not recommended to use without transformations.
566
566
 
567
567
  In order to solve this problem, the framework provides two custom methods for dealing with dates: `ez.dateIn()` and
568
568
  `ez.dateOut()` for using within input and output schemas accordingly.
@@ -578,20 +578,20 @@ provides your endpoint handler or middleware with a `Date`. It supports the foll
578
578
  ```
579
579
 
580
580
  `ez.dateOut()`, on the contrary, accepts a `Date` and provides `ResultHandler` with a `string` representation in ISO
581
- format for the response transmission. Consider the following simplified example for better understanding:
581
+ format for the response transmission. Both schemas accept metadata as an argument. Consider the following example:
582
582
 
583
583
  ```typescript
584
- import { z } from "zod";
584
+ import { z } from "zod/v4";
585
585
  import { ez, defaultEndpointsFactory } from "express-zod-api";
586
586
 
587
587
  const updateUserEndpoint = defaultEndpointsFactory.build({
588
588
  method: "post",
589
589
  input: z.object({
590
590
  userId: z.string(),
591
- birthday: ez.dateIn(), // string -> Date in handler
591
+ birthday: ez.dateIn({ examples: ["1963-04-21"] }), // string -> Date in handler
592
592
  }),
593
593
  output: z.object({
594
- createdAt: ez.dateOut(), // Date -> string in response
594
+ createdAt: ez.dateOut({ examples: ["2021-12-31"] }), // Date -> string in response
595
595
  }),
596
596
  handler: async ({ input }) => ({
597
597
  createdAt: new Date("2022-01-22"), // 2022-01-22T00:00:00.000Z
@@ -791,7 +791,7 @@ In a similar way you can enable request headers as the input source. This is an
791
791
 
792
792
  ```typescript
793
793
  import { createConfig, Middleware } from "express-zod-api";
794
- import { z } from "zod";
794
+ import { z } from "zod/v4";
795
795
 
796
796
  createConfig({
797
797
  inputSources: {
@@ -826,7 +826,7 @@ type DefaultResponse<OUT> =
826
826
  You can create your own result handler by using this example as a template:
827
827
 
828
828
  ```typescript
829
- import { z } from "zod";
829
+ import { z } from "zod/v4";
830
830
  import {
831
831
  ResultHandler,
832
832
  ensureHttpError,
@@ -872,6 +872,33 @@ const resultHandler = new ResultHandler({
872
872
  });
873
873
  ```
874
874
 
875
+ ## Non-JSON response
876
+
877
+ To configure a non-JSON responses (for example, to send an image file) you should specify its MIME type.
878
+
879
+ You can find two approaches to `EndpointsFactory` and `ResultHandler` implementation
880
+ [in this example](https://github.com/RobinTail/express-zod-api/blob/master/example/factories.ts).
881
+ One of them implements file streaming, in this case the endpoint just has to provide the filename.
882
+ The response schema can be `z.string()`, `z.base64()` or `ez.buffer()` to reflect the data accordingly in the
883
+ [generated documentation](#creating-a-documentation).
884
+
885
+ ```typescript
886
+ const fileStreamingEndpointsFactory = new EndpointsFactory(
887
+ new ResultHandler({
888
+ positive: { schema: ez.buffer(), mimeType: "image/*" },
889
+ negative: { schema: z.string(), mimeType: "text/plain" },
890
+ handler: ({ response, error, output }) => {
891
+ if (error) return void response.status(400).send(error.message);
892
+ if ("filename" in output)
893
+ fs.createReadStream(output.filename).pipe(
894
+ response.attachment(output.filename),
895
+ );
896
+ else response.status(400).send("Filename is missing");
897
+ },
898
+ }),
899
+ );
900
+ ```
901
+
875
902
  ## Error handling
876
903
 
877
904
  All runtime errors are handled by a `ResultHandler`. The default is `defaultResultHandler`. Using `ensureHttpError()`
@@ -915,34 +942,6 @@ createHttpError(500, "Something is broken"); // —> "Internal Server Error"
915
942
  createHttpError(501, "We didn't make it yet", { expose: true }); // —> "We didn't make it yet"
916
943
  ```
917
944
 
918
- ## Non-object response
919
-
920
- Thus, you can configure non-object responses too, for example, to send an image file.
921
-
922
- You can find two approaches to `EndpointsFactory` and `ResultHandler` implementation
923
- [in this example](https://github.com/RobinTail/express-zod-api/blob/master/example/factories.ts).
924
- One of them implements file streaming, in this case the endpoint just has to provide the filename.
925
- The response schema generally may be just `z.string()`, but I made more specific `ez.file()` that also supports
926
- `ez.file("binary")` and `ez.file("base64")` variants which are reflected in the
927
- [generated documentation](#creating-a-documentation).
928
-
929
- ```typescript
930
- const fileStreamingEndpointsFactory = new EndpointsFactory(
931
- new ResultHandler({
932
- positive: { schema: ez.file("buffer"), mimeType: "image/*" },
933
- negative: { schema: z.string(), mimeType: "text/plain" },
934
- handler: ({ response, error, output }) => {
935
- if (error) return void response.status(400).send(error.message);
936
- if ("filename" in output)
937
- fs.createReadStream(output.filename).pipe(
938
- response.type(output.filename),
939
- );
940
- else response.status(400).send("Filename is missing");
941
- },
942
- }),
943
- );
944
- ```
945
-
946
945
  ## HTML Forms (URL encoded)
947
946
 
948
947
  Use the proprietary schema `ez.form()` with an object shape or a custom `z.object()` with form fields in order to
@@ -952,13 +951,13 @@ which is `express.urlencoded()` by default. The request content type should be `
952
951
 
953
952
  ```ts
954
953
  import { defaultEndpointsFactory, ez } from "express-zod-api";
955
- import { z } from "zod";
954
+ import { z } from "zod/v4";
956
955
 
957
956
  export const submitFeedbackEndpoint = defaultEndpointsFactory.build({
958
957
  method: "post",
959
958
  input: ez.form({
960
959
  name: z.string().min(1),
961
- email: z.string().email(),
960
+ email: z.email(),
962
961
  message: z.string().min(1),
963
962
  }),
964
963
  });
@@ -992,7 +991,7 @@ const config = createConfig({
992
991
  Then use `ez.upload()` schema for a corresponding property. The request content type must be `multipart/form-data`:
993
992
 
994
993
  ```typescript
995
- import { z } from "zod";
994
+ import { z } from "zod/v4";
996
995
  import { ez, defaultEndpointsFactory } from "express-zod-api";
997
996
 
998
997
  const fileUploadEndpoint = defaultEndpointsFactory.build({
@@ -1070,7 +1069,7 @@ from outputs of previous middlewares, if the one being tested somehow depends on
1070
1069
  either by `errorHandler` configured within given `configProps` or `defaultResultHandler`.
1071
1070
 
1072
1071
  ```typescript
1073
- import { z } from "zod";
1072
+ import { z } from "zod/v4";
1074
1073
  import { Middleware, testMiddleware } from "express-zod-api";
1075
1074
 
1076
1075
  const middleware = new Middleware({
@@ -1110,7 +1109,7 @@ new ResultHandler({
1110
1109
  negative: [
1111
1110
  {
1112
1111
  statusCode: 409, // conflict: entity already exists
1113
- schema: z.object({ status: z.literal("exists"), id: z.number().int() }),
1112
+ schema: z.object({ status: z.literal("exists"), id: z.int() }),
1114
1113
  },
1115
1114
  {
1116
1115
  statusCode: [400, 500], // validation or internal error
@@ -1148,7 +1147,7 @@ const rawAcceptingEndpoint = defaultEndpointsFactory.build({
1148
1147
  input: ez.raw({
1149
1148
  /* the place for additional inputs, like route params, if needed */
1150
1149
  }),
1151
- output: z.object({ length: z.number().int().nonnegative() }),
1150
+ output: z.object({ length: z.int().nonnegative() }),
1152
1151
  handler: async ({ input: { raw } }) => ({
1153
1152
  length: raw.length, // raw is Buffer
1154
1153
  }),
@@ -1182,12 +1181,12 @@ Client application can subscribe to the event stream using `EventSource` class i
1182
1181
  the implementation emitting the `time` event each second.
1183
1182
 
1184
1183
  ```typescript
1185
- import { z } from "zod";
1184
+ import { z } from "zod/v4";
1186
1185
  import { EventStreamFactory } from "express-zod-api";
1187
1186
  import { setTimeout } from "node:timers/promises";
1188
1187
 
1189
1188
  const subscriptionEndpoint = new EventStreamFactory({
1190
- time: z.number().int().positive(),
1189
+ time: z.int().positive(),
1191
1190
  }).buildVoid({
1192
1191
  input: z.object({}), // optional input schema
1193
1192
  handler: async ({ options: { emit, isClosed } }) => {
@@ -1225,7 +1224,6 @@ import { Integration } from "express-zod-api";
1225
1224
  const client = new Integration({
1226
1225
  routing,
1227
1226
  variant: "client", // <— optional, see also "types" for a DIY solution
1228
- optionalPropStyle: { withQuestionMark: true, withUndefined: true }, // optional
1229
1227
  });
1230
1228
 
1231
1229
  const prettierFormattedTypescriptCode = await client.printFormatted(); // or just .print() for unformatted
@@ -1274,7 +1272,11 @@ const exampleEndpoint = defaultEndpointsFactory.build({
1274
1272
  shortDescription: "Retrieves the user.", // <—— this becomes the summary line
1275
1273
  description: "The detailed explanaition on what this endpoint does.",
1276
1274
  input: z.object({
1277
- id: z.number().describe("the ID of the user").example(123),
1275
+ id: z
1276
+ .string()
1277
+ .example("123") // input examples should be set before transformations
1278
+ .transform(Number)
1279
+ .describe("the ID of the user"),
1278
1280
  }),
1279
1281
  // ..., similarly for output and middlewares
1280
1282
  });
@@ -1323,7 +1325,7 @@ You can also deprecate all routes the `Endpoint` assigned to by setting `Endpoin
1323
1325
 
1324
1326
  ```ts
1325
1327
  import { Routing, DependsOnMethod } from "express-zod-api";
1326
- import { z } from "zod";
1328
+ import { z } from "zod/v4";
1327
1329
 
1328
1330
  const someEndpoint = factory.build({
1329
1331
  deprecated: true, // deprecates all routes the endpoint assigned to
@@ -1348,7 +1350,7 @@ need to reuse a handling rule for multiple brands, use the exposed types `Depict
1348
1350
 
1349
1351
  ```ts
1350
1352
  import ts from "typescript";
1351
- import { z } from "zod";
1353
+ import { z } from "zod/v4";
1352
1354
  import {
1353
1355
  Documentation,
1354
1356
  Integration,
@@ -1360,12 +1362,12 @@ const myBrand = Symbol("MamaToldMeImSpecial"); // I recommend to use symbols for
1360
1362
  const myBrandedSchema = z.string().brand(myBrand);
1361
1363
 
1362
1364
  const ruleForDocs: Depicter = (
1363
- schema: typeof myBrandedSchema, // you should assign type yourself
1364
- { next, path, method, isResponse }, // handle a nested schema using next()
1365
- ) => {
1366
- const defaultDepiction = next(schema.unwrap()); // { type: string }
1367
- return { summary: "Special type of data" };
1368
- };
1365
+ { zodSchema, jsonSchema }, // jsonSchema is the default depiction
1366
+ { path, method, isResponse },
1367
+ ) => ({
1368
+ ...jsonSchema,
1369
+ summary: "Special type of data",
1370
+ });
1369
1371
 
1370
1372
  const ruleForClient: Producer = (
1371
1373
  schema: typeof myBrandedSchema, // you should assign type yourself
@@ -1386,16 +1388,6 @@ new Integration({
1386
1388
  There are some well-known issues and limitations, or third party bugs that cannot be fixed in the usual way, but you
1387
1389
  should be aware of them.
1388
1390
 
1389
- ## Coercive schema of Zod
1390
-
1391
- Despite being supported by the framework, `z.coerce.*` schema
1392
- [does not work intuitively](https://github.com/RobinTail/express-zod-api/issues/759).
1393
- Please be aware that `z.coerce.number()` and `z.number({ coerce: true })` (being typed not well) still will NOT allow
1394
- you to assign anything but number. Moreover, coercive schemas are not fail-safe and their methods `.isOptional()` and
1395
- `.isNullable()` [are buggy](https://github.com/colinhacks/zod/issues/1911). If possible, try to avoid using this type
1396
- of schema. This issue [will NOT be fixed](https://github.com/colinhacks/zod/issues/1760#issuecomment-1407816838) in
1397
- Zod version 3.x.
1398
-
1399
1391
  ## Excessive properties in endpoint output
1400
1392
 
1401
1393
  The schema validator removes excessive properties by default. However, Typescript
@@ -1404,7 +1396,7 @@ in this case during development. You can achieve this verification by assigning
1404
1396
  reusing it in forced type of the output:
1405
1397
 
1406
1398
  ```typescript
1407
- import { z } from "zod";
1399
+ import { z } from "zod/v4";
1408
1400
 
1409
1401
  const output = z.object({
1410
1402
  anything: z.number(),
package/SECURITY.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  | Version | Code name | Release | Supported |
6
6
  | ------: | :------------ | :------ | :----------------: |
7
+ | 24.x.x | Ashley | 06.2025 | :white_check_mark: |
7
8
  | 23.x.x | Sonia | 04.2025 | :white_check_mark: |
8
9
  | 22.x.x | Tai | 01.2025 | :white_check_mark: |
9
10
  | 21.x.x | Kesaria | 11.2024 | :white_check_mark: |