npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gearbox-built/sanity-schema-tool

v2.0.1

Published

Helper framework to build schema for Sanity Studio V3

Downloads

457

Readme

⚡️ Gearbox Sanity Schema Tool

A fully-typed schema builder for Sanity Studio V3/V4 that provides thoughtful defaults, succinct syntax, and powerful helpers to make writing schemas faster and more maintainable.

Built on top of Sanity's defineField functions to benefit from their improved developer experience while adding helpful abstractions.

Features

  • 🎯 Shorthand Syntax - Write schemas faster with functions like F.slug(), F.title(), F.string({ name: 'foo' })
  • 🧩 Custom Fields - Pre-built composite fields for common patterns (hero, link, media, seo)
  • 📦 Smart Defaults - Sensible defaults that follow Sanity best practices
  • 🎨 Group & Fieldset Helpers - Organize complex schemas with simple, readable syntax
  • ⚙️ Configurable - Set global defaults for field types across your entire project
  • 🔌 Extensible - Add custom field types that integrate seamlessly with the tool
  • 📘 Fully Typed - Complete TypeScript support with inference and autocomplete

Installation

# yarn
yarn add @gearbox-built/sanity-schema-tool

# npm
npm install @gearbox-built/sanity-schema-tool

# pnpm
pnpm add @gearbox-built/sanity-schema-tool

Quick Start

Import the schema tool and start building schemas:

import {F} from '@gearbox-built/sanity-schema-tool'

export const movie = F.document({
  name: 'movie',
  fields: [
    F.title(),
    F.slug(),
    F.image({name: 'poster'}),
    F.text({name: 'synopsis'}),
    F.array({name: 'cast', of: [F.string()]}),
    F.publishedDate(),
  ],
})

That's equivalent to:

export const movie = {
  type: 'document',
  name: 'movie',
  title: 'Movie',
  fields: [
    {
      type: 'string',
      name: 'title',
      title: 'Title',
    },
    {
      type: 'slug',
      name: 'slug',
      title: 'Slug',
      required: true,
      description: 'Leave blank to autofill.',
      options: {
        source: 'title',
        maxLength: 96,
      },
      validation: (Rule) => Rule.required(),
    },
    {
      type: 'image',
      name: 'poster',
      title: 'Poster',
    },
    {
      type: 'text',
      name: 'synopsis',
      title: 'Synopsis',
    },
    {
      type: 'array',
      name: 'cast',
      title: 'Cast',
      of: [{type: 'string'}],
    },
    {
      type: 'date',
      name: 'publishedDate',
      title: 'Published Date',
      required: true,
      options: {
        dateFormat: 'MMMM DD, YYYY',
      },
      initialValue: () => new Date().toISOString().split('T')[0],
      validation: (Rule) => Rule.required(),
    },
  ],
}

Core Concepts

The schema tool exports five main namespaces:

  • F - Fields (all Sanity field types + custom composite fields)
  • G - Groups (tab organization helpers)
  • P - Preview (preview configuration helpers)
  • V - Validation (validation helpers)
  • FS - Fieldsets (fieldset helpers)

Auto-generated Names and Titles

Fields automatically generate names and titles using lodash utilities:

  • Names: camelCase of the title (if title provided) or explicit name
  • Titles: startCase of the name (if name provided) or explicit title
// These are equivalent:
F.string({name: 'firstName'}) // title becomes "First Name"
F.string({title: 'First Name'}) // name becomes "firstName"
F.string({name: 'firstName', title: 'Given Name'}) // uses both as specified

Required Fields

The required boolean prop is a shorthand that automatically adds validation:

F.string({name: 'email', required: true})
// Equivalent to:
F.string({
  name: 'email',
  validation: (Rule) => Rule.required(),
})

API Reference

F - Fields

Core Field Types

All standard Sanity field types are available:

// String-based types
F.string(props) // Basic string
F.text(props) // Multi-line text
F.email(props) // Email validation
F.url(props) // URL validation
F.slug(props) // Slug with smart defaults

