Schema hierarchy
The schema system has a clear hierarchy:Schema
└── Types
├── Document Types (top-level content)
│ └── Fields
│ ├── Primitive (string, number, boolean, etc.)
│ ├── Complex (array, object, reference)
│ └── Special (image, file, slug, etc.)
└── Object Types (reusable field groups)
Document types
Document types are top-level content items that can be created, edited, and published:import {defineType, defineField} from 'sanity'
import {UserIcon} from '@sanity/icons'
export default defineType({
name: 'author',
type: 'document',
title: 'Author',
icon: UserIcon,
fields: [
defineField({
name: 'name',
type: 'string',
title: 'Name',
validation: (Rule) => Rule.required(),
}),
defineField({
name: 'bio',
type: 'text',
title: 'Biography',
}),
defineField({
name: 'image',
type: 'image',
title: 'Profile Image',
options: {
hotspot: true,
},
}),
],
})
Field types
Primitive types
defineType({
name: 'post',
type: 'document',
fields: [
// String
defineField({
name: 'title',
type: 'string',
title: 'Title',
}),
// Text (multi-line string)
defineField({
name: 'excerpt',
type: 'text',
title: 'Excerpt',
rows: 4,
}),
// Number
defineField({
name: 'views',
type: 'number',
title: 'View Count',
}),
// Boolean
defineField({
name: 'featured',
type: 'boolean',
title: 'Featured',
}),
// Date
defineField({
name: 'publishedAt',
type: 'date',
title: 'Published Date',
}),
// Datetime
defineField({
name: 'scheduledAt',
type: 'datetime',
title: 'Scheduled Time',
}),
],
})
Complex types
defineType({
name: 'post',
type: 'document',
fields: [
// Array
defineField({
name: 'tags',
type: 'array',
title: 'Tags',
of: [{type: 'string'}],
}),
// Object
defineField({
name: 'seo',
type: 'object',
title: 'SEO',
fields: [
defineField({
name: 'metaTitle',
type: 'string',
title: 'Meta Title',
}),
defineField({
name: 'metaDescription',
type: 'text',
title: 'Meta Description',
}),
],
}),
// Reference
defineField({
name: 'author',
type: 'reference',
title: 'Author',
to: [{type: 'author'}],
}),
// Array of references
defineField({
name: 'relatedPosts',
type: 'array',
title: 'Related Posts',
of: [
{
type: 'reference',
to: [{type: 'post'}],
},
],
}),
],
})
Special types
defineType({
name: 'post',
type: 'document',
fields: [
// Image with hotspot
defineField({
name: 'coverImage',
type: 'image',
title: 'Cover Image',
options: {
hotspot: true,
},
fields: [
defineField({
name: 'alt',
type: 'string',
title: 'Alt Text',
}),
defineField({
name: 'caption',
type: 'string',
title: 'Caption',
}),
],
}),
// File
defineField({
name: 'attachment',
type: 'file',
title: 'Attachment',
}),
// Slug
defineField({
name: 'slug',
type: 'slug',
title: 'Slug',
options: {
source: 'title',
maxLength: 96,
},
}),
// URL
defineField({
name: 'website',
type: 'url',
title: 'Website',
}),
],
})
Portable Text (block content)
Portable Text is Sanity’s rich text format:defineField({
name: 'content',
type: 'array',
title: 'Content',
of: [
{
type: 'block',
styles: [
{title: 'Normal', value: 'normal'},
{title: 'H1', value: 'h1'},
{title: 'H2', value: 'h2'},
{title: 'H3', value: 'h3'},
{title: 'Quote', value: 'blockquote'},
],
lists: [
{title: 'Bullet', value: 'bullet'},
{title: 'Numbered', value: 'number'},
],
marks: {
decorators: [
{title: 'Strong', value: 'strong'},
{title: 'Emphasis', value: 'em'},
{title: 'Code', value: 'code'},
],
annotations: [
{
name: 'link',
type: 'object',
title: 'Link',
fields: [
{
name: 'href',
type: 'url',
title: 'URL',
},
],
},
],
},
},
// Add custom block types
{
type: 'image',
options: {hotspot: true},
},
],
})
Reusable object types
Define reusable object types that can be used across documents:// seo.ts
export const seoType = defineType({
name: 'seo',
type: 'object',
title: 'SEO',
fields: [
defineField({
name: 'metaTitle',
type: 'string',
title: 'Meta Title',
validation: (Rule) => Rule.max(60),
}),
defineField({
name: 'metaDescription',
type: 'text',
title: 'Meta Description',
rows: 3,
validation: (Rule) => Rule.max(160),
}),
defineField({
name: 'ogImage',
type: 'image',
title: 'Open Graph Image',
}),
],
})
// Use it in documents
defineType({
name: 'post',
type: 'document',
fields: [
// ...
defineField({
name: 'seo',
type: 'seo',
title: 'SEO',
}),
],
})
Validation
Add validation rules to ensure data quality:defineField({
name: 'title',
type: 'string',
title: 'Title',
validation: (Rule) => Rule.required().min(10).max(100),
})
defineField({
name: 'email',
type: 'string',
title: 'Email',
validation: (Rule) => Rule.required().email(),
})
defineField({
name: 'slug',
type: 'slug',
title: 'Slug',
validation: (Rule) => Rule.required().custom((slug) => {
if (!slug?.current) return true
const regex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
if (!regex.test(slug.current)) {
return 'Slug must be lowercase and use hyphens'
}
return true
}),
})
defineField({
name: 'content',
type: 'array',
of: [{type: 'block'}],
validation: (Rule) => Rule.required().min(1),
})
Field options
Customize field behavior with options:defineField({
name: 'status',
type: 'string',
title: 'Status',
options: {
list: [
{title: 'Draft', value: 'draft'},
{title: 'Published', value: 'published'},
{title: 'Archived', value: 'archived'},
],
layout: 'radio', // or 'dropdown'
},
})
defineField({
name: 'featured',
type: 'boolean',
title: 'Featured',
description: 'Display this post on the homepage',
initialValue: false,
})
defineField({
name: 'slug',
type: 'slug',
title: 'Slug',
options: {
source: 'title',
maxLength: 96,
slugify: (input) => input.toLowerCase().replace(/\s+/g, '-'),
},
})
Conditional fields
Show/hide fields based on other field values:defineType({
name: 'post',
type: 'document',
fields: [
defineField({
name: 'hasVideo',
type: 'boolean',
title: 'Has Video',
}),
defineField({
name: 'videoUrl',
type: 'url',
title: 'Video URL',
hidden: ({document}) => !document?.hasVideo,
}),
],
})
Read-only fields
defineField({
name: 'createdAt',
type: 'datetime',
title: 'Created At',
readOnly: true,
initialValue: () => new Date().toISOString(),
})
Field groups
Organize fields into tabs:defineType({
name: 'post',
type: 'document',
groups: [
{name: 'content', title: 'Content', default: true},
{name: 'seo', title: 'SEO'},
{name: 'settings', title: 'Settings'},
],
fields: [
defineField({
name: 'title',
type: 'string',
title: 'Title',
group: 'content',
}),
defineField({
name: 'metaTitle',
type: 'string',
title: 'Meta Title',
group: 'seo',
}),
defineField({
name: 'featured',
type: 'boolean',
title: 'Featured',
group: 'settings',
}),
],
})
Preview configuration
Customize how documents appear in lists:defineType({
name: 'author',
type: 'document',
preview: {
select: {
title: 'name',
subtitle: 'role',
media: 'image',
},
prepare({title, subtitle, media}) {
return {
title,
subtitle: subtitle ? `Role: ${subtitle}` : undefined,
media,
}
},
},
})
Initial values
Set default values for new documents:defineType({
name: 'post',
type: 'document',
initialValue: () => ({
status: 'draft',
publishedAt: new Date().toISOString(),
featured: false,
}),
})
Array member types
Define inline array item schemas:import {defineArrayMember} from 'sanity'
defineField({
name: 'sections',
type: 'array',
title: 'Page Sections',
of: [
defineArrayMember({
name: 'hero',
type: 'object',
title: 'Hero Section',
fields: [
defineField({
name: 'heading',
type: 'string',
title: 'Heading',
}),
defineField({
name: 'image',
type: 'image',
title: 'Background Image',
}),
],
preview: {
select: {title: 'heading', media: 'image'},
},
}),
defineArrayMember({
name: 'textBlock',
type: 'object',
title: 'Text Block',
fields: [
defineField({
name: 'text',
type: 'array',
of: [{type: 'block'}],
}),
],
}),
],
})
Use
defineType, defineField, and defineArrayMember helpers for better TypeScript inference and autocomplete.Document type names must be unique across your entire schema. Object types can be reused across multiple documents.