chore(server): fully remove remaining zod

This commit is contained in:
Corentin Thomasset 2026-04-18 18:12:05 +02:00
parent 2b0c258f8a
commit 3e6056972c
No known key found for this signature in database
GPG key ID: 3A871907120CFC5F
5 changed files with 13 additions and 217 deletions

View file

@ -28,7 +28,8 @@
"tailwind-merge": "^2.6.0",
"unocss-preset-animations": "catalog:",
"valibot": "catalog:",
"yaml": "^2.8.0"
"yaml": "^2.8.0",
"zod": "3.25.76"
},
"devDependencies": {
"@antfu/eslint-config": "catalog:",

View file

@ -83,8 +83,7 @@
"sanitize-html": "^2.17.0",
"stripe": "^17.7.0",
"tsx": "catalog:",
"valibot": "catalog:",
"zod": "^3.25.67"
"valibot": "catalog:"
},
"devDependencies": {
"@antfu/eslint-config": "catalog:",

View file

@ -1,146 +0,0 @@
import { Hono } from 'hono';
import { describe, expect, test } from 'vitest';
import { z } from 'zod';
import { legacyValidateJsonBody, legacyValidateParams, legacyValidateQuery } from './validation.legacy';
function makeJsonBodyPayload(payload: Record<string, unknown>) {
return {
body: JSON.stringify(payload),
headers: new Headers({ 'Content-Type': 'application/json' }),
};
}
describe('validation', () => {
describe('validateJsonBody', () => {
describe('validateJsonBody creates a validation middleware that check the request json body against a schema', async () => {
test('an invalid payload should trigger a 400 error', async () => {
const app = new Hono().post('/', legacyValidateJsonBody(z.object({ name: z.string({ required_error: 'The name is required' }) })), (context) => {
return context.json({ ok: true });
});
const response = await app.request('/', { method: 'POST', ...makeJsonBodyPayload({ }) });
const responseBody = await response.json();
expect(response.status).toBe(400);
expect(responseBody).toEqual({
error: {
message: 'Invalid request body',
code: 'server.invalid_request.body',
details: [{
path: 'name',
message: 'The name is required',
}],
},
});
});
test('a valid request should pass through', async () => {
const app = new Hono().post('/', legacyValidateJsonBody(z.object({ name: z.string() })), (context) => {
return context.json({ ok: true });
});
const response = await app.request('/', { method: 'POST', ...makeJsonBodyPayload({ name: 'hono' }) });
const responseBody = await response.json();
expect(response.status).toBe(200);
expect(responseBody).toEqual({ ok: true });
});
test('no additional properties should be allowed', async () => {
const app = new Hono().post('/', legacyValidateJsonBody(z.object({ name: z.string() })), (context) => {
return context.json({ ok: true });
});
const response = await app.request('/', { method: 'POST', ...makeJsonBodyPayload({ name: 'hono', foo: 'bar' }) });
const responseBody = await response.json();
expect(response.status).toBe(400);
expect(responseBody).toEqual({
error: {
message: 'Invalid request body',
code: 'server.invalid_request.body',
details: [{
message: 'Unrecognized key(s) in object: \'foo\'',
}],
},
});
});
});
});
describe('validateQuery', () => {
describe('validateQuery creates a validation middleware that check the request query parameters against a schema', async () => {
test('an invalid query should trigger a 400 error', async () => {
const app = new Hono().get('/', legacyValidateQuery(z.object({ name: z.string({ required_error: 'The name is required' }) })), (context) => {
return context.json({ ok: true });
});
const response = await app.request('/');
const responseBody = await response.json();
expect(response.status).toBe(400);
expect(responseBody).toEqual({
error: {
message: 'Invalid query parameters',
code: 'server.invalid_request.query',
details: [{
path: 'name',
message: 'The name is required',
}],
},
});
});
test('a valid query should pass through', async () => {
const app = new Hono().get('/', legacyValidateQuery(z.object({ name: z.string() })), (context) => {
return context.json({ ok: true });
});
const response = await app.request('/?name=hono');
const responseBody = await response.json();
expect(response.status).toBe(200);
expect(responseBody).toEqual({ ok: true });
});
});
});
describe('validateParams', () => {
describe('validateParams creates a validation middleware that check the request url parameters against a schema', async () => {
test('an invalid params should trigger a 400 error', async () => {
const app = new Hono().get('/:name', legacyValidateParams(z.object({ name: z.string().startsWith('foo-') })), (context) => {
return context.json({ ok: true });
});
const response = await app.request('/test');
const responseBody = await response.json();
expect(response.status).toBe(400);
expect(responseBody).toEqual({
error: {
message: 'Invalid URL parameters',
code: 'server.invalid_request.params',
details: [
{
path: 'name',
message: 'Invalid input: must start with "foo-"',
},
],
},
});
});
test('a valid params should pass through', async () => {
const app = new Hono().get('/:name', legacyValidateParams(z.object({ name: z.string().startsWith('foo-') })), (context) => {
return context.json({ ok: true });
});
const response = await app.request('/foo-bar');
const responseBody = await response.json();
expect(response.status).toBe(200);
expect(responseBody).toEqual({ ok: true });
});
});
});
});

View file

@ -1,48 +0,0 @@
import type { ValidationTargets } from 'hono';
import type z from 'zod';
import { validator } from 'hono/validator';
function formatValidationError({ error }: { error: z.ZodError }) {
const details = (error.errors ?? []).map((e) => {
return {
...(e.path.length === 0 ? {} : { path: e.path.join('.') }),
message: e.message,
};
});
return details;
}
function buildLegacyValidator<Target extends keyof ValidationTargets>({ target, error }: { target: Target; error: { message: string; code: string } }) {
return <Schema extends z.ZodTypeAny>(schema: Schema, { allowAdditionalFields = false }: { allowAdditionalFields?: boolean } = {}) => {
return validator(target, (value, context) => {
// @ts-expect-error try to enforce strict mode
// eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-call
const refinedSchema: Schema = allowAdditionalFields ? schema : (schema.strict?.() ?? schema);
const result = refinedSchema.safeParse(value);
if (result.success) {
// eslint-disable-next-line ts/no-unsafe-return
return result.data as z.infer<Schema>;
}
const details = formatValidationError({ error: result.error });
return context.json(
{
error: {
...error,
details,
},
},
400,
);
});
};
}
export const legacyValidateJsonBody = buildLegacyValidator({ target: 'json', error: { message: 'Invalid request body', code: 'server.invalid_request.body' } });
export const legacyValidateQuery = buildLegacyValidator({ target: 'query', error: { message: 'Invalid query parameters', code: 'server.invalid_request.query' } });
export const legacyValidateParams = buildLegacyValidator({ target: 'param', error: { message: 'Invalid URL parameters', code: 'server.invalid_request.params' } });
export const legacyValidateFormData = buildLegacyValidator({ target: 'form', error: { message: 'Invalid form data', code: 'server.invalid_request.form_data' } });

View file

@ -116,6 +116,9 @@ importers:
yaml:
specifier: ^2.8.0
version: 2.8.2
zod:
specifier: 3.25.76
version: 3.25.76
devDependencies:
'@antfu/eslint-config':
specifier: 'catalog:'
@ -152,7 +155,7 @@ importers:
dependencies:
'@better-auth/expo':
specifier: 'catalog:'
version: 1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@3.25.76))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1))(better-auth@1.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.10)(vue@3.5.13(typescript@5.9.3)))(expo-constants@18.0.10)(expo-linking@8.0.8)(expo-network@8.0.8(expo@54.0.23)(react@19.1.0))(expo-web-browser@15.0.9(expo@54.0.23)(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0)))
version: 1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.12))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1))(better-auth@1.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.10)(vue@3.5.13(typescript@5.9.3)))(expo-constants@18.0.10)(expo-linking@8.0.8)(expo-network@8.0.8(expo@54.0.23)(react@19.1.0))(expo-web-browser@15.0.9(expo@54.0.23)(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0)))
'@corentinth/chisels':
specifier: 'catalog:'
version: 2.1.0
@ -439,7 +442,7 @@ importers:
version: 12.27.0
'@better-auth/expo':
specifier: 'catalog:'
version: 1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@3.25.76))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1))(better-auth@1.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.10)(vue@3.5.13(typescript@5.9.3)))(expo-constants@18.0.10)(expo-linking@8.0.8)(expo-network@8.0.8(expo@54.0.23)(react@19.1.0))(expo-web-browser@15.0.9(expo@54.0.23)(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0)))
version: 1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.12))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1))(better-auth@1.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.10)(vue@3.5.13(typescript@5.9.3)))(expo-constants@18.0.10)(expo-linking@8.0.8)(expo-network@8.0.8(expo@54.0.23)(react@19.1.0))(expo-web-browser@15.0.9(expo@54.0.23)(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0)))
'@cadence-mq/core':
specifier: ^0.2.1
version: 0.2.1
@ -560,9 +563,6 @@ importers:
valibot:
specifier: 'catalog:'
version: 1.3.1(typescript@5.9.3)
zod:
specifier: ^3.25.67
version: 3.25.76
devDependencies:
'@antfu/eslint-config':
specifier: 'catalog:'
@ -5511,6 +5511,7 @@ packages:
'@xmldom/xmldom@0.8.11':
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
engines: {node: '>=10.0.0'}
deprecated: this version has critical issues, please update to the latest version
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
@ -13782,7 +13783,7 @@ snapshots:
'@bcoe/v8-coverage@1.0.2': {}
'@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@3.25.76))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)':
'@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.12))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)':
dependencies:
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.18
@ -13793,20 +13794,9 @@ snapshots:
nanostores: 1.0.1
zod: 4.1.12
'@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.12))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)':
'@better-auth/expo@1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.12))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1))(better-auth@1.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.10)(vue@3.5.13(typescript@5.9.3)))(expo-constants@18.0.10)(expo-linking@8.0.8)(expo-network@8.0.8(expo@54.0.23)(react@19.1.0))(expo-web-browser@15.0.9(expo@54.0.23)(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0)))':
dependencies:
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.18
'@standard-schema/spec': 1.0.0
better-call: 1.1.5(zod@4.1.12)
jose: 6.1.0
kysely: 0.28.8
nanostores: 1.0.1
zod: 4.1.12
'@better-auth/expo@1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@3.25.76))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1))(better-auth@1.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.10)(vue@3.5.13(typescript@5.9.3)))(expo-constants@18.0.10)(expo-linking@8.0.8)(expo-network@8.0.8(expo@54.0.23)(react@19.1.0))(expo-web-browser@15.0.9(expo@54.0.23)(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0)))':
dependencies:
'@better-auth/core': 1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@3.25.76))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)
'@better-auth/core': 1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.12))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)
'@better-fetch/fetch': 1.1.18
better-auth: 1.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.10)(vue@3.5.13(typescript@5.9.3))
better-call: 1.1.5(zod@4.1.12)
@ -18538,7 +18528,7 @@ snapshots:
'@better-fetch/fetch': 1.1.18
'@noble/ciphers': 2.0.1
'@noble/hashes': 2.0.1
better-call: 1.1.5(zod@4.1.12)
better-call: 1.1.5(zod@3.25.76)
defu: 6.1.4
jose: 6.1.0
kysely: 0.28.8