// Number and date types
F.number(props) // Number input
F.date(props) // Date picker
F.datetime(props) // Date and time picker

// Media types
F.image(props) // Image upload
F.file(props) // File upload

// Structural types
F.boolean(props) // Boolean toggle
F.object(props) // Object with nested fields
F.array(props) // Array of items
F.reference(types, props) // Reference to other documents
F.block(props) // Block content
F.geopoint(props) // Geographic coordinates

// Document type
F.document(props) // Document definition

Example:

const product = F.document({
  name: 'product',
  fields: [
    F.string({name: 'sku', required: true}),
    F.number({name: 'price', validation: (Rule) => Rule.min(0)}),
    F.boolean({name: 'inStock', initialValue: true}),
    F.geopoint({name: 'location'}),
  ],
})

Custom Composite Fields

Pre-built fields for common patterns:

F.title(props) // Title field with defaults
F.excerpt(props) // Excerpt/summary field
F.publishedDate(props) // Published date with formatting
F.checkbox(props) // Boolean as checkbox
F.radio(list, props) // Radio button group
F.dropdown(list, props) // Dropdown select
F.category(types, props) // Single category reference
F.categories(types, props) // Multiple categories
F.multiReference(types, props) // Multiple references
F.blockContent(props) // Rich text content
F.message(text, props) // Display-only message
F.formField(props) // Form field configuration

Radio & Dropdown:

F.radio(['small', 'medium', 'large'], {name: 'size'})
F.dropdown(['draft', 'published', 'archived'], {name: 'status'})

// With custom labels:
F.radio(
  [
    {title: 'Small (SM)', value: 'small'},
    {title: 'Large (LG)', value: 'large'},
  ],
  {name: 'size'},
)

References:

// Single category
F.category('productCategory', {name: 'category'})

// Multiple categories
F.categories(['category', 'tag'], {name: 'categories'})

// Custom multi-reference
F.multiReference(['post', 'page'], {
  name: 'relatedContent',
  title: 'Related Content',
})

Advanced Composite Fields

Complex fields with conditional sub-fields:

Hero Field
F.hero(props)

// With custom configuration:
F.hero({
  name: 'hero',
  args: {
    label: false, // Disable label field
    heading: {name: 'heading', required: true},
    content: {name: 'content'},
    link: {
      name: 'cta',
      conditions: {none: []}, // Add 'none' option
    },
    media: {
      args: {caption: false}, // Disable caption in media
    },
    align: {initialValue: 'center'},
  },
})

The hero field includes:

  • label - Optional eyebrow label
  • heading - Main heading text
  • content - Rich text content (blockContentSimple)
  • link - Call-to-action link
  • media - Image or video
  • align - Content alignment (left/center/right)
Link Field
F.link(props)

// Basic link with defaults:
F.link({name: 'cta'})

// Custom configuration:
F.link({
  name: 'mainLink',
  types: ['page', 'post', 'product'], // Document types for internal links
  conditions: {
    none: [], // Add 'none' option
    internal: [], // Internal page links (default)
    external: [], // External URLs (default)
    download: [], // File downloads (default)
    video: [], // Video links
  },
  args: {
    label: {name: 'label', required: true},
    linkStyle: false, // Disable style options
    target: true, // Show "open in new tab" option
  },
})

The link field includes conditional sub-fields based on link type:

  • Internal: page reference, hash anchor options
  • External: URL input, target (new tab) option
  • Download: file upload
  • Video: video file upload
  • Common: label, link style (text/button/ghost), size
Media Field
F.media(props)

// Basic media:
F.media({name: 'heroMedia'})

// Custom configuration:
F.media({
  name: 'featuredMedia',
  conditions: {
    none: [], // Add 'none' option
    image: [], // Image upload (default)
    video: [], // Video upload (default)
    lottie: [], // Lottie animation
  },
  args: {
    caption: {name: 'caption', hidden: ({parent}) => !parent?.image},
    ratio: {name: 'aspectRatio', initialValue: '16:9'},
  },
})

The media field includes:

  • Condition selector (image/video)
  • Conditional image or video fields
  • Optional caption
  • Optional aspect ratio
