Skip to content

Commit 14eada7

Browse files
authored
feat(server): disable validation (#1678)
1 parent be970db commit 14eada7

20 files changed

Lines changed: 304 additions & 54 deletions

apps/content/.vitepress/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export default withMermaid(defineConfig({
224224
{ text: 'Publish Client to NPM', link: '/docs/advanced/publish-client-to-npm' },
225225
{ text: 'Scaling Large Projects', link: '/docs/advanced/scaling-large-projects' },
226226
{ text: 'Testing and Mocking', link: '/docs/advanced/testing-and-mocking' },
227-
{ text: 'Validation Errors', link: '/docs/advanced/validation-errors' },
227+
{ text: 'Validation Customization', link: '/docs/advanced/validation-customization' },
228228
],
229229
},
230230
{

apps/content/docs/advanced/validation-errors.md renamed to apps/content/docs/advanced/validation-customization.md

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,53 @@
1-
# Validation Errors
1+
# Validation Customization
2+
3+
This guide explains how to customize validation in oRPC, including how to disable runtime validation and how to customize validation errors.
4+
5+
## Disable Validation
6+
7+
You can disable runtime validation with the `.$config`.
8+
9+
```ts
10+
const base = os.$config({
11+
/**
12+
* When enabled, input schemas are not validated at runtime.
13+
* Schemas are still used for type inference and OpenAPI generation.
14+
*
15+
* @warning Do not disable validation for schemas that transform values.
16+
*
17+
* @default false
18+
*/
19+
disableInputValidation: true,
20+
21+
/**
22+
* When enabled, output schemas are not validated at runtime.
23+
* Schemas are still used for type inference and OpenAPI generation.
24+
*
25+
* Useful when output schemas exist only for specification generation.
26+
*
27+
* @warning Do not disable validation for schemas that transform values.
28+
*
29+
* @default false
30+
*/
31+
disableOutputValidation: true
32+
})
33+
```
34+
35+
::: warning
36+
Do not disable validation for schemas that transform values.
37+
For example, the following schema accepts a `number` but returns a `string`:
38+
39+
```ts
40+
z.object({
41+
value: z.number().transform(value => String(value)),
42+
})
43+
```
244

3-
oRPC includes built-in validation errors that work well for most cases. Customize them when you need a different message or error shape.
45+
If runtime validation is disabled, the transformation is skipped. As a result, the server returns a `number` while the client expects a `string`, leading to unexpected behavior.
46+
:::
447

5-
## Customizing
48+
## Custom Validation Errors
649

7-
You can catch validation errors with [interceptors](/docs/rpc/handler#interceptors), [client interceptors](/docs/rpc/handler#client-interceptors), or [middleware](/docs/middleware) applied before `.input` or `.output`.
50+
You can catch validation errors with [interceptors](/docs/rpc/handler#interceptors), [client interceptors](/docs/rpc/handler#client-interceptors), or [middleware](/docs/middleware) applied before `.input` or `.output` and then throw a custom error. This is useful if you want to change the error message or shape.
851

952
```ts twoslash
1053
import { RPCHandler } from '@orpc/server/fetch'
@@ -53,7 +96,7 @@ const handler = new RPCHandler(router, {
5396
})
5497
```
5598

56-
## Typesafe Validation Errors
99+
### Typesafe Validation Errors
57100

58101
As explained in the [error handling guide](/docs/error-handling#orpcerror-compatibility), if you throw an `ORPCError` whose `code` and `data` match an error defined with `.errors`, oRPC treats it the same as `errors.[code]`.
59102

apps/content/docs/helpers/form-data.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const anyError = getIssueMessage('anything', 'path')
5555
```
5656

5757
::: warning
58-
The `getIssueMessage` utility works with any data type but requires validation errors to follow the [standard schema issue format](https://standardschema.dev/#the-specifications). It looks for issues in the `data.issues` property. If you use custom [validation errors](/docs/advanced/validation-errors), store them elsewhere, or modify the issue format, `getIssueMessage` may not work as expected.
58+
The `getIssueMessage` utility works with any data type but requires validation errors to follow the [standard schema issue format](https://standardschema.dev/#the-specifications). It looks for issues in the `data.issues` property. If you [customize validation errors](/docs/advanced/validation-customization#custom-validation-errors), store them elsewhere, or modify the issue format, `getIssueMessage` may not work as expected.
5959
:::
6060

6161
## Usage Example

apps/content/docs/integrations/nest.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export class PlanetController {
126126
By default, errors thrown in implemented procedures are caught and handled by oRPC, which then rethrows a generic `HttpException` to NestJS. If you want NestJS to catch the original error instead of `HttpException`, use the [Rethrow Plugin](/docs/plugins/rethrow) to bypass oRPC error handling and let NestJS handle the error directly.
127127

128128
::: tip
129-
Learn how to customize input and output validation errors in [Validation Errors](/docs/advanced/validation-errors) and the [ORPCModule](#configuration) section.
129+
Learn how to customize input and output validation errors in [Validation Errors](/docs/advanced/validation-customization#custom-validation-errors) and the [ORPCModule](#configuration) section.
130130
:::
131131

132132
## Body Parser

apps/content/docs/plugins/request-validation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const link = new RPCLink({
3636

3737
## Custom Validation Errors
3838

39-
If you have already [customized validation errors on the server](/docs/advanced/validation-errors), you can use interceptors to catch and map the validation errors thrown by this plugin so they match your server-side errors.
39+
If you have already [customized validation errors on the server](/docs/advanced/validation-customization#custom-validation-errors), you can use interceptors to catch and map the validation errors thrown by this plugin so they match your server-side errors.
4040

4141
```ts
4242
import { ORPCError } from '@orpc/client'

apps/content/docs/plugins/response-validation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ This plugin reconciles ORPC errors from other interceptors and plugins, allowing
3838

3939
## Custom Validation Errors
4040

41-
If you have already [customized validation errors on the server](/docs/advanced/validation-errors), you can use interceptors to catch and map the validation errors thrown by this plugin so they match your server-side errors.
41+
If you have already [customized validation errors on the server](/docs/advanced/validation-customization#custom-validation-errors), you can use interceptors to catch and map the validation errors thrown by this plugin so they match your server-side errors.
4242

4343
```ts
4444
import { ORPCError } from '@orpc/client'

packages/server/src/builder.test-d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ describe('Builder', () => {
3939
builder.$context<'invalid'>()
4040
})
4141

42+
it('$config', () => {
43+
expectTypeOf(builder.$config({ disableInputValidation: true, disableOutputValidation: true })).toEqualTypeOf<
44+
typeof builder
45+
>()
46+
47+
// @ts-expect-error - invalid setting
48+
builder.$config('invalid')
49+
})
50+
4251
it('.errors', () => {
4352
expectTypeOf(builder.errors({ INVALID: { message: 'invalid' }, OVERRIDE: { message: 'override' } })).toEqualTypeOf<
4453
Builder<

packages/server/src/builder.test.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ describe('builder', () => {
4343
orderedMiddlewares: [
4444
{ middleware: vi.fn() as AnyFunction, inputSchemasLengthAtUse: 0, outputSchemasLengthAtUse: 0 },
4545
],
46+
disableInputValidation: false,
47+
disableOutputValidation: false,
4648
}
4749

4850
const metaPlugin: AnyMetaPlugin = {
@@ -63,6 +65,20 @@ describe('builder', () => {
6365
expect(builder.$context()).toBe(builder)
6466
})
6567

68+
it('$config', () => {
69+
const applied = builder.$config({
70+
disableOutputValidation: undefined,
71+
})
72+
73+
expect(applied).toBeInstanceOf(Builder)
74+
expect(applied).not.toBe(builder)
75+
76+
expect(applied['~orpc']).toEqual({
77+
...builder['~orpc'],
78+
disableOutputValidation: undefined,
79+
})
80+
})
81+
6682
it('.meta', () => {
6783
const applied = builder.meta(metaPlugin)
6884
expect(applied).toBeInstanceOf(Builder)
@@ -374,10 +390,8 @@ describe('builder', () => {
374390
expect(applied).toBe(augmentRouterSpy.mock.results[0]?.value)
375391
expect(augmentRouterSpy).toHaveBeenCalledOnce()
376392
expect(augmentRouterSpy).toHaveBeenCalledWith(router, {
393+
...builder['~orpc'],
377394
middlewares: builder['~orpc'].orderedMiddlewares.map((m: any) => m.middleware),
378-
meta: builder['~orpc'].meta,
379-
metaPlugins: builder['~orpc'].metaPlugins,
380-
errorMap: builder['~orpc'].errorMap,
381395
})
382396
})
383397

@@ -394,10 +408,8 @@ describe('builder', () => {
394408
expect(unlazied.default).toBe(augmentRouterSpy.mock.results[0]?.value)
395409
expect(augmentRouterSpy).toHaveBeenCalledOnce()
396410
expect(augmentRouterSpy).toHaveBeenCalledWith(router, {
411+
...builder['~orpc'],
397412
middlewares: builder['~orpc'].orderedMiddlewares.map((m: any) => m.middleware),
398-
meta: builder['~orpc'].meta,
399-
metaPlugins: builder['~orpc'].metaPlugins,
400-
errorMap: builder['~orpc'].errorMap,
401413
})
402414
})
403415
})

packages/server/src/builder.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { Context, MergedInitialContext } from './context'
66
import type { ORPCErrorConstructorMap } from './error'
77
import type { Middleware } from './middleware'
88
import type { DecoratedMiddleware } from './middleware-decorated'
9-
import type { OrderedMiddleware, ProcedureHandler } from './procedure'
9+
import type { OrderedMiddleware, ProcedureConfig, ProcedureHandler } from './procedure'
1010
import type { AnyRouter } from './router'
1111
import type { AugmentedRouter } from './router-utils'
1212
import { getHiddenMetaPlugins, mergeErrorMap, resolveMetaPlugins } from '@orpc/contract'
@@ -17,14 +17,13 @@ import { DecoratedProcedure } from './procedure-decorated'
1717
import { augmentRouter } from './router-utils'
1818

1919
export interface DefaultInitialContext {
20-
2120
}
2221

2322
export interface BuilderDefinition<
2423
TInputSchema extends AnySchema,
2524
TInjectedContext extends AnySchema,
2625
TErrorMap extends ErrorMap,
27-
>extends ProcedureContractDefinition<TInputSchema, TInjectedContext, TErrorMap> {
26+
>extends ProcedureContractDefinition<TInputSchema, TInjectedContext, TErrorMap>, ProcedureConfig {
2827
orderedMiddlewares: OrderedMiddleware[]
2928
}
3029

@@ -57,6 +56,13 @@ export class Builder<
5756
return this as any
5857
}
5958

59+
$config(config: ProcedureConfig): Builder<TInitialContext, TErrorMap> {
60+
return new Builder({
61+
...this['~orpc'],
62+
...config,
63+
})
64+
}
65+
6066
meta(
6167
...plugins: MetaPlugin<InitialInputSchema, InitialOutputSchema, TErrorMap>[]
6268
): Builder<TInitialContext, TErrorMap> {
@@ -208,10 +214,8 @@ export class Builder<
208214
router: T,
209215
): AugmentedRouter<T, TErrorMap> {
210216
return augmentRouter(router, {
217+
...this['~orpc'],
211218
middlewares: this['~orpc'].orderedMiddlewares.map(({ middleware }) => middleware),
212-
meta: this['~orpc'].meta,
213-
metaPlugins: this['~orpc'].metaPlugins,
214-
errorMap: this['~orpc'].errorMap,
215219
}) as any
216220
}
217221

packages/server/src/implementer-procedure.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ describe('procedureImplementer', () => {
1313
outputSchemas: [z.number()],
1414
meta: { meta: true },
1515
metaPlugins: [{ name: 'plugin', init: vi.fn() }],
16+
disableInputValidation: true,
1617
}
1718

1819
const implementer = new ProcedureImplementer(definition)

0 commit comments

Comments
 (0)