Accept single value or array
?ids=1 and ?ids=1&ids=2 both produce the same shape:
evseIds: z
.union([z.string(), z.array(z.string())])
.optional()
.transform((val) => {
if (!val) return []
return Array.isArray(val) ? val : [val]
})Coerce string → number
Query params are always strings — z.coerce.number() converts automatically:
limit: z.coerce.number().optional().transform((val) => val ?? 20)
dateFrom: z.coerce.number().optional()Mutual exclusivity with .refine()
.refine((data) => !(data.mobileNumber && data.userId), {
message: 'Use either mobileNumber or userId, not both',
path: ['mobileNumber']
})Infer TypeScript type from schema
export type GetSessionsQueryParams = z.infer<typeof GetSessionsQuerySchema>No need to maintain a separate type definition — it stays in sync automatically.