SEO Field
F.seo(props)

// Basic SEO field:
F.seo() // Uses 'seo' as name

// Custom name:
F.seo({name: 'pageSeo', title: 'Page SEO'})

Note: The SEO field currently expects a custom 'seo' type to be defined in your Sanity schema. It's an alias field that references your project's SEO object type.

Content Group

Returns an array of fields (not a single object field):

F.contentGroup(props)

// Basic usage:
const fields = F.contentGroup()

// Custom configuration:
const fields = F.contentGroup({
  label: {name: 'eyebrow'},
  heading: {name: 'title', required: true},
  content: false, // Disable content field
  link: {name: 'cta'},
})

// Use in document:
F.document({
  name: 'section',
  fields: [
    ...F.contentGroup(), // Spreads label, heading, content, link fields
    F.image({name: 'background'}),
  ],
})

Utility Functions

F.field(type, props) // Generic field creator
F.apply(props, fields) // Apply props to multiple fields

Example:

// Apply common props to multiple fields:
const requiredFields = F.apply({required: true, group: 'contact'}, [
  F.string({name: 'email'}),
  F.string({name: 'phone'}),
])

G - Groups

Groups organize fields into tabs in the Sanity Studio:

G.define(name, props) // Define a group
G.group(name, fields) // Assign fields to a group
G.content() // Predefined 'content' group
G.meta() // Predefined 'meta' group
G.options() // Predefined 'options' group

Example:

import {AiOutlineHome as icon} from 'react-icons/ai'
import {F, G} from '@gearbox-built/sanity-schema-tool'

export const page = F.document({
  icon,
  name: 'page',
  // Define the tabs:
  groups: [G.define('content'), G.define('meta'), G.define('seo', {title: 'SEO'})],
  // Assign fields to tabs:
  fields: [
    ...G.group('content', [F.title(), F.field('hero'), F.field('components')]),
    ...G.group('meta', [F.slug(), F.field('path'), F.publishedDate()]),
    ...G.group('seo', [F.seo()]),
  ],
})

Predefined Groups:

// These are pre-configured groups you can use:
groups: [G.content(), G.meta(), G.options()]

// Equivalent to:
groups: [
  {name: 'content', title: 'Content', default: true},
  {name: 'meta', title: 'Meta'},
  {name: 'options', title: 'Options'},
]

P - Preview

Preview configuration helpers:

P.preview(props) // Generic preview config
P.titleImage(props) // Title + image preview (default)
P.text(title) // Text-only preview
P.richText(blocks) // Convert rich text to preview
P.link(props) // Link preview helper
P.contentGroup(props) // Content group preview

Example:

export const article = F.document({
  name: 'article',
  fields: [F.title(), F.image({name: 'cover'}), F.publishedDate()],

  // Simple preview:
  preview: P.titleImage(), // Uses 'title' and 'image'

  // Custom preview:
  preview: P.preview({
    title: 'title',
    media: 'cover',
    subtitle: 'publishedDate',
  }),

  // Custom prepare function:
  preview: {
    select: {
      title: 'title',
      date: 'publishedDate',
    },
    prepare: ({title, date}) => ({
      title: title || 'Untitled',
      subtitle: date ? new Date(date).toLocaleDateString() : 'No date',
    }),
  },
})

Text-only preview:

export const category = F.document({
  name: 'category',
  fields: [F.title()],
  preview: P.text('title'),
})

V - Validation

Validation helpers:

V.required // Required field validation
V.validation(customValidator) // Custom validation function

Example:

import {V} from '@gearbox-built/sanity-schema-tool'

F.string({
  name: 'email',
  validation: V.required, // Note: this is a function, not a call
})

// Custom validation:
F.string({
  name: 'username',
  validation: V.validation((value) => {
    if (!value) return true
    return value.length >= 3 || 'Username must be at least 3 characters'
  }),
})

// Multiple validations:
F.string({
  name: 'password',
  validation: (Rule) => Rule.required().min(8).max(100),
})

FS - Fieldsets

