
Working With Arrays in Sanity Schemas
An array field in Sanity is a field with type: 'array' and an of property listing the types its items can be. Arrays can hold primitives such as strings and numbers, objects, references to other documents, images, or Portable Text blocks, and editors can add, remove and reorder items in the Studio. Each object item gets a unique _key, which Sanity uses to track items through edits and real-time collaboration. You can control how arrays look with options.layout, restrict them with validation rules like min, max and unique, and query them in GROQ with the [] operator.
Arrays are behind most of the flexible content you'll build in Sanity: tag lists, image galleries, related posts, FAQ sections and page builders made of modular blocks. Getting the array design right early saves painful migrations later. This article covers the kinds of arrays you can build, the rules about what can go inside them, display options, the insert menu for page builders, validation, how _key works, querying arrays with GROQ and updating them through the API.
The Basic Array Field
Every array has a name, type: 'array' and an of list:
// schemaTypes/post.ts
import { defineArrayMember, defineField, defineType } from 'sanity'
export const post = defineType({
name: 'post',
type: 'document',
fields: [
defineField({ name: 'title', type: 'string' }),
defineField({
name: 'tags',
type: 'array',
of: [defineArrayMember({ type: 'string' })],
}),
],
})
The of list says which types are allowed, not how many items there are. Wrapping each member in defineArrayMember() gives you type checking for the member's options, as explained in using defineType and defineField.
Types of Arrays
Arrays of Primitives
Strings, numbers and booleans are stored directly:
{ "tags": ["sanity", "cms", "typescript"] }
Primitive arrays have no _key, because there's nothing to attach it to. That's fine for short, simple lists like tags, but it makes them harder to target precisely in patches.
Arrays of Objects
Objects can be defined inline or as a named type registered in your schema:
defineField({
name: 'faqs',
title: 'FAQs',
type: 'array',
of: [
defineArrayMember({
type: 'object',
name: 'faq',
fields: [
defineField({ name: 'question', type: 'string' }),
defineField({ name: 'answer', type: 'text', rows: 3 }),
],
preview: {
select: { title: 'question', subtitle: 'answer' },
},
}),
],
})
Stored data includes a _key and the member's _type:
{
"faqs": [
{ "_key": "a1b2c3", "_type": "faq", "question": "What is GROQ?", "answer": "Sanity's query language." }
]
}
Always give inline objects a name. It becomes the _type of each item, which is what you'll filter and switch on in your front end. For objects you use in several places, define them once as named types; building reusable object types in Sanity covers that approach.
Arrays of References
To link to several documents, use references as members:
defineField({
name: 'relatedPosts',
type: 'array',
of: [defineArrayMember({ type: 'reference', to: [{ type: 'post' }] })],
validation: (rule) => rule.unique().max(3),
})
Reference arrays are ordered, so editors control the order in which related items appear. The post on how to use references in Sanity schemas goes deeper into reference options and filters.
Mixed Arrays
An array can allow several types at once. This is how page builders work:
defineField({
name: 'sections',
type: 'array',
of: [
defineArrayMember({ type: 'hero' }),
defineArrayMember({ type: 'textWithImage' }),
defineArrayMember({ type: 'callToAction' }),
defineArrayMember({ type: 'reference', to: [{ type: 'testimonial' }] }),
],
})
Editors see an Add item menu listing each type. Portable Text is a specialised mixed array too: its members are block plus any custom objects you add.
Rules About What Can Go in an Array
Two restrictions catch most people:
- No arrays directly inside arrays: you can't put
type: 'array'in anoflist. Wrap the inner array in an object instead, for example arowobject with acellsarray field. - No mixing primitives and objects: an array can hold primitives or objects, not both. You can't combine
stringandimagemembers in one array.
The workaround for nesting looks like this:
defineField({
name: 'table',
type: 'array',
of: [
defineArrayMember({
type: 'object',
name: 'row',
fields: [
defineField({
name: 'cells',
type: 'array',
of: [defineArrayMember({ type: 'string' })],
}),
],
}),
],
})
A third, softer rule: if you use several members of the same base type, such as two different inline objects, each needs a unique name so Sanity can tell them apart.
Display Options
Arrays accept several options that change the editing experience.
| Option | Values | Effect |
|---|---|---|
layout | 'list', 'grid', 'tags' | list is the default; grid suits images; tags turns a string array into a tag input |
list | Array of values or titled values | Shows predefined choices as checkboxes for primitive arrays |
sortable | Boolean | Set to false to remove drag handles |
modal | { type: 'dialog' or 'popover', width } | Controls how object items open for editing |
insertMenu | Object | Configures the add-item menu for mixed arrays |
Tags Layout
defineField({
name: 'tags',
type: 'array',
of: [defineArrayMember({ type: 'string' })],
options: { layout: 'tags' },
})
Editors type a value and press Enter to add a tag, which is much faster than clicking Add item for each one.
Predefined Choices
For a fixed set of values, use list to render checkboxes:
defineField({
name: 'platforms',
type: 'array',
of: [defineArrayMember({ type: 'string' })],
options: {
list: [
{ title: 'Web', value: 'web' },
{ title: 'iOS', value: 'ios' },
{ title: 'Android', value: 'android' },
],
},
})
The stored values are the value strings, so your front end doesn't depend on display titles.
Grid Layout for Galleries
defineField({
name: 'gallery',
type: 'array',
of: [
defineArrayMember({
type: 'image',
options: { hotspot: true },
fields: [defineField({ name: 'alt', type: 'string', title: 'Alt text' })],
}),
],
options: { layout: 'grid' },
})
Images appear as thumbnails, and editors can drag files onto the array to upload several at once.
The Insert Menu for Page Builders
When a mixed array has many members, a plain dropdown gets unwieldy. The insertMenu option lets you add groups, a filter and a grid view with preview images:
defineField({
name: 'sections',
type: 'array',
of: [
defineArrayMember({ type: 'hero' }),
defineArrayMember({ type: 'textWithImage' }),
defineArrayMember({ type: 'callToAction' }),
defineArrayMember({ type: 'faqList' }),
],
options: {
insertMenu: {
filter: true,
groups: [
{ name: 'intro', title: 'Intro', of: ['hero'] },
{ name: 'content', title: 'Content', of: ['textWithImage', 'faqList'] },
{ name: 'conversion', title: 'Conversion', of: ['callToAction'] },
],
views: [
{ name: 'list' },
{ name: 'grid', previewImageUrl: (type) => `/static/insert-menu/${type}.png` },
],
},
},
})
The grid view loads preview images from the paths you return, so place them in your Studio's static folder. The insert menu API is marked as alpha in Sanity's types, so check the release notes when upgrading.
Validation
Array rules are checked against the whole array:
validation: (rule) => rule.required().min(1).max(10).unique()
- min and max: the number of items.
- length: an exact number of items.
- unique: no duplicate values. For references, it compares
_ref, so the same document can't be added twice.
Members can have their own validation too. For example, an image member with a required alt field is validated per item. See how to add validation rules to Sanity fields for custom rules, such as checking that only one item has featured set to true.
How _key Works
Every object in an array needs a _key that is unique within that array. The Studio generates them automatically. Sanity uses keys to:
- Track items through reordering, so real-time edits from two editors don't overwrite each other.
- Target items in patches, with paths like
faqs[_key=="a1b2c3"].answer. - Render lists in React, where
_keymakes an idealkeyprop.
If you create documents through the API or a migration script, you must add keys yourself or ask the client to do it. Missing keys show a warning in the Studio with a button to add them. Use _key as the React key rather than the array index:
{post.faqs?.map((faq) => (
<details key={faq._key}>
<summary>{faq.question}</summary>
<p>{faq.answer}</p>
</details>
))}
Querying Arrays With GROQ
The [] operator flattens or traverses arrays:
*[_type == "post" && slug.current == $slug][0]{
title,
tags,
"tagCount": count(tags),
faqs[]{ _key, question, answer },
"related": relatedPosts[]->{ title, "slug": slug.current },
"heroSection": sections[_type == "hero"][0]
}
A few useful patterns:
- Membership:
*[_type == "post" && "sanity" in tags]finds posts tagged "sanity". - Filtering items:
sections[_type == "callToAction"]returns only matching members. - Dereferencing:
relatedPosts[]->resolves every reference in the array. - Counting:
count(faqs)returns the number of items.
For arrays inside arrays, such as sections[].items[], see how to query nested arrays in GROQ.
Updating Arrays Through the API
@sanity/client has patch methods designed for arrays. Use autoGenerateArrayKeys so new object items get keys:
// scripts/add-faq.ts
import { createClient } from '@sanity/client'
const client = createClient({
projectId: 'your-project-id',
dataset: 'production',
apiVersion: '2026-11-01',
token: process.env.SANITY_WRITE_TOKEN,
useCdn: false,
})
await client
.patch('post-123')
.setIfMissing({ faqs: [] })
.append('faqs', [{ _type: 'faq', question: 'Is it free?', answer: 'There is a free plan.' }])
.commit({ autoGenerateArrayKeys: true })
// Insert after a specific item
await client
.patch('post-123')
.insert('after', 'faqs[_key=="a1b2c3"]', [
{ _type: 'faq', question: 'Can I self-host?', answer: 'The Studio, yes. The Content Lake is hosted.' },
])
.commit({ autoGenerateArrayKeys: true })
// Remove an item by key
await client.patch('post-123').unset(['faqs[_key=="a1b2c3"]']).commit()
setIfMissing prevents errors when the field doesn't exist yet, and targeting by _key is safer than using an index, which can change if someone reorders the list at the same time.
FAQ: Arrays in Sanity
No. Sanity doesn't allow arrays as direct members of arrays. Wrap the inner array in an object type, such as a row object with a cells field, and use that object as the member.
No. An array can hold primitive values or objects, but not a mix of both. If you need both, convert the primitives into small objects with a single field.
_key is a unique identifier for each object item in an array. The Studio generates it automatically. When writing through the API, either set it yourself or commit with autoGenerateArrayKeys set to true.
Add rule.unique() to the array's validation. For reference arrays it compares the referenced document IDs, so duplicates are flagged before publishing.
Use the in operator, for example [_type == "post" && "sanity" in tags]. For object arrays, filter on a field, such as count(faqs[question match "price"]) > 0.
Use references when items are documents in their own right that are reused or edited separately, like authors or related posts. Use objects when the items only make sense inside this one document, like FAQs on a single page.
Conclusion
Arrays are one of Sanity's most versatile field types. Primitive arrays handle tags and simple lists, object arrays model repeating structured content, reference arrays link documents in a chosen order, and mixed arrays power page builders and Portable Text. Options like layout, list and insertMenu shape the editing experience, while min, max and unique keep the data clean.
Remember the two structural rules, no arrays directly inside arrays and no mixing primitives with objects, and always name inline objects. Use _key for React keys and patch targets, and you'll have arrays that are pleasant to edit, easy to query and safe to update programmatically.


