Type something to search...
How to Create Your First Schema in Sanity

How to Create Your First Schema in Sanity

To create your first schema in Sanity, add a TypeScript file to your Studio's schemaTypes folder that exports a document type built with defineType, list its fields with defineField, then add the type to the array exported from schemaTypes/index.ts, which sanity.config.ts passes to schema.types. Run npm run dev, open Studio at http://localhost:3333, and your new document type appears in the sidebar, ready for editors to create content. A schema is simply a description of your content's shape: the document types, their fields and the rules each field follows.

Getting this first schema right sets the pattern for the rest of your project. In this guide you'll build a small blog model step by step: a post document with a title, slug, date, image and body, plus an author document that posts can reference. Along the way you'll learn how Sanity schemas are structured, how Studio turns them into forms, how to make list previews useful, and how to query your new content with GROQ.

What a Sanity Schema Is

A Sanity schema is JavaScript or TypeScript that tells Sanity Studio:

  • Which document types exist: such as post, author or product.
  • Which fields each type has: and what kind of data each field holds.
  • How to present and validate them: labels, help text, input options and rules.

The Content Lake that stores your data doesn't enforce the schema. It stores whatever JSON you send it. The schema shapes the editing experience in Studio, which is why it lives alongside your Studio code. If you haven't set up a project yet, follow how to set up your first Sanity project first.

Where Schema Files Live

A new Studio created with npm create sanity@latest and the clean template has this structure:

my-studio/
├── schemaTypes/
│   └── index.ts
├── sanity.cli.ts
├── sanity.config.ts
├── package.json
└── tsconfig.json

The config imports the schema types and passes them to Studio:

// sanity.config.ts
import { defineConfig } from 'sanity';
import { structureTool } from 'sanity/structure';
import { visionTool } from '@sanity/vision';
import { schemaTypes } from './schemaTypes';

export default defineConfig({
  name: 'default',
  title: 'My Blog',
  projectId: 'your-project-id',
  dataset: 'production',
  plugins: [structureTool(), visionTool()],
  schema: {
    types: schemaTypes,
  },
});

And schemaTypes/index.ts starts as an empty array:

// schemaTypes/index.ts
export const schemaTypes = [];

The project structure guide explains the other files.

Step 1: Create a Document Type

Create schemaTypes/post.ts:

// schemaTypes/post.ts
import { defineField, defineType } from 'sanity';

export const post = defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      title: 'Title',
      type: 'string',
    }),
  ],
});

Each part has a job:

  • name: the identifier stored on every document as _type. Use camelCase, and don't change it once content exists, because existing documents keep the old value.
  • title: the human-friendly label shown in Studio.
  • type: 'document': makes this a top-level type editors can create, list and publish. Other types, such as object, can only live inside documents.
  • fields: the inputs on the form.

defineType and defineField don't change what the schema does. They add type checking and autocomplete, so mistakes like a misspelled option are caught in your editor. The post on defineType and defineField goes into more detail.

Step 2: Register the Type

Add the new type to the array:

// schemaTypes/index.ts
import { post } from './post';

export const schemaTypes = [post];

Start the Studio:

npm run dev

Open http://localhost:3333, sign in, and you'll see Post in the content sidebar. Click it, create a new document, type a title and publish. You've just stored your first document in the Content Lake.

Step 3: Add More Fields

A blog post needs more than a title. Extend the fields array:

// schemaTypes/post.ts
import { defineArrayMember, defineField, defineType } from 'sanity';

export const post = defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      title: 'Title',
      type: 'string',
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: { source: 'title', maxLength: 96 },
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'publishedAt',
      title: 'Published at',
      type: 'datetime',
      initialValue: () => new Date().toISOString(),
    }),
    defineField({
      name: 'excerpt',
      title: 'Excerpt',
      type: 'text',
      rows: 3,
      description: 'A short summary for listings and search results.',
    }),
    defineField({
      name: 'mainImage',
      title: 'Main image',
      type: 'image',
      options: { hotspot: true },
      fields: [
        defineField({ name: 'alt', title: 'Alternative text', type: 'string' }),
      ],
    }),
    defineField({
      name: 'body',
      title: 'Body',
      type: 'array',
      of: [defineArrayMember({ type: 'block' }), defineArrayMember({ type: 'image' })],
    }),
  ],
});

What you've added:

FieldTypeWhat editors get
titlestringA single-line text input, required before publishing
slugslugA URL-friendly value with a Generate button that uses the title
publishedAtdatetimeA date and time picker, defaulting to now
excerpttextA multi-line plain text box
mainImageimageAn image upload with hotspot and crop, plus an alt text field
bodyarray of block and imageA rich text editor that also accepts inline images