Fieldset helpers for grouping related fields:

FS.define(name, props) // Define a fieldset
FS.fieldset(name, fields) // Assign fields to a fieldset
FS.seo() // Predefined SEO fieldset

Example:

export const product = F.document({
  name: 'product',
  fieldsets: [
    FS.define('pricing', {
      title: 'Pricing',
      options: {collapsible: true, collapsed: false},
    }),
  ],
  fields: [
    F.title(),
    ...FS.fieldset('pricing', [
      F.number({name: 'price'}),
      F.number({name: 'salePrice'}),
      F.boolean({name: 'onSale'}),
    ]),
  ],
})

Configuration

Use withConfig() to set global defaults for field types across your project.

Basic Configuration

Create a configuration file:

// lib/schemaTool.ts
import {F as _F, withConfig} from '@gearbox-built/sanity-schema-tool'

export const {F, FS, G, P, V} = withConfig({
  // Set defaults for image fields:
  image: {
    fields: [_F.string({name: 'alt', title: 'Alt Text'}), _F.string({name: 'caption'})],
  },

  // Make slugs optional by default:
  slug: {
    required: false,
  },

  // Add default validation to strings:
  string: {
    validation: (Rule) => Rule.max(200),
  },

  // Set default for published dates:
  publishedDate: {
    options: {
      dateFormat: 'YYYY-MM-DD',
    },
  },
})

// Re-export types:
export type * from '@gearbox-built/sanity-schema-tool'

Then use your configured schema tool:

// schemas/product.ts
import {F} from '@/lib/schemaTool'

export const product = F.document({
  name: 'product',
  fields: [
    F.title(),
    F.slug(), // Automatically optional (from config)
    F.image({name: 'photo'}), // Automatically includes alt and caption
  ],
})

All Configurable Fields

You can configure defaults for these field types:

withConfig({
  // Core types:
  array: {},
  block: {},
  blockContent: {},
  boolean: {},
  date: {},
  datetime: {},
  document: {},
  email: {},
  file: {},
  geopoint: {},
  image: {},
  number: {},
  object: {},
  reference: {},
  slug: {},
  string: {},
  text: {},
  url: {},

  // Custom types:
  categories: {},
  category: {},
  checkbox: {},
  contentGroup: {},
  excerpt: {},
  formField: {},
  hero: {},
  link: {},
  media: {},
  message: {},
  multiReference: {},
  publishedDate: {},
  radio: {},
  seo: {},
  title: {},
})

Extension

Extend the schema tool with custom field types using the second parameter of withConfig().

Adding Custom Fields

// lib/schemaTool.ts
import {
  F as _F,
  withConfig,
  type StringField,
  type FieldReturn,
  type RadioList,
} from '@gearbox-built/sanity-schema-tool'

// Define custom types:
export const customTypes = {
  F: {
    // Custom dropdown field:
    dropdown: (list: RadioList, props?: StringField): FieldReturn =>
      _F.string({
        ...props,
        options: {
          list: list.map((item) =>
            typeof item === 'string' ? {title: item, value: item.toLowerCase()} : item,
          ),
          layout: 'dropdown',
        },
      }),

    // Custom phone field:
    phone: (props?: StringField): FieldReturn =>
      _F.string({
        ...props,
        name: props?.name || 'phone',
        validation: (Rule) =>
          Rule.regex(
            /^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,6}$/,
            'Please enter a valid phone number',
          ),
      }),

    // Custom color picker:
    color: (props?: StringField): FieldReturn =>
      _F.string({
        ...props,
        name: props?.name || 'color',
        options: {
          list: [
            {title: 'Red', value: '#ff0000'},
            {title: 'Blue', value: '#0000ff'},
            {title: 'Green', value: '#00ff00'},
          ],
        },
      }),
  },
}

// Export configured schema tool with custom types:
export const {F, FS, G, P, V} = withConfig(
  {
    // Your config here
    slug: {required: false},
  },
  customTypes,
)

export type * from '@gearbox-built/sanity-schema-tool'

Using Custom Types

import {F} from '@/lib/schemaTool'

