-
Notifications
You must be signed in to change notification settings - Fork 291
feat(standard-validator): Add standard schema validation #887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
45cff3f
4b94c5b
4c397bb
e9c47b7
0cbad7f
f926fc5
58693dd
a1e204e
c50da6a
5a4ae90
7edb69b
986c34f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@hono/standard-validator': minor | ||
| --- | ||
|
|
||
| Initial implementation for Standard Schema support |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| name: ci-standard-validator | ||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - 'packages/standard-validator/**' | ||
| pull_request: | ||
| branches: ['*'] | ||
| paths: | ||
| - 'packages/standard-validator/**' | ||
|
|
||
| jobs: | ||
| ci: | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| working-directory: ./packages/standard-validator | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20.x | ||
| - run: yarn install --frozen-lockfile | ||
| - run: yarn build | ||
| - run: yarn test |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,4 +48,4 @@ | |
| "engines": { | ||
| "node": ">=18.14.1" | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # Standard Schema validator middleware for Hono | ||
|
|
||
| The validator middleware using [Standard Schema Spec](https://github.com/standard-schema/standard-schema) for [Hono](https://honojs.dev) applications. | ||
| You can write a schema with any validation library supporting Standard Schema and validate the incoming values. | ||
|
|
||
| ## Usage | ||
|
|
||
|
|
||
| ### Basic: | ||
| ```ts | ||
| import { z } from 'zod' | ||
| import { sValidator } from '@hono/standard-validator' | ||
|
|
||
| const schema = z.object({ | ||
| name: z.string(), | ||
| age: z.number(), | ||
| }); | ||
|
|
||
| app.post('/author', sValidator('json', schema), (c) => { | ||
| const data = c.req.valid('json') | ||
| return c.json({ | ||
| success: true, | ||
| message: `${data.name} is ${data.age}`, | ||
| }) | ||
| }) | ||
| ``` | ||
|
|
||
| ### Hook: | ||
| ```ts | ||
| app.post( | ||
| '/post', | ||
| sValidator('json', schema, (result, c) => { | ||
| if (!result.success) { | ||
| return c.text('Invalid!', 400) | ||
| } | ||
| }) | ||
| //... | ||
| ) | ||
| ``` | ||
|
|
||
| ### Headers: | ||
| Headers are internally transformed to lower-case in Hono. Hence, you will have to make them lower-cased in validation object. | ||
| ```ts | ||
| import { object, string } from 'valibot' | ||
| import { sValidator } from '@hono/standard-validator' | ||
|
|
||
| const schema = object({ | ||
| 'content-type': string(), | ||
| 'user-agent': string() | ||
| }); | ||
|
|
||
| app.post('/author', sValidator('header', schema), (c) => { | ||
| const headers = c.req.valid('header') | ||
| // do something with headers | ||
| }) | ||
| ``` | ||
|
|
||
|
|
||
| ## Author | ||
|
|
||
| Rokas Muningis <https://github.com/muningis> | ||
|
|
||
| ## License | ||
|
|
||
| MIT |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| { | ||
| "name": "@hono/standard-validator", | ||
| "version": "0.0.0", | ||
muningis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "description": "Validator middleware using Standard Schema", | ||
| "type": "module", | ||
| "main": "dist/index.cjs", | ||
| "module": "dist/index.js", | ||
| "types": "dist/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "import": "./dist/index.js", | ||
| "require": "./dist/index.cjs" | ||
| } | ||
| }, | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "scripts": { | ||
| "test": "tsc --noEmit && vitest --run", | ||
yusukebe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "build": "tsup ./src/index.ts --format esm,cjs --dts", | ||
| "publint": "publint", | ||
| "prerelease": "yarn build && yarn test", | ||
| "release": "yarn publish" | ||
| }, | ||
| "license": "MIT", | ||
| "publishConfig": { | ||
| "registry": "https://registry.npmjs.org", | ||
| "access": "public" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/honojs/middleware.git" | ||
| }, | ||
| "homepage": "https://github.com/honojs/middleware", | ||
| "peerDependencies": { | ||
| "@standard-schema/spec": "1.0.0", | ||
| "hono": ">=3.9.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@standard-schema/spec": "1.0.0", | ||
| "arktype": "^2.0.0-rc.26", | ||
| "hono": "^4.0.10", | ||
| "publint": "^0.2.7", | ||
| "tsup": "^8.1.0", | ||
| "typescript": "^5.7.3", | ||
| "valibot": "^1.0.0-beta.9", | ||
| "vitest": "^1.4.0", | ||
| "zod": "^3.24.0" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono' | ||
| import { validator } from 'hono/validator' | ||
| import type { StandardSchemaV1 } from '@standard-schema/spec' | ||
|
|
||
| type HasUndefined<T> = undefined extends T ? true : false | ||
| type TOrPromiseOfT<T> = T | Promise<T> | ||
|
|
||
| type Hook< | ||
| T, | ||
| E extends Env, | ||
| P extends string, | ||
| Target extends keyof ValidationTargets = keyof ValidationTargets, | ||
| O = {} | ||
| > = ( | ||
| result: ( | ||
| | { success: boolean; data: T } | ||
| | { success: boolean; error: ReadonlyArray<StandardSchemaV1.Issue>; data: T } | ||
| ) & { | ||
| target: Target | ||
| }, | ||
| c: Context<E, P> | ||
| ) => TOrPromiseOfT<Response | void | TypedResponse<O>> | ||
|
|
||
| const isStandardSchemaValidator = (validator: unknown): validator is StandardSchemaV1 => | ||
| !!validator && typeof validator === 'object' && '~standard' in validator | ||
|
|
||
| const sValidator = < | ||
| Schema extends StandardSchemaV1, | ||
| Target extends keyof ValidationTargets, | ||
| E extends Env, | ||
| P extends string, | ||
| In = StandardSchemaV1.InferInput<Schema>, | ||
| Out = StandardSchemaV1.InferOutput<Schema>, | ||
| I extends Input = { | ||
| in: HasUndefined<In> extends true | ||
| ? { | ||
| [K in Target]?: In extends ValidationTargets[K] | ||
| ? In | ||
| : { [K2 in keyof In]?: ValidationTargets[K][K2] } | ||
| } | ||
| : { | ||
| [K in Target]: In extends ValidationTargets[K] | ||
| ? In | ||
| : { [K2 in keyof In]: ValidationTargets[K][K2] } | ||
| } | ||
| out: { [K in Target]: Out } | ||
| }, | ||
| V extends I = I | ||
| >( | ||
| target: Target, | ||
| schema: Schema, | ||
| hook?: Hook<StandardSchemaV1.InferOutput<Schema>, E, P, Target> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Valibot and Zod has different
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @yusukebe should this be uniformed?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It's better to unify the implementation. Following Zod Validator is good. The current implementation in this PR is okay, but we should change the implementation of the Valibot Validator. |
||
| ): MiddlewareHandler<E, P, V> => | ||
| // @ts-expect-error not typed well | ||
| validator(target, async (value, c) => { | ||
| const result = await schema['~standard'].validate(value) | ||
|
|
||
| if (hook) { | ||
| const hookResult = await hook( | ||
| !!result.issues | ||
| ? { data: value, error: result.issues, success: false, target } | ||
| : { data: value, success: true, target }, | ||
| c | ||
| ) | ||
| if (hookResult) { | ||
| if (hookResult instanceof Response) { | ||
| return hookResult | ||
| } | ||
|
|
||
| if ('response' in hookResult) { | ||
| return hookResult.response | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (result.issues) { | ||
| return c.json({ data: value, error: result.issues, success: false }, 400) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably not a problem in this implementation, but the Valibot validator middleware uses
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm. Does valibot transform values if it fails to validate? Personally, I think, ideally, we should return whatever was passed, before transformations and defaults, in case of failed validation.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, Valibot always provides an output that can contain transformations when using
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Working with original data IMO has slightly more better handling if there’s something you need to do what’s not available/possible with schema and/or want to use it for observability. Would be nice to hear what @yusukebe has to say on this.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's okay with @muningis's implementation for this Standard Validator. It's helpful to see the original data. Regarding the Validabot Validator, I think both updating and not are okay.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would probably be good/nice to update Valibot Validator, but that would be ugly breaking change :/ |
||
| } | ||
|
|
||
| return result.value as StandardSchemaV1.InferOutput<Schema> | ||
muningis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }) | ||
|
|
||
| export type { Hook } | ||
| export { sValidator } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { type } from 'arktype' | ||
|
|
||
| const personJSONSchema = type({ | ||
| name: 'string', | ||
| age: 'number', | ||
| }) | ||
|
|
||
| const postJSONSchema = type({ | ||
| id: 'number', | ||
| title: 'string', | ||
| }) | ||
|
|
||
| const idJSONSchema = type({ | ||
| id: 'string', | ||
| }) | ||
|
|
||
| const queryNameSchema = type({ | ||
| 'name?': 'string', | ||
| }) | ||
|
|
||
| const queryPaginationSchema = type({ | ||
| page: type('unknown').pipe((p) => Number(p)), | ||
| }) | ||
|
|
||
| const querySortSchema = type({ | ||
| order: "'asc'|'desc'", | ||
| }) | ||
|
|
||
| export { | ||
| idJSONSchema, | ||
| personJSONSchema, | ||
| postJSONSchema, | ||
| queryNameSchema, | ||
| queryPaginationSchema, | ||
| querySortSchema, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { object, string, number, optional, pipe, unknown, transform, picklist } from 'valibot' | ||
|
|
||
| const personJSONSchema = object({ | ||
| name: string(), | ||
| age: number(), | ||
| }) | ||
|
|
||
| const postJSONSchema = object({ | ||
| id: number(), | ||
| title: string(), | ||
| }) | ||
|
|
||
| const idJSONSchema = object({ | ||
| id: string(), | ||
| }) | ||
|
|
||
| const queryNameSchema = optional( | ||
| object({ | ||
| name: optional(string()), | ||
| }) | ||
| ) | ||
|
|
||
| const queryPaginationSchema = object({ | ||
| page: pipe(unknown(), transform(Number)), | ||
| }) | ||
|
|
||
| const querySortSchema = object({ | ||
| order: picklist(['asc', 'desc']), | ||
| }) | ||
|
|
||
| export { | ||
| idJSONSchema, | ||
| personJSONSchema, | ||
| postJSONSchema, | ||
| queryNameSchema, | ||
| queryPaginationSchema, | ||
| querySortSchema, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { z } from 'zod' | ||
|
|
||
| const personJSONSchema = z.object({ | ||
| name: z.string(), | ||
| age: z.number(), | ||
| }) | ||
|
|
||
| const postJSONSchema = z.object({ | ||
| id: z.number(), | ||
| title: z.string(), | ||
| }) | ||
|
|
||
| const idJSONSchema = z.object({ | ||
| id: z.string(), | ||
| }) | ||
|
|
||
| const queryNameSchema = z | ||
| .object({ | ||
| name: z.string().optional(), | ||
| }) | ||
| .optional() | ||
|
|
||
| const queryPaginationSchema = z.object({ | ||
| page: z.coerce.number(), | ||
| }) | ||
|
|
||
| const querySortSchema = z.object({ | ||
| order: z.enum(['asc', 'desc']), | ||
| }) | ||
|
|
||
| export { | ||
| idJSONSchema, | ||
| personJSONSchema, | ||
| postJSONSchema, | ||
| queryNameSchema, | ||
| queryPaginationSchema, | ||
| querySortSchema, | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.