import { Expand } from "../type_utils.js";
import { GenericId } from "./index.js";
import {
OptionalProperty,
VAny,
VArray,
VBoolean,
VBytes,
VFloat64,
VId,
VInt64,
VLiteral,
VNull,
VObject,
VOptional,
VRecord,
VString,
VUnion,
Validator,
} from "./validators.js";
/**
* The type that all validators must extend.
*
* @public
*/
export type GenericValidator = Validator<any, any, any>;
export function isValidator(v: any): v is GenericValidator {
return !!v.isConvexValidator;
}
/**
* Coerce an object with validators as properties to a validator.
* If a validator is passed, return it.
*
* @public
*/
export function asObjectValidator<
V extends Validator<any, any, any> | PropertyValidators,
>(
obj: V,
): V extends Validator<any, any, any>
? V
: V extends PropertyValidators
? Validator<ObjectType<V>>
: never {
if (isValidator(obj)) {
return obj as any;
} else {
return v.object(obj as PropertyValidators) as any;
}
}
/**
* Coerce an object with validators as properties to a validator.
* If a validator is passed, return it.
*
* @public
*/
export type AsObjectValidator<
V extends Validator<any, any, any> | PropertyValidators,
> =
V extends Validator<any, any, any>
? V
: V extends PropertyValidators
? Validator<ObjectType<V>>
: never;
/**
* The validator builder.
*
* This builder allows you to build validators for Convex values. Validators
* are used in two places:
*
* 1. **Schema definitions** - to define the shape of documents in your tables.
* 2. **Function arguments and return values** - to validate inputs and outputs
* of your Convex queries, mutations, and actions.
*
* Always include `args` and `returns` validators on all Convex functions. If a
* function doesn't return a value, use `returns: v.null()`.
*
* **Convex type reference:**
*
* | Convex Type | JS/TS Type | Validator |
* |-------------|---------------|--------------------------------|
* | Id | `string` | `v.id("tableName")` |
* | Null | `null` | `v.null()` |
* | Float64 | `number` | `v.number()` |
* | Int64 | `bigint` | `v.int64()` |
* | Boolean | `boolean` | `v.boolean()` |
* | String | `string` | `v.string()` |
* | Bytes | `ArrayBuffer` | `v.bytes()` |
* | Array | `Array` | `v.array(element)` |
* | Object | `Object` | `v.object({ field: value })` |
* | Record | `Record` | `v.record(keys, values)` |
*
* **Modifiers and meta-types:**
* - `v.union(member1, member2)` - a value matching at least one validator
* - `v.literal("value")` - a specific literal string, number, bigint, or boolean
* - `v.optional(validator)` - makes a property optional in an object (`T | undefined`)
*
* **Important notes:**
* - JavaScript's `undefined` is **not** a valid Convex value. Functions that
* return `undefined` or have no return will return `null` to the client.
* Objects with `undefined` values will strip those keys during serialization.
* For arrays, use an explicit `null` instead.
* - `v.bigint()` is deprecated, use `v.int64()` instead.
* - `v.map()` and `v.set()` are not supported. Use `v.array()` of tuples or
* `v.record()` as alternatives.
*
* @example
* ```typescript
* import { v } from "convex/values";
*
* // Use in function definition:
* export const createUser = mutation({
* args: {
* name: v.string(),
* email: v.string(),
* age: v.optional(v.number()),
* },
* returns: v.id("users"),
* handler: async (ctx, args) => {
* return await ctx.db.insert("users", args);
* },
* });
* ```
*
* @see https://docs.convex.dev/database/types
* @see https://docs.convex.dev/functions/validation
* @public
*/
export const v = {
/**
* Validates that the value is a document ID for the given table.
*
* IDs are strings at runtime but are typed as `Id<"tableName">` in
* TypeScript for type safety.
*
* @example
* ```typescript
* args: { userId: v.id("users") }
* ```
*
* @param tableName The name of the table.
*/
id: <TableName extends string>(tableName: TableName) => {
return new VId<GenericId<TableName>>({
isOptional: "required",
tableName,
});
},
/**
* Validates that the value is `null`.
*
* Use `returns: v.null()` for functions that don't return a meaningful value.
* JavaScript `undefined` is not a valid Convex value, it is automatically
* converted to `null`.
*/
null: () => {
return new VNull({ isOptional: "required" });
},
/**
* Validates that the value is a JavaScript `number` (Convex Float64).
*
* Supports all IEEE-754 double-precision floating point numbers including
* NaN and Infinity.
*
* Alias for `v.float64()`.
*/
number: () => {
return new VFloat64({ isOptional: "required" });
},
/**
* Validates that the value is a JavaScript `number` (Convex Float64).
*
* Supports all IEEE-754 double-precision floating point numbers.
*/
float64: () => {
return new VFloat64({ isOptional: "required" });
},
/**
* @deprecated Use `v.int64()` instead.
*/
bigint: () => {
return new VInt64({ isOptional: "required" });
},
/**
* Validates that the value is a JavaScript `bigint` (Convex Int64).
*
* Supports BigInts between -2^63 and 2^63-1.
*
* @example
* ```typescript
* args: { timestamp: v.int64() }
* // Usage: createDoc({ timestamp: 1234567890n })
* ```
*/
int64: () => {
return new VInt64({ isOptional: "required" });
},
/**
* Validates that the value is a `boolean`.
*/
boolean: () => {
return new VBoolean({ isOptional: "required" });
},
/**
* Validates that the value is a `string`.
*
* Strings are stored as UTF-8 and their storage size is calculated as their
* UTF-8 encoded size.
*/
string: () => {
return new VString({ isOptional: "required" });
},
/**
* Validates that the value is an `ArrayBuffer` (Convex Bytes).
*
* Use for binary data.
*/
bytes: () => {
return new VBytes({ isOptional: "required" });
},
/**
* Validates that the value is exactly equal to the given literal.
*
* Useful for discriminated unions and enum-like patterns.
*
* @example
* ```typescript
* // Discriminated union pattern:
* v.union(
* v.object({ kind: v.literal("error"), message: v.string() }),
* v.object({ kind: v.literal("success"), value: v.number() }),
* )
* ```
*
* @param literal The literal value to compare against.
*/
literal: <T extends string | number | bigint | boolean>(literal: T) => {
return new VLiteral<T>({ isOptional: "required", value: literal });
},
/**
* Validates that the value is an `Array` where every element matches the
* given validator.
*
* Arrays can have at most 8192 elements.
*
* @example
* ```typescript
* args: { tags: v.array(v.string()) }
* args: { coordinates: v.array(v.number()) }
* args: { items: v.array(v.object({ name: v.string(), qty: v.number() })) }
* ```
*
* @param element The validator for the elements of the array.
*/
array: <T extends Validator<any, "required", any>>(element: T) => {
return new VArray<T["type"][], T>({ isOptional: "required", element });
},
/**
* Validates that the value is an `Object` with the specified properties.
*
* Objects can have at most 1024 entries. Field names must be non-empty and
* must not start with `"$"` or `"_"` (`_` is reserved for system fields
* like `_id` and `_creationTime`; `$` is reserved for Convex internal use).
*
* @example
* ```typescript
* args: {
* user: v.object({
* name: v.string(),
* email: v.string(),
* age: v.optional(v.number()),
* })
* }
* ```
*
* @param fields An object mapping property names to their validators.
*/
object: <T extends PropertyValidators>(fields: T) => {
return new VObject<ObjectType<T>, T>({ isOptional: "required", fields });
},
/**
* Validates that the value is a `Record` (object with dynamic keys).
*
* Records are objects at runtime but allow dynamic keys, unlike `v.object()`
* which requires known property names. Keys must be ASCII characters only,
* non-empty, and not start with `"$"` or `"_"`.
*
* @example
* ```typescript
* // Map of user IDs to scores:
* args: { scores: v.record(v.id("users"), v.number()) }
*
* // Map of string keys to string values:
* args: { metadata: v.record(v.string(), v.string()) }
* ```
*
* @param keys The validator for the keys of the record.
* @param values The validator for the values of the record.
*/
record: <
Key extends Validator<string, "required", any>,
Value extends Validator<any, "required", any>,
>(
keys: Key,
values: Value,
) => {
return new VRecord<Record<Infer<Key>, Value["type"]>, Key, Value>({
isOptional: "required",
key: keys,
value: values,
});
},
/**
* Validates that the value matches at least one of the given validators.
*
* @example
* ```typescript
* // Allow string or number:
* args: { value: v.union(v.string(), v.number()) }
*
* // Discriminated union (recommended pattern):
* v.union(
* v.object({ kind: v.literal("text"), body: v.string() }),
* v.object({ kind: v.literal("image"), url: v.string() }),
* )
*
* // Nullable value:
* returns: v.union(v.object({ ... }), v.null())
* ```
*
* @param members The validators to match against.
*/
union: <T extends Validator<any, "required", any>[]>(...members: T) => {
return new VUnion<T[number]["type"], T>({
isOptional: "required",
members,
});
},
/**
* A validator that accepts any Convex value without validation.
*
* Prefer using specific validators when possible for better type safety
* and runtime validation.
*/
any: () => {
return new VAny({ isOptional: "required" });
},
/**
* Makes a property optional in an object validator.
*
* An optional property can be omitted entirely when creating a document or
* calling a function. This is different from `v.nullable()` which requires
* the property to be present but allows `null`.
*
* @example
* ```typescript
* v.object({
* name: v.string(), // required
* nickname: v.optional(v.string()), // can be omitted
* })
*
* // Valid: { name: "Alice" }
* // Valid: { name: "Alice", nickname: "Ali" }
* // Invalid: { name: "Alice", nickname: null } - use v.nullable() for this
* ```
*
* @param value The property value validator to make optional.
*/
optional: <T extends GenericValidator>(value: T) => {
return value.asOptional() as VOptional<T>;
},
/**
* Allows a value to be either the given type or `null`.
*
* This is shorthand for `v.union(value, v.null())`. Unlike `v.optional()`,
* the property must still be present, but may be `null`.
*
* @example
* ```typescript
* v.object({
* name: v.string(),
* deletedAt: v.nullable(v.number()), // must be present, can be null
* })
*
* // Valid: { name: "Alice", deletedAt: null }
* // Valid: { name: "Alice", deletedAt: 1234567890 }
* // Invalid: { name: "Alice" } - deletedAt is required
* ```
*/
nullable: <T extends Validator<any, "required", any>>(value: T) => {
return v.union(value, v.null());
},
};
/**
* Validators for each property of an object.
*
* This is represented as an object mapping the property name to its
* {@link Validator}.
*
* @public
*/
export type PropertyValidators = Record<
string,
Validator<any, OptionalProperty, any>
>;
/**
* Compute the type of an object from {@link PropertyValidators}.
*
* @public
*/
export type ObjectType<Fields extends PropertyValidators> = Expand<
// Map each key to the corresponding property validator's type making
// the optional ones optional.
{
// This `Exclude<..., undefined>` does nothing unless
// the tsconfig.json option `"exactOptionalPropertyTypes": true,`
// is used. When it is it results in a more accurate type.
// When it is not the `Exclude` removes `undefined` but it is
// added again by the optional property.
[Property in OptionalKeys<Fields>]?: Exclude<
Infer<Fields[Property]>,
undefined
>;
} & {
[Property in RequiredKeys<Fields>]: Infer<Fields[Property]>;
}
>;
type OptionalKeys<PropertyValidators extends Record<string, GenericValidator>> =
{
[Property in keyof PropertyValidators]: PropertyValidators[Property]["isOptional"] extends "optional"
? Property
: never;
}[keyof PropertyValidators];
type RequiredKeys<PropertyValidators extends Record<string, GenericValidator>> =
Exclude<keyof PropertyValidators, OptionalKeys<PropertyValidators>>;
/**
* Extract a TypeScript type from a validator.
*
* Example usage:
* ```ts
* const objectSchema = v.object({
* property: v.string(),
* });
* type MyObject = Infer<typeof objectSchema>; // { property: string }
* ```
* @typeParam V - The type of a {@link Validator} constructed with {@link v}.
*
* @public
*/
export type Infer<T extends Validator<any, OptionalProperty, any>> = T["type"];