The Validation API allows you to define rules that ensure data quality and consistency in your Sanity documents. Validation rules are defined in your schema and executed both in the Studio UI and server-side.
Rule Class
The Rule class provides a fluent API for defining validation rules. Each method returns a new Rule instance, allowing you to chain multiple constraints.
import {defineField} from 'sanity'
defineField({
name: 'title',
type: 'string',
validation: (Rule) => Rule.required().min(5).max(100)
})
Type-Specific Constructors
Create rules for specific types:
Rule.string() // String validation rules
Rule.number() // Number validation rules
Rule.boolean() // Boolean validation rules
Rule.array() // Array validation rules
Rule.object() // Object validation rules
Rule.dateTime() // Date/DateTime validation rules
Example:
defineField({
name: 'age',
type: 'number',
validation: (Rule) => Rule.number().integer().positive().max(120)
})
Common Validation Methods
These methods work across all field types:
required
Mark a field as required (cannot be null or undefined).
Example:
defineField({
name: 'email',
type: 'string',
validation: (Rule) => Rule.required()
})
optional
Explicitly mark a field as optional (allows null/undefined).
custom
Define custom validation logic with full access to document context.
Rule.custom<T>(
fn: (value: T, context: ValidationContext) => true | string | Promise<true | string>,
options?: {bypassConcurrencyLimit?: boolean}
)
Validation function that receives the field value and context. Return true for valid, or an error message string for invalid.
options.bypassConcurrencyLimit
Whether to bypass the default concurrency limit for async validators (default: false)
Example:
defineField({
name: 'slug',
type: 'string',
validation: (Rule) => Rule.custom((slug) => {
if (!slug) return true
if (!/^[a-z0-9-]+$/.test(slug)) {
return 'Slug can only contain lowercase letters, numbers, and hyphens'
}
return true
})
})
Async validation:
defineField({
name: 'username',
type: 'string',
validation: (Rule) => Rule.custom(async (username, context) => {
if (!username) return true
// Check if username is already taken
const client = context.getClient({apiVersion: '2024-01-01'})
const existing = await client.fetch(
'*[_type == "user" && username == $username && _id != $id][0]',
{username, id: context.document._id}
)
if (existing) {
return 'Username is already taken'
}
return true
})
})
Validation Context:
interface ValidationContext {
document: SanityDocument // The full document being edited
parent: unknown // Parent object/array containing this field
path: Path // Path to this field
type: SchemaType // Schema type of this field
getClient: (options: {apiVersion: string}) => SanityClient
i18n: LocaleSource // Internationalization utilities
}
String Validation
min / max
Set minimum and maximum length constraints.
Rule.min(length: number)
Rule.max(length: number)
Example:
defineField({
name: 'bio',
type: 'string',
validation: (Rule) => Rule.min(10).max(500)
})
length
Require an exact length.
Rule.length(length: number)
Example:
defineField({
name: 'countryCode',
type: 'string',
validation: (Rule) => Rule.length(2)
})
uppercase / lowercase
Enforce letter casing.
Rule.uppercase()
Rule.lowercase()
Example:
defineField({
name: 'code',
type: 'string',
validation: (Rule) => Rule.required().uppercase()
})
regex
Validate against a regular expression.
Rule.regex(pattern: RegExp, name?: string, options?: {invert?: boolean})
Regular expression to match
Name for the pattern (used in error messages)
Invert the match (pattern must NOT match)
Example:
defineField({
name: 'phoneNumber',
type: 'string',
validation: (Rule) => Rule.regex(
/^\+?[1-9]\d{1,14}$/,
'E.164 format',
{name: 'E.164 phone number'}
)
})
email
Validate email address format.
Example:
defineField({
name: 'email',
type: 'string',
validation: (Rule) => Rule.required().email()
})
uri
Validate URL format with scheme restrictions.
Rule.uri(options?: {
scheme?: string | RegExp | Array<string | RegExp>
allowRelative?: boolean
relativeOnly?: boolean
allowCredentials?: boolean
})
Example:
defineField({
name: 'website',
type: 'url',
validation: (Rule) => Rule.uri({
scheme: ['https'], // Only allow HTTPS
allowRelative: false
})
})
Number Validation
min / max
Set minimum and maximum value constraints.
Rule.min(value: number)
Rule.max(value: number)
Example:
defineField({
name: 'rating',
type: 'number',
validation: (Rule) => Rule.min(1).max(5)
})
greaterThan / lessThan
Exclusive comparison (value must be strictly greater/less).
Rule.greaterThan(value: number)
Rule.lessThan(value: number)
Example:
defineField({
name: 'discount',
type: 'number',
validation: (Rule) => Rule.greaterThan(0).lessThan(100)
})
integer
Require an integer (no decimals).
Example:
defineField({
name: 'quantity',
type: 'number',
validation: (Rule) => Rule.required().integer().positive()
})
precision
Limit decimal places.
Rule.precision(limit: number)
Example:
defineField({
name: 'price',
type: 'number',
validation: (Rule) => Rule.precision(2) // Max 2 decimal places
})
positive / negative
Require positive or negative values.
Rule.positive() // value >= 0
Rule.negative() // value < 0
Example:
defineField({
name: 'amount',
type: 'number',
validation: (Rule) => Rule.required().positive()
})
Array Validation
min / max
Set minimum and maximum array length.
Rule.min(length: number)
Rule.max(length: number)
Example:
defineField({
name: 'tags',
type: 'array',
of: [{type: 'string'}],
validation: (Rule) => Rule.min(1).max(5)
})
unique
Require all array items to be unique.
Example:
defineField({
name: 'keywords',
type: 'array',
of: [{type: 'string'}],
validation: (Rule) => Rule.required().min(3).unique()
})
Object Validation
fields
Define validation rules for object fields.
Rule.fields(rules: FieldRules)
Example:
defineField({
name: 'author',
type: 'object',
fields: [
{name: 'name', type: 'string'},
{name: 'email', type: 'string'}
],
validation: (Rule) => Rule.fields({
name: (nameRule) => nameRule.required(),
email: (emailRule) => emailRule.required().email()
})
})
Date Validation
min / max
Set minimum and maximum date constraints (ISO 8601 format).
Rule.min(date: string)
Rule.max(date: string)
Example:
defineField({
name: 'publishDate',
type: 'date',
validation: (Rule) => Rule.min('2023-01-01').max('2025-12-31')
})
defineField({
name: 'eventTime',
type: 'datetime',
validation: (Rule) => Rule.min('2024-01-01T00:00:00Z')
})
Asset Validation
assetRequired
Require that an asset (image/file) has been uploaded.
Example:
defineField({
name: 'coverImage',
type: 'image',
validation: (Rule) => Rule.required().assetRequired()
})
Reference Validation
reference
Validate reference fields.
Example:
defineField({
name: 'category',
type: 'reference',
to: [{type: 'category'}],
validation: (Rule) => Rule.required().reference()
})
Validation Levels
Set the severity level of validation messages:
error
Block document publishing (default).
Rule.error(message?: string)
Example:
defineField({
name: 'title',
type: 'string',
validation: (Rule) => Rule.required().error('Title is mandatory')
})
warning
Show warning but allow publishing.
Rule.warning(message?: string)
Example:
defineField({
name: 'excerpt',
type: 'text',
validation: (Rule) => Rule.max(200).warning('Excerpt should be under 200 characters for better SEO')
})
info
Show informational message.
Rule.info(message?: string)
Example:
defineField({
name: 'slug',
type: 'slug',
validation: (Rule) => Rule.required().info('Slug will be used in the URL')
})
Localized Messages
Provide validation messages in multiple languages:
defineField({
name: 'title',
type: 'string',
validation: (Rule) => Rule.required().error({
'en-US': 'Title is required',
'nb-NO': 'Tittel er påkrevd',
'es-ES': 'El título es obligatorio'
})
})
Field References
Compare field values using Rule.valueOfField():
defineField({
name: 'maxPrice',
type: 'number',
validation: (Rule) => Rule.min(Rule.valueOfField('minPrice'))
})
Example with context:
defineField({
name: 'endDate',
type: 'date',
validation: (Rule) => Rule.custom((endDate, context) => {
const startDate = context.parent?.startDate
if (endDate && startDate && endDate < startDate) {
return 'End date must be after start date'
}
return true
})
})
Complex Validation Examples
Conditional Required Fields
defineField({
name: 'externalUrl',
type: 'url',
validation: (Rule) => Rule.custom((url, context) => {
const type = context.parent?.linkType
if (type === 'external' && !url) {
return 'External URL is required when link type is external'
}
return true
})
})
Cross-Field Validation
defineType({
name: 'event',
type: 'document',
fields: [
defineField({
name: 'startDate',
type: 'datetime',
validation: (Rule) => Rule.required()
}),
defineField({
name: 'endDate',
type: 'datetime',
validation: (Rule) => Rule.custom((endDate, context) => {
const startDate = context.document?.startDate
if (!endDate || !startDate) return true
if (endDate <= startDate) {
return 'End date must be after start date'
}
return true
})
})
]
})
Array Item Validation
defineField({
name: 'contributors',
type: 'array',
of: [
{
type: 'object',
fields: [
{name: 'name', type: 'string'},
{name: 'role', type: 'string'}
]
}
],
validation: (Rule) => Rule.custom((contributors) => {
if (!contributors || contributors.length === 0) {
return 'At least one contributor is required'
}
const hasAuthor = contributors.some(c => c.role === 'author')
if (!hasAuthor) {
return 'At least one contributor must have the role "author"'
}
return true
})
})
Async Validation with API
defineField({
name: 'isbn',
type: 'string',
validation: (Rule) => Rule.custom(async (isbn, context) => {
if (!isbn) return true
const client = context.getClient({apiVersion: '2024-01-01'})
const query = '*[_type == "book" && isbn == $isbn && _id != $id][0]'
const existing = await client.fetch(query, {
isbn,
id: context.document._id
})
if (existing) {
return `ISBN ${isbn} is already used by "${existing.title}"`
}
return true
})
})
Best Practices
-
Combine multiple constraints: Chain methods for comprehensive validation
Rule.required().min(10).max(200).regex(/^[A-Z]/)
-
Use appropriate severity levels: Reserve
error() for blocking issues, use warning() for recommendations
-
Provide helpful messages: Explain what’s wrong and how to fix it
Rule.required().error('Product name is required for the catalog')
-
Consider performance: Expensive async validations should use
bypassConcurrencyLimit: true sparingly
-
Test edge cases: Validate with null, undefined, empty strings, etc.
-
Use custom validators for complex logic: Don’t try to express everything with built-in methods