Skip to content

Commit 3904cce

Browse files
blomqmaJulien Lestavel
andcommitted
feat: migrate to Zod v4 and harden schema tooling
- upgrade workspace to Zod 4.1.13 and refresh the lockfile - fix schema detection, JSON conversion, and middleware handling regressions - add coverage for shared schema helpers plus app/router and RPC edge cases Co-authored-by: Julien Lestavel <[email protected]>
1 parent 43287fd commit 3904cce

File tree

21 files changed

+441
-125
lines changed

21 files changed

+441
-125
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- uses: actions/checkout@v3
1717
- uses: pnpm/action-setup@v2
1818
with:
19-
version: 7
19+
version: 9
2020
- uses: actions/setup-node@v3
2121
with:
2222
node-version: ${{ matrix.node }}

apps/example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"jsdom": "24.0.0",
1717
"next-rest-framework": "workspace:*",
1818
"tsx": "4.7.2",
19-
"zod-form-data": "2.0.2"
19+
"zod-form-data": "3.0.1"
2020
},
2121
"devDependencies": {
2222
"@types/jsdom": "^21.1.6",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"next": "15.1.6",
3030
"prettier": "3.0.2",
3131
"typescript": "5.2.2",
32-
"zod": "3.22.2"
32+
"zod": "^4.1.13"
3333
},
3434
"prettier": {
3535
"semi": true,

packages/next-rest-framework/package.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@
4141
"formidable": "^3.5.1",
4242
"lodash": "4.17.21",
4343
"prettier": "3.0.2",
44-
"qs": "6.11.2",
45-
"zod-to-json-schema": "3.21.4"
44+
"qs": "6.11.2"
4645
},
4746
"devDependencies": {
4847
"@types/formidable": "^3.4.5",
@@ -58,7 +57,7 @@
5857
"ts-node": "10.9.1",
5958
"tsup": "8.0.1",
6059
"typescript": "*",
61-
"zod": "*",
62-
"zod-form-data": "*"
60+
"zod": "^4.1.13",
61+
"zod-form-data": "3.0.1"
6362
}
6463
}

packages/next-rest-framework/src/app-router/route-operation.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
type ContentTypesThatSupportInputValidation
1818
} from '../types';
1919
import { NextResponse, type NextRequest } from 'next/server';
20-
import { type ZodSchema, type z } from 'zod';
20+
import { type ZodType, type z } from 'zod';
2121
import { type ValidMethod } from '../constants';
2222
import { type I18NConfig } from 'next/dist/server/config-shared';
2323
import { type NextURL } from 'next/dist/server/web/next-url';
@@ -200,14 +200,14 @@ interface InputObject<
200200
body?: ContentType extends ContentTypesThatSupportInputValidation
201201
? ContentType extends FormDataContentType
202202
? ZodFormSchema<Body>
203-
: ZodSchema<Body>
203+
: ZodType<Body>
204204
: never;
205205
/*! If defined, this will override the body schema for the OpenAPI spec. */
206206
bodySchema?: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject;
207-
query?: ZodSchema<Query>;
207+
query?: ZodType<Query>;
208208
/*! If defined, this will override the query schema for the OpenAPI spec. */
209209
querySchema?: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject;
210-
params?: ZodSchema<Params>;
210+
params?: ZodType<Params>;
211211
/*! If defined, this will override the params schema for the OpenAPI spec. */
212212
paramsSchema?: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject;
213213
}

packages/next-rest-framework/src/app-router/route.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
6565
let middlewareOptions: BaseOptions = {};
6666

6767
if (middleware1) {
68-
const res = await middleware1(reqClone, {...context, params: await context.params}, middlewareOptions);
68+
const res = await middleware1(
69+
reqClone,
70+
{ ...context, params: await context.params },
71+
middlewareOptions
72+
);
6973

7074
const isOptionsResponse = (res: unknown): res is BaseOptions =>
7175
typeof res === 'object';
@@ -77,7 +81,11 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
7781
}
7882