export const contact = F.document({
  name: 'contact',
  fields: [
    F.string({name: 'name'}),
    F.phone(), // Custom phone field
    F.dropdown(['Mr', 'Mrs', 'Ms', 'Dr'], {name: 'title'}), // Custom dropdown
    F.color({name: 'favoriteColor'}), // Custom color picker
  ],
})

Adding Custom Validators

const customTypes = {
  V: {
    email: () => (Rule: any) => Rule.regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Invalid email format'),

    url: () => (Rule: any) => Rule.uri({scheme: ['http', 'https']}),
  },
}

// Usage:
F.string({
  name: 'email',
  validation: V.email(),
})

Adding Custom Previews

const customTypes = {
  P: {
    articlePreview: () => ({
      select: {
        title: 'title',
        author: 'author.name',
        date: 'publishedDate',
        image: 'coverImage',
      },
      prepare: ({title, author, date, image}) => ({
        title: title || 'Untitled',
        subtitle: `${author} - ${new Date(date).toLocaleDateString()}`,
        media: image,
      }),
    }),
  },
}

Complete Example

Here's a complete example showing various features:

// lib/schemaTool.ts
import {F as _F, withConfig} from '@gearbox-built/sanity-schema-tool'

const customTypes = {
  F: {
    dropdown: (list, props) => _F.string({...props, options: {list, layout: 'dropdown'}}),
  },
}

export const {F, FS, G, P, V} = withConfig(
  {
    image: {
      fields: [_F.string({name: 'alt', title: 'Alt Text'})],
    },
    slug: {required: false},
  },
  customTypes,
)

export type * from '@gearbox-built/sanity-schema-tool'
// schemas/blogPost.ts
import {AiOutlineFileText as icon} from 'react-icons/ai'
import {F, G, P} from '@/lib/schemaTool'

export const blogPost = F.document({
  icon,
  name: 'blogPost',
  title: 'Blog Post',
  groups: [G.define('content'), G.define('meta'), G.define('seo', {title: 'SEO'})],
  fields: [
    // Content group:
    ...G.group('content', [
      F.title({required: true}),
      F.excerpt(),
      F.image({name: 'coverImage', title: 'Cover Image'}),
      F.hero({
        name: 'hero',
        args: {
          label: false,
          media: {
            conditions: {
              none: [],
              image: [],
              video: [],
            },
          },
        },
      }),
      F.blockContent({name: 'body'}),
      F.array({
        name: 'tags',
        of: [F.string()],
        options: {layout: 'tags'},
      }),
    ]),

    // Meta group:
    ...G.group('meta', [
      F.slug(),
      F.reference(['author'], {name: 'author', title: 'Author'}),
      F.categories(['category', 'topic'], {name: 'categories'}),
      F.publishedDate(),
      F.dropdown(['draft', 'review', 'published'], {
        name: 'status',
        initialValue: 'draft',
      }),
    ]),

    // SEO group:
    ...G.group('seo', [F.seo()]),
  ],
  preview: P.preview({
    title: 'title',
    subtitle: 'excerpt',
    media: 'coverImage',
  }),
  orderings: [
    {
      title: 'Published Date, New',
      name: 'publishedDateDesc',
      by: [{field: 'publishedDate', direction: 'desc'}],
    },
  ],
})

TypeScript

This package is fully typed. Import types as needed:

import type {
  FieldReturn,
  FieldProps,
  StringField,
  ImageField,
  ArrayField,
  ConfigType,
  CustomTypes,
} from '@gearbox-built/sanity-schema-tool'

License

MIT © Gearbox Development Inc.

Develop & Test

This plugin uses @sanity/plugin-kit with default configuration for build & watch scripts.

See Testing a plugin in Sanity Studio on how to run this plugin with hotreload in the studio.

Build Commands

# Build the package
yarn build

# Type check
yarn ts

# Watch mode for development
yarn watch

# Link and watch for local testing
yarn link-watch

# Linting
yarn lint

# Format code
yarn format

# Run tests
yarn test

# Test with coverage
yarn test:coverage