Save the file and Studio reloads with the new fields. Existing documents simply show the new fields as empty, with no migration needed. For every available field type and option, see Sanity schema types.

Step 4: Add a Second Document Type

Posts usually have authors. Create schemaTypes/author.ts:

// schemaTypes/author.ts
import { defineField, defineType } from 'sanity';

export const author = defineType({
  name: 'author',
  title: 'Author',
  type: 'document',
  fields: [
    defineField({
      name: 'name',
      title: 'Name',
      type: 'string',
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'image',
      title: 'Image',
      type: 'image',
      options: { hotspot: true },
    }),
    defineField({
      name: 'bio',
      title: 'Bio',
      type: 'text',
      rows: 4,
    }),
  ],
});

Register it:

// schemaTypes/index.ts
import { author } from './author';
import { post } from './post';

export const schemaTypes = [post, author];

Step 5: Link Posts to Authors

Add a reference field to post, after slug:

defineField({
  name: 'author',
  title: 'Author',
  type: 'reference',
  to: [{ type: 'author' }],
}),

Editors can now search for an existing author or create a new one from inside the post. In the stored document, the reference is just an ID:

{
  "_type": "post",
  "title": "Hello Sanity",
  "author": { "_type": "reference", "_ref": "a1b2c3d4-..." }
}

References are a big topic, including weak references and filtered pickers, so they have their own guide: how to use references in Sanity schemas.

Step 6: Make the Document List Readable

By default, Studio shows each document's title in lists if it can find a title field. You can control this with a preview:

// in schemaTypes/post.ts, alongside fields
preview: {
  select: {
    title: 'title',
    author: 'author.name',
    media: 'mainImage',
  },
  prepare({ title, author, media }) {
    return {
      title,
      subtitle: author ? `by ${author}` : 'No author',
      media,
    };
  },
},

select can follow references with dot notation, as in author.name, and prepare turns the selected values into the title, subtitle and thumbnail shown in lists and reference pickers. It's a small touch that makes Studio much easier to use once you have dozens of documents.

Step 7: Query Your Content

With a couple of posts published, open the Vision tab in Studio and run:

*[_type == "post"] | order(publishedAt desc){
  title,
  "slug": slug.current,
  publishedAt,
  "author": author->name
}

You'll get something like:

[
  {
    "title": "Hello Sanity",
    "slug": "hello-sanity",
    "publishedAt": "2026-11-04T09:00:00.000Z",
    "author": "Sajjad"
  }
]

The -> operator follows the reference and pulls in the author's name. That's the same query your frontend will run with @sanity/client or next-sanity. To learn more, start with what is GROQ.

Common First-Schema Mistakes

  • Forgetting to register the type: if a type doesn't appear in Studio, check it's in the schemaTypes array.
  • Duplicate names: every type name must be unique across the schema, including object types. Studio shows a schema error if two clash.
  • Renaming fields with content: changing name hides the old data in Studio, because the documents still store the old key. Add a new field and migrate instead.
  • Using string for long text: use text for multi-line plain text and an array of block for rich text.
  • Skipping descriptions: a short description on tricky fields saves editors a lot of guessing.

Once you have a few types, read content modelling best practices in Sanity to plan the rest of your model.


FAQ: Your First Sanity Schema

No. Schemas can be plain JavaScript, and the shape is identical. TypeScript is the default in new projects because defineType and defineField give you autocomplete and catch mistakes, so it's worth using even if the rest of your code is JavaScript.

A document type is a top-level item that editors create, list and publish on its own, with its own ID. An object type is a group of fields that only exists inside a document or another object, such as an address or an SEO block.

Not when you add fields, because the Content Lake is schemaless and new fields simply start empty. When you rename or remove a field, or change its type, existing documents keep the old data, so you'll need a migration script if you want to move it.

The most common cause is that the type isn't included in the schemaTypes array passed to schema.types in sanity.config.ts. Also check the terminal and browser console for schema errors, such as a duplicate type name or a missing type property.

Yes, and you should. The usual pattern is one file per type in schemaTypes, sometimes grouped into documents and objects folders, all imported into schemaTypes/index.ts. Sanity only cares about the final array of types.

Add a validation function to the field, such as validation: (rule) => rule.required(). Studio then blocks publishing until the field has a value. Validation only runs in Studio, not on API writes.


Conclusion

Creating your first schema in Sanity takes three moves: define a document type with defineType and defineField, add it to the array in schemaTypes/index.ts, and run the Studio to see the form it produces. From there you grow the model by adding fields, more document types and references between them, with previews to keep lists readable and GROQ to check what's stored.

Keep each type in its own file, choose field names carefully because they become your data's keys, and add validation and descriptions as you go. When you're ready to make fields stricter, continue with how to add validation rules to Sanity fields.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper