Skip to content

Commit aab3814

Browse files
authored
1 parent a60c123 commit aab3814

24 files changed

Lines changed: 207 additions & 90 deletions

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
- The `numericRange` option removed from `Documentation` class constructor argument;
1717
- The `brandHandling` should consist of postprocessing functions altering the depiction made by Zod 4;
1818
- The `Depicter` type signature changed;
19-
- The `optionalPropStyle` option removed from `Integration` class constructor:
20-
- Use the new `z.interface()` schema to describe key-optional objects: https://v4.zod.dev/v4#zinterface;
19+
- The `optionalPropStyle` option removed from `Integration` class constructor.
2120
- Changes to the plugin:
2221
- Brand is the only kind of metadata that withstands refinements and checks.
2322

example/endpoints/retrieve-user.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { z } from "zod";
44
import { defaultEndpointsFactory } from "express-zod-api";
55
import { methodProviderMiddleware } from "../middlewares";
66

7-
// Demonstrating circular schemas using z.interface()
8-
const feature = z.interface({
7+
// Demonstrating circular schemas using z.object()
8+
const feature = z.object({
99
title: z.string(),
10-
get "features?"() {
11-
return z.array(feature);
10+
get features() {
11+
return z.array(feature).optional();
1212
},
1313
});
1414

example/example.client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
type Type1 = {
22
title: string;
3-
features?: Type1[];
3+
features?: Type1[] | undefined;
44
};
55

66
type SomeOf<T> = T[keyof T];

example/example.documentation.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ paths:
3636
properties:
3737
id:
3838
type: integer
39+
minimum: 0
3940
maximum: 9007199254740991
4041
name:
4142
type: string
@@ -269,6 +270,7 @@ paths:
269270
properties:
270271
id:
271272
type: integer
273+
exclusiveMinimum: 0
272274
exclusiveMaximum: 9007199254740991
273275
required:
274276
- id
@@ -290,6 +292,7 @@ paths:
290292
properties:
291293
id:
292294
type: integer
295+
exclusiveMinimum: 0
293296
exclusiveMaximum: 9007199254740991
294297
required:
295298
- id
@@ -476,6 +479,7 @@ paths:
476479
type: string
477480
size:
478481
type: integer
482+
minimum: 0
479483
maximum: 9007199254740991
480484
mime:
481485
type: string
@@ -550,6 +554,7 @@ paths:
550554
properties:
551555
length:
552556
type: integer
557+
minimum: 0
553558
maximum: 9007199254740991
554559
required:
555560
- length
@@ -607,6 +612,7 @@ paths:
607612
properties:
608613
data:
609614
type: integer
615+
exclusiveMinimum: 0
610616
exclusiveMaximum: 9007199254740991
611617
event:
612618
type: string
@@ -615,6 +621,7 @@ paths:
615621
type: string
616622
retry:
617623
type: integer
624+
exclusiveMinimum: 0
618625
exclusiveMaximum: 9007199254740991
619626
required:
620627
- data
@@ -668,6 +675,7 @@ paths:
668675
properties:
669676
crc:
670677
type: integer
678+
exclusiveMinimum: 0
671679
exclusiveMaximum: 9007199254740991
672680
required:
673681
- crc

express-zod-api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
"express-fileupload": "^1.5.0",
7777
"http-errors": "^2.0.0",
7878
"typescript": "^5.1.3",
79-
"zod": "^4.0.0-beta.20250503T014749"
79+
"zod": "^4.0.0-beta.20250505T012514"
8080
},
8181
"peerDependenciesMeta": {
8282
"@types/compression": {

express-zod-api/src/common-helpers.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,12 @@ export const getTransformedType = R.tryCatch(
170170
R.always(undefined),
171171
);
172172

173-
/** @link https://github.com/colinhacks/zod/issues/4159 */
173+
/**
174+
* @link https://github.com/colinhacks/zod/issues/4159
175+
* @todo replace undefined check with using using ._zod.optionality
176+
* @see https://github.com/RobinTail/express-zod-api/pull/2600/files#r2073174475
177+
* @link https://v4.zod.dev/v4/changelog#changes-zunknown-optionality
178+
* */
174179
export const doesAccept = R.tryCatch(
175180
(schema: $ZodType, value: undefined | null) => {
176181
z.parse(schema, value);

express-zod-api/src/deep-checks.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { $ZodType } from "@zod/core";
1+
import type { $ZodType, JSONSchema } from "@zod/core";
22
import * as R from "ramda";
33
import { globalRegistry, z } from "zod";
44
import { ezDateInBrand } from "./date-in-schema";
@@ -34,6 +34,30 @@ export const findNestedSchema = (
3434
(err: DeepCheckError) => err.cause,
3535
)();
3636

37+
/** not using cycle:"throw" because it also affects parenting objects */
38+
export const hasCycle = (
39+
subject: $ZodType,
40+
{ io }: Pick<NestedSchemaLookupProps, "io">,
41+
) => {
42+
const json = z.toJSONSchema(subject, {
43+
io,
44+
unrepresentable: "any",
45+
override: ({ jsonSchema }) => {
46+
if (typeof jsonSchema.default === "bigint") delete jsonSchema.default;
47+
},
48+
});
49+
const stack: unknown[] = [json];
50+
while (stack.length) {
51+
const entry = stack.shift()!;
52+
if (R.is(Object, entry)) {
53+
if ((entry as JSONSchema.BaseSchema).$ref === "#") return true;
54+
stack.push(...R.values(entry));
55+
}
56+
if (R.is(Array, entry)) stack.push(...R.values(entry));
57+
}
58+
return false;
59+
};
60+
3761
export const findRequestTypeDefiningSchema = (subject: IOSchema) =>
3862
findNestedSchema(subject, {
3963
condition: (schema) => {

express-zod-api/src/documentation-helpers.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,15 @@ const samples = {
101101
export const reformatParamsInPath = (path: string) =>
102102
path.replace(routePathParamsRegex, (param) => `{${param.slice(1)}}`);
103103

104-
export const depictDefault: Depicter = ({ zodSchema, jsonSchema }) => ({
105-
...jsonSchema,
106-
default:
104+
export const depictDefault: Depicter = ({ zodSchema, jsonSchema }) => {
105+
const value =
107106
globalRegistry.get(zodSchema)?.[metaSymbol]?.defaultLabel ??
108-
jsonSchema.default,
109-
});
107+
jsonSchema.default;
108+
return {
109+
...jsonSchema,
110+
default: typeof value === "bigint" ? String(value) : value,
111+
};
112+
};
110113

111114
export const depictUpload: Depicter = ({}, ctx) => {
112115
if (ctx.isResponse)

express-zod-api/src/metadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as R from "ramda";
55
export const metaSymbol = Symbol.for("express-zod-api");
66

77
export interface Metadata {
8-
examples: z.$input[];
8+
examples: unknown[];
99
/** @override ZodDefault::_zod.def.defaultValue() in depictDefault */
1010
defaultLabel?: string;
1111
brand?: string | number | symbol;

express-zod-api/src/zod-plugin.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { z, globalRegistry } from "zod";
1313
import { FlatObject } from "./common-helpers";
1414
import { Metadata, metaSymbol } from "./metadata";
1515
import { Intact, Remap } from "./mapping-helpers";
16-
import type { $ZodType, $ZodShape } from "@zod/core";
16+
import type { $ZodType, $ZodShape, $ZodLooseShape } from "@zod/core";
1717

1818
declare module "@zod/core" {
1919
interface GlobalMeta {
@@ -34,8 +34,9 @@ declare module "zod" {
3434
}
3535
interface ZodObject<
3636
// @ts-expect-error -- external issue
37-
out Shape extends $ZodShape = $ZodShape,
38-
Extra extends Record<string, unknown> = Record<string, unknown>,
37+
out Shape extends $ZodShape = $ZodLooseShape,
38+
OutExtra extends Record<string, unknown> = Record<string, unknown>,
39+
InExtra extends Record<string, unknown> = Record<string, unknown>,
3940
> extends ZodType {
4041
remap<V extends string, U extends { [P in keyof Shape]?: V }>(
4142
mapping: U,
@@ -44,7 +45,7 @@ declare module "zod" {
4445
this,
4546
z.ZodTransform<FlatObject, FlatObject> // internal type simplified
4647
>,
47-
z.ZodObject<Remap<Shape, U, V> & Intact<Shape, U>, Extra>
48+
z.ZodObject<Remap<Shape, U, V> & Intact<Shape, U>, OutExtra, InExtra>
4849
>;
4950
remap<U extends $ZodShape>(
5051
mapper: (subject: Shape) => U,
@@ -110,7 +111,6 @@ const objectMapper = function (
110111
const nextShape = transformer(R.clone(this._zod.def.shape)); // immutable
111112
const hasPassThrough = this._zod.def.catchall instanceof z.ZodUnknown;
112113
const output = (hasPassThrough ? z.looseObject : z.object)(nextShape); // proxies unknown keys when set to "passthrough"
113-
// @ts-expect-error -- ignoring inconsistency of Extra type
114114
return this.transform(transformer).pipe(output);
115115
};
116116

0 commit comments

Comments
 (0)