Shopify + Sanity: Read about the investment and partnership –>

sanity-typed-schema-builder

By Shayan Hashimoto

Build Sanity schemas declaratively and get typescript types of schema values for free!

sanity-typed-schema-builder

Build Sanity schemas declaratively and get typescript types of schema values for free!

  • Typescript types for Sanity Values!
  • Get mock values for tests!
  • Get zod schemas for parsing values (most notably, date values into javascript Date)

Install

npm install sanity-typed-schema-builder

Usage

import { s } from "sanity-typed-schema-builder";

// Declare types in a familiar way
const fooType = s.document({
  name: "foo",
  fields: [
    {
      name: "foo",
      type: s.string(),
    },
    {
      name: "bar",
      type: s.array({ of: [s.boolean(), s.number({ readOnly: true })] }),
    },
    {
      name: "hello",
      optional: true,
      type: s.object({
        fields: [
          {
            name: "world",
            type: s.number(),
          },
        ],
      }),
    },
  ],
});

// Typescript Types!
type FooType = s.infer<typeof fooType>;

/**
 *  s.infer<typeof fooType> = {
 *    _createdAt: string;
 *    _id: string;
 *    _rev: string;
 *    _type: "foo";
 *    _updatedAt: string;
 *    bar: (boolean | number)[];
 *    foo: string;
 *    hello?: {
 *      world: number;
 *    };
 *  };
 **/

// Use @faker-js/faker to create mocks for tests!
const fooMock = fooType.mock();

// Use zod to parse untyped values (and transform values, note _createdAt & _updatedAt specifically)
const parsedFoo: s.output<typeof fooType> = fooType.parse(someInput);

/**
 *  s.output<typeof fooType> = {
 *    _createdAt: Date;
 *    _id: string;
 *    _rev: string;
 *    _type: "foo";
 *    _updatedAt: Date;
 *    bar: (boolean | number)[];
 *    foo: string;
 *    hello?: {
 *      world: number;
 *    };
 *  };
 **/

// Use schemas in Sanity
createSchema({
  name: "default",
  types: [fooType.schema()],
});

Notable Differences:

For all types, the properties provided are the same as the sanity schema types except for these specific differences:

type is removed

type is defined via the typed methods, so they aren't required directly

name, title, description, fieldset, & group are defined in fields

For all types except document and named objects, type, name, title, description, fieldset, & group are not defined in the type but in the fields. These aren't relevant specifically to the type, but rather in their relationship to the parent object or document:

s.object({
  fields: [
    {
      // All of these are defined here in the field
      name: "foo",
      title: "Foo",
      description: "This is foo",
      // Not inside of the type itself
      type: s.number({ hidden: true }),
    },
  ],
});

Rule.required() replaced with optional boolean in fields

For types with fields (document, object, objectNamed, file, & image), the fields can be marked as optional. This will both not set the validation: (Rule) => Rule.required() and type the inferred type.

