tRPC v11 accepts any Standard Schema V1 as procedure input. ata-validator implements Standard Schema natively, so a Validator instance drops straight into .input().
npm install ata-validator @trpc/serverimport { initTRPC } from '@trpc/server'
import { Validator } from 'ata-validator'
const t = initTRPC.create()
const userSchema = new Validator({
type: 'object',
properties: {
id: { type: 'integer', minimum: 1 },
name: { type: 'string', minLength: 1 },
email: { type: 'string' },
},
required: ['id', 'name', 'email'],
})
export const appRouter = t.router({
createUser: t.procedure
.input(userSchema)
.mutation(({ input }) => {
// `input` is typed as unknown; add a build-time step to narrow.
return { ok: true, id: (input as any).id }
}),
})Validator itself is not parameterized, so input comes in as unknown by default. Two honest approaches for getting a typed input:
npx ata compile schemas/user.json -o src/user.validator.mjs --name Userimport { initTRPC } from '@trpc/server'
import { Validator } from 'ata-validator'
import type { User } from './schemas/user.validator.mjs'
import userSchemaJson from './schemas/user.json' with { type: 'json' }
const t = initTRPC.create()
const userSchema = new Validator(userSchemaJson)
export const appRouter = t.router({
createUser: t.procedure
.input(userSchema)
.mutation(({ input }) => {
const user = input as User // runtime-validated, type-asserted
return { ok: true, id: user.id }
}),
})The cast is safe because .input(userSchema) has already run the validator; if input is reachable, the shape matches User.
If the schema is static, the compiled module has both the type and a runtime check. You can bypass t.procedure.input() entirely:
import { initTRPC } from '@trpc/server'
import { TRPCError } from '@trpc/server'
import { isValid, type User } from './schemas/user.validator.mjs'
const t = initTRPC.create()
export const appRouter = t.router({
createUser: t.procedure
.mutation(({ rawInput }) => {
if (!isValid(rawInput)) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'invalid input' })
}
// `rawInput` is narrowed to User here by the type predicate.
return { ok: true, id: rawInput.id }
}),
})Trade-off: loses tRPC's automatic error formatting, gains direct narrowing and no runtime dependency on ata-validator.
tRPC surfaces validation failures as TRPCError with code BAD_REQUEST. ata's Standard Schema issues array maps to tRPC's default error formatter. Each issue has message and path:
{
"code": "BAD_REQUEST",
"issues": [
{ "message": "must be >= 1", "path": ["id"] }
]
}Override via errorFormatter in initTRPC.create() if the default is too verbose.
- Versions before tRPC v11 do not support Standard Schema directly. For v10, wrap the Validator manually in a function that matches the legacy validator contract.
- Using
ata compilefor both the runtime validator and types keeps the schema file as the single source of truth. - abortEarly mode works as input validator but discards the detailed
issueslist, so the client only sees "validation failed". Good for high-throughput internal endpoints, bad for public API with informative error responses.