Validation Customization
This guide explains how to customize validation in oRPC, including how to disable runtime validation and how to customize validation errors.
Disable Validation
You can disable runtime validation with the .$config.
const base = os.$config({
/**
* When enabled, input schemas are not validated at runtime.
* Schemas are still used for type inference and OpenAPI generation.
*
* @warning Do not disable validation for schemas that transform values.
*
* @default false
*/
disableInputValidation: true,
/**
* When enabled, output schemas are not validated at runtime.
* Schemas are still used for type inference and OpenAPI generation.
*
* Useful when output schemas exist only for specification generation.
*
* @warning Do not disable validation for schemas that transform values.
*
* @default false
*/
disableOutputValidation: true
})
Custom Validation Errors
You can catch validation errors with interceptors, client interceptors, or 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.
import * as import zz from 'zod'
import { class ORPCError<TCode extends ORPCErrorCode, TData>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError, class ValidationErrorError thrown when input, output, or error data fails schema validation,
carrying the standard-schema `issues` and the invalid data.
Usually found as the `cause` of an `ORPCError`.ValidationError } from '@orpc/server'
const const handler: RPCHandler<{
headers?: IncomingHttpHeaders;
} & object>
handler = new new RPCHandler<{
headers?: IncomingHttpHeaders;
} & object>(router: Router<{
headers?: IncomingHttpHeaders;
} & object>, options?: NoInfer<RPCHandlerOptions<{
headers?: IncomingHttpHeaders;
} & object>>): RPCHandler<{
headers?: IncomingHttpHeaders;
} & object>
Serves an oRPC router over the RPC protocol using the Fetch API
(Request/Response), supported by modern runtimes like Deno, Bun,
Cloudflare Workers, and browsers.RPCHandler(const router: {
planet: {
list: ImplementedProcedure<{
headers?: IncomingHttpHeaders;
} & object, object, z.ZodObject<{
limit: z.ZodOptional<z.ZodNumber>;
cursor: z.ZodDefault<z.ZodNumber>;
}, z.core.$strip>, z.ZodArray<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>, object>;
find: ImplementedProcedure<{
headers?: IncomingHttpHeaders;
} & object, object, z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, object>;
create: ImplementedProcedure<...>;
};
}
router, {
interceptors?: StandardHandlerInterceptor<{
headers?: IncomingHttpHeaders;
} & object>[] | undefined
interceptor run after routing and before error handler,
useful for error handling, logging, metrics, etc.interceptors: [
async ({ next: (options?: StandardHandlerInterceptorOptions<{
headers?: IncomingHttpHeaders;
} & object> | undefined) => Promise<StandardResponse>
next }) => {
try {
return await next: (options?: StandardHandlerInterceptorOptions<{
headers?: IncomingHttpHeaders;
} & object> | undefined) => Promise<StandardResponse>
next()
}
catch (function (local var) error: unknownerror) {
if (
function (local var) error: unknownerror instanceof class ORPCError<TCode extends ORPCErrorCode, TData>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError
&& function (local var) error: ORPCError<any, any>error.ORPCError<any, any>.code: anycode === 'BAD_REQUEST'
&& function (local var) error: ORPCError<any, any>error.Error.cause?: unknowncause instanceof class ValidationErrorError thrown when input, output, or error data fails schema validation,
carrying the standard-schema `issues` and the invalid data.
Usually found as the `cause` of an `ORPCError`.ValidationError
) {
// If you only use Zod you can safely cast to ZodIssue[]
const const zodError: z.ZodError<unknown>zodError = new import zz.const ZodError: z.core.$constructor
new (def: z.core.$ZodIssue[]) => z.ZodError<unknown>
An Error-like class used to store Zod validation issues.ZodError(function (local var) error: ORPCError<any, any>error.Error.cause?: ValidationErrorcause.ValidationError.issues: readonly StandardSchemaV1.Issue[]This array is readonly because the upstream Standard Schema returns readonly issues.issues as import zz.import corecore.type $ZodIssue = z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueTooBig<unknown> | z.core.$ZodIssueTooSmall<unknown> | z.core.$ZodIssueInvalidStringFormat | z.core.$ZodIssueNotMultipleOf<number | bigint> | z.core.$ZodIssueInvalidUnion | z.core.$ZodIssueInvalidKey<unknown> | z.core.$ZodIssueInvalidElement<unknown> | z.core.$ZodIssueInvalidValue<unknown> | z.core.$ZodIssueCustom$ZodIssue[])
throw new new ORPCError<"INPUT_VALIDATION_FAILED", _FlattenedError<unknown, string>>(code: "INPUT_VALIDATION_FAILED", options: ORPCErrorOptions<_FlattenedError<unknown, string>>): ORPCError<"INPUT_VALIDATION_FAILED", _FlattenedError<unknown, string>>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError('INPUT_VALIDATION_FAILED', {
message?: string | undefinedmessage: import zz.function prettifyError(error: StandardSchemaV1<Input = unknown, Output = Input>.FailureResult): string
export prettifyError
prettifyError(const zodError: z.ZodError<unknown>zodError),
data: _FlattenedError<unknown, string>data: import zz.flattenError<unknown>(error: z.core.$ZodError<unknown>): _FlattenedError<unknown, string> (+1 overload)
export flattenError
flattenError(const zodError: z.ZodError<unknown>zodError),
ErrorOptions.cause?: unknowncause: function (local var) error: ORPCError<any, any>error,
})
}
if (
function (local var) error: unknownerror instanceof class ORPCError<TCode extends ORPCErrorCode, TData>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError
&& function (local var) error: ORPCError<any, any>error.ORPCError<any, any>.code: anycode === 'INTERNAL_SERVER_ERROR'
&& function (local var) error: ORPCError<any, any>error.Error.cause?: unknowncause instanceof class ValidationErrorError thrown when input, output, or error data fails schema validation,
carrying the standard-schema `issues` and the invalid data.
Usually found as the `cause` of an `ORPCError`.ValidationError
) {
// do not expose validation details for output validation errors
throw new new ORPCError<"OUTPUT_VALIDATION_FAILED", unknown>(code: "OUTPUT_VALIDATION_FAILED", options?: ORPCErrorOptions<unknown> | undefined): ORPCError<"OUTPUT_VALIDATION_FAILED", unknown>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError('OUTPUT_VALIDATION_FAILED', {
ErrorOptions.cause?: unknowncause: function (local var) error: ORPCError<any, any>error,
})
}
throw function (local var) error: unknownerror
}
},
],
})
Typesafe Validation Errors
As explained in the error handling guide, if you throw an ORPCError whose code and data match an error defined with .errors, oRPC treats it the same as errors.[code].
This does not work in interceptors. Use client interceptors or middleware applied before .input or .output instead.
import { class ORPCError<TCode extends ORPCErrorCode, TData>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError, const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os, class ValidationErrorError thrown when input, output, or error data fails schema validation,
carrying the standard-schema `issues` and the invalid data.
Usually found as the `cause` of an `ORPCError`.ValidationError } from '@orpc/server'
import * as import zz from 'zod'
const const base: Builder<DefaultInitialContext & object, {
INPUT_VALIDATION_FAILED: {
data: z.ZodObject<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}, z.core.$strip>;
};
}>
base = const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os.Builder<DefaultInitialContext & object, Record<never, never>>.errors<{
INPUT_VALIDATION_FAILED: {
data: z.ZodObject<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}, z.core.$strip>;
};
}>(errors: {
INPUT_VALIDATION_FAILED: {
data: z.ZodObject<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}, z.core.$strip>;
};
}): Builder<DefaultInitialContext & object, {
INPUT_VALIDATION_FAILED: {
data: z.ZodObject<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}, z.core.$strip>;
};
}>
errors({
type INPUT_VALIDATION_FAILED: {
data: z.ZodObject<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}, z.core.$strip>;
}
INPUT_VALIDATION_FAILED: {
data: z.ZodObject<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}, z.core.$strip>
data: import zz.function object<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}>(shape?: {
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
formErrors: z.ZodArray<...>;
fieldErrors: z.ZodRecord<...>;
}, z.core.$strip>
object({
formErrors: z.ZodArray<z.ZodString>formErrors: import zz.function array<z.ZodString>(element: z.ZodString, params?: string | z.core.$ZodArrayParams): z.ZodArray<z.ZodString>array(import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string()),
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>fieldErrors: import zz.function record<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>(keyType: z.ZodString, valueType: z.ZodOptional<z.ZodArray<z.ZodString>>, params?: string | z.core.$ZodRecordParams): z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>record(import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string(), import zz.function array<z.ZodString>(element: z.ZodString, params?: string | z.core.$ZodArrayParams): z.ZodArray<z.ZodString>array(import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string()).ZodType<any, any, $ZodArrayInternals<ZodString>>.optional(): z.ZodOptional<z.ZodArray<z.ZodString>>optional()),
}),
},
})
const const example: DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
id: z.ZodUUID;
}, z.core.$strip>, Schema<void>, {
INPUT_VALIDATION_FAILED: {
data: z.ZodObject<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}, z.core.$strip>;
};
}, never>
example = const base: Builder<DefaultInitialContext & object, {
INPUT_VALIDATION_FAILED: {
data: z.ZodObject<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}, z.core.$strip>;
};
}>
base
.Builder<DefaultInitialContext & object, { INPUT_VALIDATION_FAILED: { data: ZodObject<{ formErrors: ZodArray<ZodString>; fieldErrors: ZodRecord<ZodString, ZodOptional<...>>; }, $strip>; }; }>.input<z.ZodObject<{
id: z.ZodUUID;
}, z.core.$strip>>(schema: z.ZodObject<{
id: z.ZodUUID;
}, z.core.$strip>): BuilderWithInput<DefaultInitialContext & object, object, z.ZodObject<{
id: z.ZodUUID;
}, z.core.$strip>, {
INPUT_VALIDATION_FAILED: {
data: z.ZodObject<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}, z.core.$strip>;
};
}>
input(import zz.function object<{
id: z.ZodUUID;
}>(shape?: {
id: z.ZodUUID;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
id: z.ZodUUID;
}, z.core.$strip>
object({ id: z.ZodUUIDid: import zz.function uuid(params?: string | z.core.$ZodUUIDParams): z.ZodUUIDuuid() }))
.BuilderWithInput<DefaultInitialContext & object, object, ZodObject<{ id: ZodUUID; }, $strip>, { INPUT_VALIDATION_FAILED: { data: ZodObject<{ formErrors: ZodArray<ZodString>; fieldErrors: ZodRecord<ZodString, ZodOptional<...>>; }, $strip>; }; }>['handler']<void>(handler: ProcedureHandler<DefaultInitialContext & object, {
id: string;
}, void, ORPCErrorConstructorMap<{
INPUT_VALIDATION_FAILED: {
data: z.ZodObject<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}, z.core.$strip>;
};
}>>): DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
id: z.ZodUUID;
}, z.core.$strip>, Schema<void>, {
INPUT_VALIDATION_FAILED: {
data: z.ZodObject<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}, z.core.$strip>;
};
}, never>
handler(() => { /** do something */ })
const const handler: RPCHandler<DefaultInitialContext & object>handler = new new RPCHandler<DefaultInitialContext & object>(router: Router<DefaultInitialContext & object>, options?: NoInfer<RPCHandlerOptions<DefaultInitialContext & object>>): RPCHandler<DefaultInitialContext & object>Serves an oRPC router over the RPC protocol using the Fetch API
(Request/Response), supported by modern runtimes like Deno, Bun,
Cloudflare Workers, and browsers.RPCHandler({ example: DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
id: z.ZodUUID;
}, z.core.$strip>, Schema<void>, {
INPUT_VALIDATION_FAILED: {
data: z.ZodObject<{
formErrors: z.ZodArray<z.ZodString>;
fieldErrors: z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodArray<z.ZodString>>>;
}, z.core.$strip>;
};
}, never>
example }, {
clientInterceptors?: ProcedureClientInterceptor<DefaultInitialContext & object, Schema<unknown>, ErrorMap, any>[] | undefined
ClientInterceptor equivalent with createRouterClient.interceptors / createProcedure.interceptors
useful for error handling, logging, metrics, etc. (not counting encoding/decoding)clientInterceptors: [
async ({ next: (options?: ProcedureClientInterceptorOptions<DefaultInitialContext & object, ErrorMap> | undefined) => PromiseWithError<unknown, any>next }) => {
try {
return await next: (options?: ProcedureClientInterceptorOptions<DefaultInitialContext & object, ErrorMap> | undefined) => PromiseWithError<unknown, any>next()
}
catch (function (local var) error: unknownerror) {
if (
function (local var) error: unknownerror instanceof class ORPCError<TCode extends ORPCErrorCode, TData>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError
&& function (local var) error: ORPCError<any, any>error.ORPCError<any, any>.code: anycode === 'BAD_REQUEST'
&& function (local var) error: ORPCError<any, any>error.Error.cause?: unknowncause instanceof class ValidationErrorError thrown when input, output, or error data fails schema validation,
carrying the standard-schema `issues` and the invalid data.
Usually found as the `cause` of an `ORPCError`.ValidationError
) {
// If you only use Zod you can safely cast to ZodIssue[]
const const zodError: z.ZodError<unknown>zodError = new import zz.const ZodError: z.core.$constructor
new (def: z.core.$ZodIssue[]) => z.ZodError<unknown>
An Error-like class used to store Zod validation issues.ZodError(function (local var) error: ORPCError<any, any>error.Error.cause?: ValidationErrorcause.ValidationError.issues: readonly StandardSchemaV1.Issue[]This array is readonly because the upstream Standard Schema returns readonly issues.issues as import zz.import corecore.type $ZodIssue = z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidStringFormat | z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueTooBig<unknown> | z.core.$ZodIssueTooSmall<unknown> | z.core.$ZodIssueNotMultipleOf<number | bigint> | z.core.$ZodIssueInvalidUnion | z.core.$ZodIssueInvalidKey<unknown> | z.core.$ZodIssueInvalidElement<unknown> | z.core.$ZodIssueInvalidValue<unknown> | z.core.$ZodIssueCustom$ZodIssue[])
throw new new ORPCError<"INPUT_VALIDATION_FAILED", _FlattenedError<unknown, string>>(code: "INPUT_VALIDATION_FAILED", options: ORPCErrorOptions<_FlattenedError<unknown, string>>): ORPCError<"INPUT_VALIDATION_FAILED", _FlattenedError<unknown, string>>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError('INPUT_VALIDATION_FAILED', {
message?: string | undefinedmessage: import zz.function prettifyError(error: StandardSchemaV1<Input = unknown, Output = Input>.FailureResult): string
export prettifyError
prettifyError(const zodError: z.ZodError<unknown>zodError),
data: _FlattenedError<unknown, string>data: import zz.flattenError<unknown>(error: z.core.$ZodError<unknown>): _FlattenedError<unknown, string> (+1 overload)
export flattenError
flattenError(const zodError: z.ZodError<unknown>zodError),
ErrorOptions.cause?: unknowncause: function (local var) error: ORPCError<any, any>error,
})
}
throw function (local var) error: unknownerror
}
},
],
})