7983
if (middleware2) {
80-
const res2 = await middleware2(reqClone, {...context, params: await context.params}, middlewareOptions);
84+
const res2 = await middleware2(
85+
reqClone,
86+
{ ...context, params: await context.params },
87+
middlewareOptions
88+
);
8189

8290
if (res2 instanceof Response) {
8391
return res2;
@@ -88,7 +96,7 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
8896
if (middleware3) {
8997
const res3 = await middleware3(
9098
reqClone,
91-
{...context, params: await context.params},
99+
{ ...context, params: await context.params },
92100
middlewareOptions
93101
);
94102

@@ -165,16 +173,16 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
165173
try {
166174
const formData = await reqClone.formData();
167175

168-
const { valid, errors, data } = validateSchema({
176+
const result = validateSchema({
169177
schema: bodySchema,
170178
obj: formData
171179
});
172180

173-
if (!valid) {
181+
if (!result.valid) {
174182
return NextResponse.json(
175183
{
176184
message: DEFAULT_ERRORS.invalidRequestBody,
177-
errors
185+
errors: result.errors
178186
},
179187
{
180188
status: 400
@@ -186,14 +194,16 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
186194
reqClone = new NextRequest(reqClone.url, {
187195
method: reqClone.method,
188196
headers: reqClone.headers,
189-
body: JSON.stringify(data)
197+
body: JSON.stringify(result.data)
190198
});
191199

192200
// Return parsed form data.
193201
reqClone.formData = async () => {
194202
const formData = new FormData();
195203

196-
for (const [key, value] of Object.entries(data)) {
204+
for (const [key, value] of Object.entries(
205+
result.data as Record<string, unknown>
206+
)) {
197207
formData.append(key, value as string | Blob);
198208
}
199209

@@ -239,7 +249,7 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
239249
url.searchParams.delete(key);
240250

241251
if (data[key]) {
242-
url.searchParams.append(key, data[key]);
252+
url.searchParams.append(key, data[key] as string);
243253
}
244254
});
245255

@@ -268,13 +278,13 @@ export const route = <T extends Record<string, RouteOperationDefinition>>(
268278
);
269279
}
270280

271-
context.params = data;
281+
context.params = Promise.resolve(data);
272282
}
273283
}
274284

275285
const res = await handler?.(
276286
reqClone as TypedNextRequest,
277-
{...context, params: await context.params},
287+
{ ...context, params: await context.params },
278288
middlewareOptions
279289
);
280290

packages/next-rest-framework/src/app-router/rpc-route.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,19 @@ export const rpcRoute = <
8888
let middlewareOptions: BaseOptions = {};
8989

9090
if (middleware1) {
91-
middlewareOptions = await middleware1(body, middlewareOptions);
91+
const res1 = await middleware1(body, middlewareOptions);
92+
if (res1 && typeof res1 === 'object')
93+
middlewareOptions = res1 as BaseOptions;
9294

9395
if (middleware2) {
94-
middlewareOptions = await middleware2(body, middlewareOptions);
96+
const res2 = await middleware2(body, middlewareOptions);
97+
if (res2 && typeof res2 === 'object')
98+
middlewareOptions = res2 as BaseOptions;
9599

96100
if (middleware3) {
97-
middlewareOptions = await middleware3(body, middlewareOptions);
101+
const res3 = await middleware3(body, middlewareOptions);
102+
if (res3 && typeof res3 === 'object')
103+
middlewareOptions = res3 as BaseOptions;
98104
}
99105
}
100106
}
@@ -147,16 +153,16 @@ export const rpcRoute = <
147153
)
148154
) {
149155
try {
150-
const { valid, errors, data } = validateSchema({
156+
const result = validateSchema({
151157
schema: bodySchema,
152158
obj: body
153159
});
154160

155-
if (!valid) {
161+
if (!result.valid) {
156162
return NextResponse.json(
157163
{
158164
message: DEFAULT_ERRORS.invalidRequestBody,
159-
errors
165+
errors: result.errors
160166
},
161167
{
162168
status: 400
@@ -166,7 +172,9 @@ export const rpcRoute = <
166172

167173
const formData = new FormData();
168174

169-
for (const [key, value] of Object.entries(data)) {
175+
for (const [key, value] of Object.entries(
176+
result.data as Record<string, unknown>
177+
)) {
170178
formData.append(key, value as string | Blob);
171179
}
172180

packages/next-rest-framework/src/pages-router/api-route-operation.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
type BaseParams
1919
} from '../types';
2020
import { type NextApiRequest, type NextApiResponse } from 'next/types';
21-
import { type ZodSchema, type z } from 'zod';
21+
import { type ZodType, type z } from 'zod';
2222

2323
export type TypedNextApiRequest<
2424
Method = keyof typeof ValidMethod,
@@ -135,14 +135,14 @@ interface InputObject<
135135
body?: ContentType extends ContentTypesThatSupportInputValidation
136136
? ContentType extends FormDataContentType
137137
? ZodFormSchema<Body>
138-
: ZodSchema<Body>
138+
: ZodType<Body>
139139
: never;
140140
/*! If defined, this will override the body schema for the OpenAPI spec. */
141141
bodySchema?: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject;
142-
query?: ZodSchema<Query>;
142+
query?: ZodType<Query>;
143143
/*! If defined, this will override the query schema for the OpenAPI spec. */
144144
querySchema?: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject;
145-
params?: ZodSchema<Params>;
145+
params?: ZodType<Params>;
146146
/*! If defined, this will override the params schema for the OpenAPI spec. */
147147
paramsSchema?: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject;
148148
}

packages/next-rest-framework/src/pages-router/api-route.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,11 @@ export const apiRoute = <T extends Record<string, ApiRouteOperationDefinition>>(
176176

177177
const formData = new FormData();
178178

179-
Object.entries(data).forEach(([key, value]) => {
180-
formData.append(key, value as string | Blob);
181-
});
179+
Object.entries(data as Record<string, unknown>).forEach(
180+
([key, value]) => {
181+
formData.append(key, value as string | Blob);
182+
}
183+
);
182184

183185
req.body = formData;
184186
} catch {

packages/next-rest-framework/src/pages-router/rpc-api-route.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,19 @@ export const rpcApiRoute = <
6262
let middlewareOptions: BaseOptions = {};
6363

6464
if (middleware1) {
65-
middlewareOptions = await middleware1(req.body, {});
65+
const res1 = await middleware1(req.body, {});
66+
if (res1 && typeof res1 === 'object')
67+
middlewareOptions = res1 as BaseOptions;
6668

6769
if (middleware2) {
68-
middlewareOptions = await middleware2(req.body, middlewareOptions);
70+
const res2 = await middleware2(req.body, middlewareOptions);
71+
if (res2 && typeof res2 === 'object')
72+
middlewareOptions = res2 as BaseOptions;
6973

7074
if (middleware3) {
71-
middlewareOptions = await middleware3(req.body, middlewareOptions);
75+
const res3 = await middleware3(req.body, middlewareOptions);
76+
if (res3 && typeof res3 === 'object')
77+
middlewareOptions = res3 as BaseOptions;
7278
}
7379
}
7480
}
@@ -128,25 +134,27 @@ export const rpcApiRoute = <
128134
}
129135

130136
try {
131-
const { valid, errors, data } = validateSchema({
137+
const result = validateSchema({
132138
schema: bodySchema,
133139
obj: req.body
134140
});
135141

136-
if (!valid) {
142+
if (!result.valid) {
137143
res.status(400).json({
138144
message: DEFAULT_ERRORS.invalidRequestBody,
139-
errors
145+
errors: result.errors
140146
});
141147

142148
return;
143149
}
144150

145151
const formData = new FormData();
146152

147-
Object.entries(data).forEach(([key, value]) => {
148-
formData.append(key, value as string | Blob);
149-
});
153+
Object.entries(result.data as Record<string, unknown>).forEach(
154+
([key, value]) => {
155+
formData.append(key, value as string | Blob);
156+
}
157+
);
150158

151159
req.body = formData;
152160
} catch {

0 commit comments

Comments
 (0)