Source:
ocean/docs/adr/0004-valibot-tanstack-form-validation.md| ✏️ Edit on GitHub
ADR-0004: Adopt Valibot with TanStack Form for Validation
Date: 2025-07-27
Status
Accepted
Context
Our application uses TanStack Form for form state management and requires a robust validation solution. Initially, we implemented Zod for schema validation, but encountered issues with error message display - validators were showing "[object Object]" or "Invalid input" instead of proper error messages. Additionally, we experienced type mismatches where validators received objects when expecting strings.
Key issues encountered:
- Error messages displaying as "[object Object]" in the UI
- Generic "Invalid input" messages instead of specific validation errors
- Type errors: "Invalid type: Expected string but received Object"
- Complex integration between Zod and TanStack Form's validator API
Decision
We migrated from Zod to Valibot for schema validation, implementing a custom integration pattern with TanStack Form that properly handles the validator API.
Specifically, we:
- Replaced all Zod schemas with equivalent Valibot schemas
- Implemented type-safe validators that properly destructure TanStack Form's validator parameters
- Added type guards to ensure validators receive string values
- Removed dependency on form adapter packages in favor of direct integration
Implementation Pattern
Schema Definition (Valibot)
import * as v from 'valibot'
export const signupSchema = v.object({
name: v.pipe(v.string(), v.minLength(2, 'Please enter your full name')),
email: v.pipe(v.string(), v.email('Please enter a valid email address')),
organization: v.pipe(v.string(), v.minLength(2, 'Please enter your organization name')),
industry: v.pipe(v.string(), v.minLength(1, 'Please select an industry')),
region: v.pipe(v.string(), v.minLength(1, 'Please select a region')),
})
Form Field Validation Pattern
<form.Field
name="email"
validators={{
onChange: ({ value }) => {
// Ensure we're working with a string
const stringValue = typeof value === 'string' ? value : String(value || '')
const result = v.safeParse(signupSchema.entries.email, stringValue)
if (result.success) return undefined
return result.issues?.[0]?.message || 'Please enter a valid email address'
},
}}
children={(field) => (
// Field UI implementation
)}
/>
Key Integration Points
- Validator Function Signature: TanStack Form passes
{ value, fieldApi }to validators, not just the raw value - Type Safety: Always check and convert the value to ensure it's a string before validation
- Error Message Extraction: Use
result.issues?.[0]?.messageto get the first validation error - Return Convention: Return
undefinedfor valid input, string message for errors
Consequences
Positive
- Clear Error Messages: Users now see specific, helpful validation messages
- Type Safety: Proper type checking prevents runtime errors
- Simpler Integration: Direct integration without adapter complexity
- Smaller Bundle: Valibot has a smaller footprint than Zod
- Better Performance: Valibot's modular design allows better tree-shaking
- Consistent API: Valibot's pipe-based API is more predictable
Negative
- Less Ecosystem Support: Valibot has fewer integrations compared to Zod
- Manual Integration: No official TanStack Form adapter means more boilerplate
- Migration Effort: Required updating all schemas and validators
- Documentation: Less community resources compared to Zod
Neutral
- Different API: Developers need to learn Valibot's pipe-based syntax
- Custom Patterns: Team must maintain custom integration patterns
Lessons Learned
- Understand the Validator API: TanStack Form's validator functions receive an object, not raw values
- Type Guards are Essential: Always validate input types in form validators
- Test Error Scenarios: Ensure validation errors display correctly in the UI
- Direct Integration Can Be Simpler: Sometimes avoiding adapters reduces complexity
References
Decision Makers
- Development Team
- Technical Lead
Related ADRs
- ADR-0001: Adopt TanStack Ecosystem for State Management