import { v } from 'convex/values';
// Primitives
v.null()
v.boolean()
v.number() // Float64 (JavaScript number)
v.bigint() // Int64 (JavaScript bigint)
v.string()
v.bytes() // ArrayBuffer
// Document IDs
v.id('tableName')
// Structures
v.array(elementValidator)
v.object({ field1: validator1, field2: validator2 })
v.record(v.string(), valueValidator) // Dynamic keys
// Special
v.optional(validator) // Field may be undefined
v.nullable(validator) // Value may be null
v.literal(value) // Exact value
v.union(v1, v2, ...) // One of multiple types
v.any() // Any valueconst userValidator = v.object({
name: v.string(),
age: v.number(),
active: v.boolean(),
data: v.bytes(),
});
// Usage in function
export const createUser = mutation({
args: userValidator, // or args: { name: v.string(), ... }
handler: async (ctx, args) => {
// args is typed: { name: string; age: number; active: boolean; data: ArrayBuffer }
},
});args: {
tags: v.array(v.string()),
scores: v.array(v.number()),
items: v.array(v.object({
id: v.string(),
count: v.number(),
})),
}args: {
user: v.object({
name: v.string(),
email: v.string(),
profile: v.object({
bio: v.string(),
avatar: v.optional(v.string()),
}),
}),
}args: {
metadata: v.record(v.string(), v.any()),
scores: v.record(v.string(), v.number()),
config: v.record(v.string(), v.object({
enabled: v.boolean(),
value: v.string(),
})),
}
// Usage
const metadata = { key1: 'value', key2: 123, key3: true }; // Record<string, any>
const scores = { alice: 100, bob: 95 }; // Record<string, number>args: {
name: v.string(), // Required
nickname: v.optional(v.string()), // May be undefined
bio: v.optional(v.string()),
}
// In handler
handler: async (ctx, args) => {
// args.name: string
// args.nickname: string | undefined
if (args.nickname) {
// Use args.nickname
}
}// Optional: field may be absent
v.optional(v.string()) // string | undefined
// Nullable: field is present but may be null
v.nullable(v.string()) // string | null
// Both
v.optional(v.nullable(v.string())) // string | null | undefinedargs: {
status: v.union(
v.literal('active'),
v.literal('inactive'),
v.literal('pending')
), // 'active' | 'inactive' | 'pending'
result: v.union(
v.object({ success: v.literal(true), data: v.any() }),
v.object({ success: v.literal(false), error: v.string() })
),
}args: {
userId: v.id('users'),
postId: v.id('posts'),
commentIds: v.array(v.id('comments')),
parentId: v.nullable(v.id('items')), // null or Id<'items'>
}import { v, Infer } from 'convex/values';
// Define validator
const userValidator = v.object({
name: v.string(),
age: v.number(),
email: v.optional(v.string()),
tags: v.array(v.string()),
});
// Extract TypeScript type
type User = Infer<typeof userValidator>;
// type User = {
// name: string;
// age: number;
// email?: string;
// tags: string[];
// }
// Use in code
const user: User = {
name: 'Alice',
age: 30,
tags: ['developer'],
};type Value =
| null
| bigint
| number
| boolean
| string
| ArrayBuffer
| Value[]
| { [key: string]: undefined | Value };
// All values stored in Convex must be of type Value
// JavaScript objects with functions, undefined values, etc. are not supportedimport { convexToJson, jsonToConvex } from 'convex/values';
// Convex to JSON (for serialization)
const value = { count: 123n, data: new ArrayBuffer(8) };
const json = convexToJson(value);
// { count: { "$integer": "123" }, data: { "$bytes": "..." } }
// JSON to Convex (for deserialization)
const value = jsonToConvex(json);
// { count: 123n, data: ArrayBuffer(8) }import { ConvexError } from 'convex/values';
// Simple error
throw new ConvexError('User not found');
// Structured error with data
throw new ConvexError({
code: 'VALIDATION_ERROR',
field: 'email',
message: 'Invalid email format',
});
// Client-side handling
try {
await convex.mutation(api.users.create, { email: 'invalid' });
} catch (error) {
if (error instanceof ConvexError) {
console.log('Error data:', error.data);
}
}// Using union of literals
const STATUS = {
ACTIVE: 'active',
INACTIVE: 'inactive',
PENDING: 'pending',
} as const;
args: {
status: v.union(
v.literal(STATUS.ACTIVE),
v.literal(STATUS.INACTIVE),
v.literal(STATUS.PENDING)
),
}args: {
event: v.union(
v.object({ type: v.literal('click'), x: v.number(), y: v.number() }),
v.object({ type: v.literal('scroll'), delta: v.number() }),
v.object({ type: v.literal('submit'), formId: v.string() })
),
}
handler: async (ctx, args) => {
switch (args.event.type) {
case 'click':
console.log(args.event.x, args.event.y);
break;
case 'scroll':
console.log(args.event.delta);
break;
case 'submit':
console.log(args.event.formId);
break;
}
}const addressValidator = v.object({
street: v.string(),
city: v.string(),
state: v.string(),
zip: v.string(),
country: v.string(),
});
const userValidator = v.object({
name: v.string(),
email: v.string(),
addresses: v.array(addressValidator),
preferences: v.object({
theme: v.union(v.literal('light'), v.literal('dark')),
notifications: v.object({
email: v.boolean(),
push: v.boolean(),
sms: v.boolean(),
}),
}),
metadata: v.record(v.string(), v.any()),
});
export const updateUser = mutation({
args: userValidator,
handler: async (ctx, args) => {
// Fully typed args
},
});// Define once
const timestampValidator = v.object({
createdAt: v.number(),
updatedAt: v.optional(v.number()),
});
// Reuse
args: {
post: v.object({
title: v.string(),
body: v.string(),
...timestampValidator.fields, // Spread fields
}),
}