s.object({
  fields: [
    {
      name: "foo",
      type: s.number(),
    },
    {
      name: "bar",
      optional: true,
      type: s.number(),
    },
  ],
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = {
 *   foo: number;
 *   bar?: number;
 * }
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = {
 *   foo: number;
 *   bar?: number;
 * }
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "object",
 *   fields: [
 *     {
 *       name: "foo",
 *       type: "number",
 *       validation: (Rule) => Rule.validation(),
 *     },
 *     {
 *       name: "bar",
 *       type: "number",
 *     },
 *   ],
 * };
 */

Rule.custom is typed

Custom validation is typed with a deep partial version of the value. We set it as a deep partial because values are not necessarily valid.

s.object({
  fields: [
    {
      name: "foo",
      type: s.number({ hidden: true }),
      validation: (Rule) =>
        Rule.custom((value) => {
          // value: { foo?: number }
        }),
    },
  ],
});

preview is typed

TODO

Custom mock

Our mocks are using Faker under the hood and give default mocks. These mocks are configurable.

const type = s.string({
  mock: (faker: Faker, path: string) => faker.name.firstName(),
});

const mock = type.mock(); // "Katelynn"

Custom zod

Our parsing is using Zod under the hood and has default parsing. These zod schemas are configurable.

const type = s.string({
  zod: (zod) => zod.transform((value) => value.length),
});

type Value = s.infer<typeof type>; // This is still a string.

const parsedValue: s.output<typeof type> = type.parse("hello"); // This is a number, specifically `5` in this case

Types

Array

const type = array({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/array-type

  // `of` uses other types directly:
  of: [s.boolean(), s.datetime()],

  // length?: number    sets both zod and validation: (Rule) => Rule.length(length)
  // max?: number       sets both zod and validation: (Rule) => Rule.max(max)
  // min?: number       sets both zod and validation: (Rule) => Rule.min(min)
  // nonempty?: boolean sets both zod and validation: (Rule) => Rule.min(1)
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = (boolean | string)[];
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * Notice the recursive transform, particularly with `datetime` becoming a `Date`
 *
 * s.output<typeof type> = (boolean | Date)[];
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "array",
 *   of: [{ type: "boolean" }, { type: "datetime" }],
 *   ...
 * };
 */

Block

const type = block({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/block-type
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = PortableTextBlock;
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = PortableTextBlock;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "block",
 *   ...
 * };
 */

Boolean

const type = boolean({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/boolean-type
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = boolean;
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = boolean;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "boolean",
 *   ...
 * };
 */

Date

const type = date({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/date-type
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = string;
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = string;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "date",
 *   ...
 * };
 */

Datetime

const type = datetime({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/datetime-type
  // max?: string sets both zod and validation: (Rule) => Rule.max(max)
  // min?: string sets both zod and validation: (Rule) => Rule.min(min)
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = string;
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = Date;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "datetime",
 *   ...
 * };
 */

Document

const type = document({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/document-type
  name: "foo",
  fields: [
    {
      name: "foo",
      type: s.number(),
    },
    {
      name: "bar",
      optional: true,
      type: s.number(),
    },
  ],
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = {
 *   _createdAt: string;
 *   _id: string;
 *   _rev: string;
 *   _type: "foo";
 *   _updatedAt: string;
 *   foo: number;
 *   bar?: number;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = {
 *   _createdAt: Date;
 *   _id: string;
 *   _rev: string;
 *   _type: "foo";
 *   _updatedAt: Date;
 *   foo: number;
 *   bar?: number;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   name: "foo",
 *   type: "document",
 *   fields: [...],
 *   ...
 * };
 */

File

const type = file({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/file-type
  fields: [
    {
      name: "foo",
      type: s.number(),
    },
    {
      name: "bar",
      optional: true,
      type: s.number(),
    },
  ],
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = {
 *   _type: "file";
 *   asset: {
 *     _type: "reference";
 *     _ref: string;
 *   };
 *   foo: number;
 *   bar?: number;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = {
 *   _type: "file";
 *   asset: {
 *     _type: "reference";
 *     _ref: string;
 *   };
 *   foo: number;
 *   bar?: number;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   name: "foo",
 *   type: "file",
 *   fields: [...],
 *   ...
 * };
 */

Geopoint

const type = geopoint({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/geopoint-type
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = {
 *   _type: "geopoint";
 *   alt: number;
 *   lat: number;
 *   lng: number;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = {
 *   _type: "geopoint";
 *   alt: number;
 *   lat: number;
 *   lng: number;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "geopoint",
 *   ...
 * };
 */

Image

const type = image({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/image-type
  // hotspot?: true adds the `crop` & `hotspot` to the value types, mocks, and parsing
  fields: [
    {
      name: "foo",
      type: s.number(),
    },
    {
      name: "bar",
      optional: true,
      type: s.number(),
    },
  ],
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = {
 *   _type: "image";
 *   asset: {
 *     _type: "reference";
 *     _ref: string;
 *   };
 *   foo: number;
 *   bar?: number;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = {
 *   _type: "image";
 *   asset: {
 *     _type: "reference";
 *     _ref: string;
 *   };
 *   foo: number;
 *   bar?: number;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   name: "foo",
 *   type: "image",
 *   fields: [...],
 *   ...
 * };
 */

Number

const type = number({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/number-type
  // greaterThan?: number sets both zod and validation: (Rule) => Rule.greaterThan(greaterThan)
  // integer?: boolean    sets both zod and validation: (Rule) => Rule.integer()
  // lessThan?: number    sets both zod and validation: (Rule) => Rule.lessThan(lessThan)
  // max?: number         sets both zod and validation: (Rule) => Rule.max(max)
  // min?: number         sets both zod and validation: (Rule) => Rule.min(min)
  // negative?: boolean   sets both zod and validation: (Rule) => Rule.negative()
  // positive?: boolean   sets both zod and validation: (Rule) => Rule.positive()
  // precision?: number   sets both zod and validation: (Rule) => Rule.precision(precision)
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = number;
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = number;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "number",
 *   ...
 * };
 */

Object

const type = object({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/object-type
  fields: [
    {
      name: "foo",
      type: s.number(),
    },
    {
      name: "bar",
      optional: true,
      type: s.number(),
    },
  ],
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = {
 *   foo: number;
 *   bar?: number;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = {
 *   foo: number;
 *   bar?: number;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   name: "foo",
 *   type: "object",
 *   fields: [...],
 *   ...
 * };
 */

Object (Named)

This is separate from object because, when objects are named in sanity, there are significant differences:

  • The value has a _type field equal to the object's name.
  • They can be used directly in schemas (like any other schema).
  • They can also be registered as a top level object and simply referenced by type within another schema.
const type = objectNamed({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/object-type
  name: "aNamedObject",
  fields: [
    {
      name: "foo",
      type: s.number(),
    },
    {
      name: "bar",
      optional: true,
      type: s.number(),
    },
  ],
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = {
 *   _type: "aNamedObject";
 *   foo: number;
 *   bar?: number;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = {
 *   _type: "aNamedObject";
 *   foo: number;
 *   bar?: number;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   name: "foo",
 *   type: "object",
 *   fields: [...],
 *   ...
 * };
 */
// Use `.ref()` to reference it in another schema.
const someOtherType = array({ of: [type.ref()] });

// The reference value is used directly.
type SomeOtherValue = s.infer<typeof someOtherType>;

/**
 * type SomeOtherValue = [{
 *   _type: "aNamedObject";
 *   foo: number;
 *   bar?: number;
 * }];
 */

// The schema is made within the referencing schema
const someOtherTypeSchema = someOtherType.schema();

/**
 * const someOtherTypeSchema = {
 *   type: "array",
 *   of: [{ type: "" }],
 *   ...
 * };
 */

createSchema({
  name: "default",
  types: [type.schema(), someOtherType.schema()],
});

Reference

const type = reference({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/reference-type
  to: [someDocumentType, someOtherDocumentType],
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = {
 *   _ref: string;
 *   _type: "reference";
 *   _weak?: boolean;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = {
 *   _ref: string;
 *   _type: "reference";
 *   _weak?: boolean;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "reference",
 *   to: [...],
 *   ...
 * };
 */

Slug

const type = slug({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/slug-type
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = {
 *   _type: "slug";
 *   current: string;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = string;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "slug",
 *   ...
 * };
 */

String

const type = string({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/string-type
  // length?: number sets both zod and validation: (Rule) => Rule.length(length)
  // max?: number    sets both zod and validation: (Rule) => Rule.max(max)
  // min?: number    sets both zod and validation: (Rule) => Rule.min(min)
  // regex?: Regex   sets both zod and validation: (Rule) => Rule.regex(regex)
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = string;
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = string;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "string",
 *   ...
 * };
 */

Text

const type = text({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/text-type
  // length?: number sets both zod and validation: (Rule) => Rule.length(length)
  // max?: number    sets both zod and validation: (Rule) => Rule.max(max)
  // min?: number    sets both zod and validation: (Rule) => Rule.min(min)
  // regex?: Regex   sets both zod and validation: (Rule) => Rule.regex(regex)
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = string;
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = string;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "text",
 *   ...
 * };
 */

URL

const type = url({
  // Any of the same properties as a normal sanity schema
  // https://www.sanity.io/docs/url-type
});

type Value = s.infer<typeof type>;

/**
 * s.infer<typeof type> = string;
 */

const parsedValue: s.output<typeof type> = type.parse(someInput);

/**
 * s.output<typeof type> = string;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "url",
 *   ...
 * };
 */

Install command

npm install --save sanity-typed-schema-builder

Contributor

Categorized in