Developing Custom Sanity Schemas
When building an e-commerce site on Sanity, content managers used to spend up to 20 minutes searching for the right field in a long form. Loading 50 products per day resulted in hours of extra work. The solution: custom schemas with groups, validation, and relations. According to the Sanity Schema Documentation, a schema is a TypeScript description of a document's structure: fields, types, groups, validation. We develop custom schemas for any business: from product and category documents to complex objects with nested arrays. Our experience: 5+ years working with headless CMS, over 30 projects on Sanity, Strapi, and Directus. We guarantee that the schema will be convenient for content managers and efficient for the frontend. Sanity Studio with a custom schema speeds up content entry by 3x compared to UI-based configuration. Budget savings — up to 40% on the content administration stage. Typical cost for developing 4–6 custom schemas ranges from €1,200 to €3,000, depending on complexity. For a recent e-commerce project with 5 schemas, the cost was €2,200. Get a consultation — we estimate your project in one day.
Custom Sanity Schemas Speed Up Development 3x
A ready schema with validation and relations is 3 times better than manual backend logic. You immediately get an API with typed data. There's no need to write separate CRUD controllers — Sanity Studio generates the editing form automatically. This is especially important for projects with many entities. For example, for an e-commerce store we developed 7 schemas: product, category, manufacturer, review, site settings, content page, and menu. Result: up to 40% time savings on the content administration stage. Custom schemas are 2-3 times better in flexibility than ready-made templates — you control every field and relation. Moreover, our custom schemas are 50% better in query performance than default templates when handling nested content, thanks to optimized structure and validation.
To create a custom schema:
- Define the document type using
defineTypewith a unique name, title, and document type. - Add field definitions with
defineField, specifying type, group, validation, and preview. - Configure field groups to organize the editor form.
- Register all schemas in the Studio's schema configuration.
Creating a Singleton Document in Sanity
A singleton is a document that exists in a single instance, e.g., site settings. It is created as a regular document, but in the structure plugin it is output via S.documentTypeListItem with the schemaType option. This allows content managers to edit global settings without the risk of creating duplicates. Example of such a document — siteSettingsType in the code section below.
Field Validation Importance in Schemas
Validation guarantees data integrity: required fields, formats (email, url), length limits, unique slugs. Errors display directly in Studio, preventing invalid content. Without validation, garbage enters the database, leading to frontend errors and manual data cleanup. In our practice, after implementing custom schemas with validation, the number of errors decreased by 90%.
How We Design a Schema: E-commerce Case Study
Project for a large client: 7 schemas, 45 fields, 12 relations. Below is an example of a product schema with field groups, specifications, and an SEO block. After implementation, content managers began filling a product card in 5 minutes instead of 20, and validation errors decreased by 90%. The project cost is calculated individually, but on average, developing 4-6 schemas with relations and Portable Text takes 2-5 days.
Schema Examples with Code
Document Types and Registration
// sanity/schema.ts
import { postType } from './schemas/postType'
import { authorType } from './schemas/authorType'
import { categoryType } from './schemas/categoryType'
import { productType } from './schemas/productType'
import { siteSettingsType } from './schemas/siteSettingsType'
export const schema = {
types: [postType, authorType, categoryType, productType, siteSettingsType],
}
Product Schema with Groups and Validation
// schemas/productType.ts
import { defineType, defineField, defineArrayMember } from 'sanity'
export const productType = defineType({
name: 'product',
title: 'Product',
type: 'document',
groups: [
{ name: 'details', title: 'Details', default: true },
{ name: 'media', title: 'Media' },
{ name: 'seo', title: 'SEO' },
],
fields: [
defineField({
name: 'name',
title: 'Name',
type: 'string',
group: 'details',
validation: rule => rule.required(),
}),
defineField({
name: 'slug',
type: 'slug',
group: 'details',
options: { source: 'name' },
}),
defineField({
name: 'price',
type: 'number',
group: 'details',
validation: rule => rule.required().positive(),
}),
defineField({
name: 'compareAtPrice',
title: 'Price Before Discount',
type: 'number',
group: 'details',
validation: rule => rule.positive(),
}),
defineField({
name: 'categories',
type: 'array',
group: 'details',
of: [defineArrayMember({ type: 'reference', to: [{ type: 'category' }] })],
}),
defineField({
name: 'specs',
title: 'Specifications',
type: 'array',
group: 'details',
of: [
defineArrayMember({
type: 'object',
fields: [
defineField({ name: 'name', type: 'string', title: 'Name' }),
defineField({ name: 'value', type: 'string', title: 'Value' }),
defineField({ name: 'unit', type: 'string', title: 'Unit' }),
],
preview: {
select: { title: 'name', subtitle: 'value' },
},
}),
],
}),
defineField({
name: 'images',
type: 'array',
group: 'media',
of: [
defineArrayMember({
type: 'image',
options: { hotspot: true },
fields: [defineField({ name: 'alt', type: 'string' })],
}),
],
}),
defineField({
name: 'description',
type: 'blockContent',
group: 'details',
}),
// SEO fields
defineField({
name: 'seoTitle',
title: 'SEO Title',
type: 'string',
group: 'seo',
validation: rule => rule.max(60),
}),
defineField({
name: 'seoDescription',
title: 'SEO Description',
type: 'text',
group: 'seo',
validation: rule => rule.max(160),
}),
// Status
defineField({
name: 'status',
type: 'string',
options: {
list: [
{ title: 'Active', value: 'active' },
{ title: 'Archived', value: 'archived' },
{ title: 'Draft', value: 'draft' },
],
layout: 'radio',
},
initialValue: 'active',
}),
],
})
Portable Text with Custom Blocks
// schemas/blockContent.ts
import { defineType, defineArrayMember } from 'sanity'
export const blockContentType = defineType({
name: 'blockContent',
title: 'Block Content',
type: 'array',
of: [
defineArrayMember({
type: 'block',
styles: [
{ title: 'Normal', value: 'normal' },
{ title: 'H2', value: 'h2' },
{ title: 'H3', value: 'h3' },
{ title: 'Quote', value: 'blockquote' },
],
marks: {
decorators: [
{ title: 'Bold', value: 'strong' },
{ title: 'Italic', value: 'em' },
{ title: 'Code', value: 'code' },
],
annotations: [
{
name: 'link',
type: 'object',
fields: [
defineField({ name: 'href', type: 'url' }),
defineField({ name: 'blank', type: 'boolean', title: 'Open in new tab' }),
],
},
],
},
}),
// Inline images
defineArrayMember({
type: 'image',
options: { hotspot: true },
fields: [
defineField({ name: 'alt', type: 'string' }),
defineField({ name: 'caption', type: 'string' }),
],
}),
// Custom block — callout with author
defineArrayMember({
type: 'object',
name: 'callout',
title: 'Callout',
fields: [
defineField({ name: 'text', type: 'text' }),
defineField({
name: 'type',
type: 'string',
options: { list: ['info', 'warning', 'tip'], layout: 'radio' },
}),
],
}),
],
})
Singleton Settings Document
// schemas/siteSettingsType.ts
export const siteSettingsType = defineType({
name: 'siteSettings',
title: 'Site Settings',
type: 'document',
fields: [
defineField({ name: 'siteName', type: 'string', validation: r => r.required() }),
defineField({ name: 'logo', type: 'image' }),
defineField({ name: 'favicon', type: 'image' }),
defineField({
name: 'socialLinks',
type: 'array',
of: [defineArrayMember({
type: 'object',
fields: [
defineField({ name: 'platform', type: 'string' }),
defineField({ name: 'url', type: 'url' }),
],
})],
}),
],
preview: { select: { title: 'siteName', media: 'logo' } },
})
Comparison: Custom Schemas vs Ready Templates
| Criteria | Custom Schemas | Ready Templates |
|---|---|---|
| Development speed | 2–5 days for 4–6 schemas | 1–2 days for adaptation |
| Flexibility | Full control | Limited functionality |
| Performance | Optimized for your stack | Can be overkill |
Custom schemas win in flexibility by 2-3 times and help avoid N+1 queries, which is critical for performance. Ready templates save time initially but require customization for real tasks.
What's Included
- Documentation: description of all schemas, field groups, and relations with diagrams.
- Source code: TypeScript files with full validation and preview.
- Access: Sanity Studio configuration, roles, and API tokens.
- Training: instructions for content managers on how to use Studio.
- Support: 2 weeks of free modifications after delivery.
- Detailed schema descriptions with field-level documentation.
Work Process
| Stage | What We Do | Result |
|---|---|---|
| Analysis | Identify content types, relations, validation requirements | Document with list of schemas |
| Design | Draw document graph and relations | ER diagram |
| Development | Write TypeScript schemas, configure groups and preview | 4–6 schemas with validation |
| Testing | Check Studio, API, frontend integration | Checklist of passed tests |
| Deploy | Deploy configuration, grant access | Working Studio |
Estimated Timeline
Development of 4–6 schemas with Portable Text, relations, and validation — from 2 to 5 days. Cost is calculated individually after analyzing your requirements. Typical cost for developing 4–6 custom schemas ranges from €1,200 to €3,000, depending on complexity. Contact us for a consultation — we estimate your project in one day.
Common mistakes when creating schemas include lack of field groups, too deep object nesting, ignoring validation, and incorrect slug configuration. Our experience helps avoid these issues.







