Skip to Content
DocsAdvanced GuidesInput Resolvers & Validation

Input Resolvers & Validation

Actyx RPC leverages adapter resolvers to support popular schema validation libraries out-of-the-box, ensuring type safety both on client-side compilation and server-side execution.


Supported Resolvers

Zod

import { z } from "zod"; import { zodResolver } from "@explita/actyx-rpc/resolvers/zod"; const schema = z.object({ name: z.string().min(1, "Name is required"), description: z.string().optional(), }); const action = procedure.input(zodResolver(schema)).mutation(({ input }) => { ... });

Valibot

import * as v from "valibot"; import { valibotResolver } from "@explita/actyx-rpc/resolvers/valibot"; const schema = v.object({ name: v.pipe(v.string(), v.minLength(1, "Name is required")), description: v.optional(v.string()), }); const action = procedure.input(valibotResolver(schema)).mutation(({ input }) => { ... });

ArkType

import { type } from "arktype"; import { arktypeResolver } from "@explita/actyx-rpc/resolvers/arktype"; const schema = type({ name: "string > 1", description: "string?", }); const action = procedure .input(arktypeResolver<typeof schema>(schema)) .mutation(({ input }) => { ... });

Joi

import Joi from "joi"; import { joiResolver } from "@explita/actyx-rpc/resolvers/joi"; const schema = Joi.object({ name: Joi.string().min(1, "Name is required"), description: Joi.string().optional(), }); const action = procedure .input(joiResolver<{ name: string; description?: string }>(schema)) .mutation(({ input }) => { ... });

Yup

import * as yup from "yup"; import { yupResolver } from "@explita/actyx-rpc/resolvers/yup"; const schema = yup.object({ name: yup.string().min(1, "Name is required"), description: yup.string().optional(), }); const action = procedure.input(yupResolver(schema)).mutation(({ input }) => { ... });

Custom Resolver

You can build custom validation adapters by wrapping functions inside the resolver helper:

import { resolver } from "@explita/actyx-rpc/resolvers"; const customResolver = resolver<{ slug: string }>((data) => { if (typeof data.slug !== "string" || data.slug.length === 0) { return { success: false, errors: { slug: "Slug is required" }, }; } return { success: true, data: { slug: data.slug }, // returned shape is used for input type }; });

Design Constraint: Why No Primitives?

By design, Actyx RPC does not support primitive schemas (such as z.string(), z.number(), or z.boolean()) at the root level of .input().

Reason: Global Enrichment

During procedure execution, Actyx RPC merges the validated schema inputs with global request context data (e.g. userId, tenantId) created by enrichInput settings:

// 1. Root builder injects tenant/user const procedure = createProcedure({ enrichInput: { tenant: "tenant-456" }, }); // 2. Resolver must construct an object structure procedure .input(z.object({ email: z.string() })) .query(async ({ input }) => { input.email; // ✅ schema property input.tenant; // ✅ automatically merged enrichment key });

If primitive schemas were allowed, the compiler would fail to merge context keys onto the primitive value structure:

// ❌ If primitive schemas were allowed, this would crash procedure.input(z.string()).query(async ({ input }) => { // input would have to be typed as: string & { tenant: string } // Primitives in JS cannot hold properties! });

Therefore, all input schemas passed to .input() must evaluate to an object shape.

Last updated on