Skip to content

Schema types ​

All creators are named exports of @sanity-labs/typed-schema.

CreatorInferred type
string()string
number() and its aliasesnumber
boolean()boolean
literal()the literal
enum()a union of the values
date()string
dateTime()string
object()an object with the declared fields
document()an object with the declared fields and the system fields
array()an array, with _key on object elements
union()a union of the members' types
reference(){_type: 'reference'; _ref: string; _weak?: boolean}
image() and file(){_type: 'image' | 'file'; asset: reference} plus your fields
imageAsset(), fileAsset()the sanity.imageAsset and sanity.fileAsset documents
geopoint(){_type: 'geopoint'; lat: number; lng: number; alt?: number}
block()one Portable Text block
portableText()an array of blocks and the objects between them
optional()the inner type, or undefined
lazy()the type of the schema the getter returns
never()never

Common to every schema ​

A schema is a plain object with these properties:

PropertyNotes
typeNameThe kind of schema: 'string', 'object', 'objectArray', 'union', ...
annotationsThe annotations given as the creator's last argument, if any
~standardThe Standard Schema properties. Not enumerable

Every creator except optional() and never() takes annotations as its last argument. number() and its aliases take them in the same object as the constraints.

parse leaves fields the schema does not declare out of the parsed value, and does not report them as issues. An optional() field set to null fails to parse, since null is not undefined.

string() ​

ts
string(annotations?): StringSchema
  • Inferred type: string
  • Parsing: Any string. Anything else is INVALID_TYPE

number() ​

ts
number(options?: NumberConstraints & SchemaAnnotations): NumberSchema
safeint(annotations?)
int32(annotations?)
uint32(annotations?)
float32(annotations?)
float64(annotations?)
  • Inferred type: number
  • Parsing: A finite number within the constraints. NaN, Infinity and anything that is not a number are INVALID_TYPE
  • Error codes: INVALID_TYPE, NUM_TOO_SMALL, NUM_TOO_BIG, NUM_NOT_MULTIPLE_OF
  • Properties: constraints, which holds the options other than annotations, or is undefined if there are none
OptionNotes
minSmallest allowed value, inclusive
maxLargest allowed value, inclusive
gtSmallest allowed value, exclusive
ltLargest allowed value, exclusive
stepThe value must be a multiple of step. step: 1 allows whole numbers

Use min and max, or gt and lt. Mixing the two pairs is a type error.

The aliases are number() with a preset range:

AliasRangeWhole numbers
safeint()Number.MIN_SAFE_INTEGER to MAX_SAFE_INTEGERyes
int32()-231 to 231-1yes
uint32()0 to 232-1yes
float32()±3.4028234663852886e38no
float64()±Number.MAX_VALUEno
ts
import * as 
s
from '@sanity-labs/typed-schema'
const
Rating
=
s
.
number
({
min
: 1,
max
: 5,
step
: 1,
title
: 'Rating'})
// {ok: false, error}, with the issue code NUM_TOO_BIG
s
.
parse
(
Rating
, 6)

boolean() ​

ts
boolean(annotations?): BooleanSchema
  • Inferred type: boolean
  • Parsing: true or false. Anything else is INVALID_TYPE

literal() ​

ts
literal(value: string | number | boolean, annotations?): LiteralSchema
  • Inferred type: The literal type of value, such as 'post' or 42
  • Parsing: Only value itself (===). Anything else is INVALID_TYPE
  • Properties: value

The _type field of a document or typed object is a literal(). A union of literals is a closed set of values, like enum().

enum() ​

ts
enum(values: string[] | {[name: string]: string | number}, annotations?): EnumSchema
  • Inferred type: A union of the values
  • Parsing: One of the values. Anything else is INVALID_ENUM_VALUE
  • Properties: enum, the values as an object. An array of values is stored as an object that maps each value to itself

With an object, the values are the object's values, not its keys. An enum() can't be an array element or a union member; use a union of literals there.

enum is a reserved word in JavaScript, so a named import has to rename it: import {enum as enumOf} from '@sanity-labs/typed-schema'. With a namespace import, s.enum() works as is. enums() is a deprecated alias of enum().

ts
import * as 
s
from '@sanity-labs/typed-schema'
const
Status
=
s
.
enum
(['draft', 'review', 'published'])
type Status =
s
.
Infer
<typeof
Status
>
type Status = "draft" | "review" | "published"
const
Priority
=
s
.
enum
({
Low
: 1,
High
: 2})
type Priority =
s
.
Infer
<typeof
Priority
>
type Priority = 1 | 2

date() ​

ts
date(annotations?): DateSchema
  • Inferred type: string
  • Parsing: A calendar date written YYYY-MM-DD. 2024-02-30 and 2024-2-3 are INVALID_TYPE

dateTime() ​

ts
dateTime(annotations?): DateTimeSchema
  • Inferred type: string
  • Parsing: An ISO 8601 date and time with a time zone, YYYY-MM-DDTHH:mm:ss[.sss](Z|±HH:mm). Without a time zone it is INVALID_TYPE

object() ​

ts
object(shape: {[field: string]: Schema}, annotations?): ObjectSchema
  • Inferred type: An object with a property per field. optional() fields are optional properties
  • Parsing: A plain object. parse checks each field with its schema, and adds the field name to the path of each issue it finds. Anything that is not an object is INVALID_TYPE
  • Properties: shape, the fields

A field declared with a getter is stored as a lazy() schema, so a field can refer to its own schema. See Recursive schemas.

An object with a _type field holding a string literal() is a typed object. An object must be typed to be a member of a union or to be used in Portable Text.

Type errors:

  • A field name that does not start with a letter A-Z, or that has characters other than letters A-Z, digits and _. The one exception is _type.
  • A field value that is not a schema.
ts
import * as 
s
from '@sanity-labs/typed-schema'
const
Link
=
s
.
object
({
_type
:
s
.
literal
('link'),
href
:
s
.
string
(),
label
:
s
.
optional
(
s
.
string
()),
}) type Link =
s
.
Infer
<typeof
Link
>
type Link = {
    _type: "link";
    href: string;
    label?: string | undefined;
}
const
Invalid
=
s
.
object
({
_internal
:
s
.
string
()})

document() ​

ts
document(shape: {_type: LiteralSchema<string>; [field: string]: Schema}, annotations?): DocumentSchema
  • Inferred type: The fields, plus the system fields _id, _rev, _createdAt and _updatedAt, all optional strings
  • Parsing: As for object(). _type must equal the literal
  • Properties: shape, the fields, including the system fields

The system fields are optional because a document you create has none of them until the Content Lake commits it. committed() makes them required. To require _id, declare it in the shape with string() or a string literal().

Type errors, in addition to those of object():

  • No _type field, or a _type that is not a string literal().
  • An _id field that is not string() or a string literal().
ts
import * as 
s
from '@sanity-labs/typed-schema'
const
Recipe
=
s
.
document
({
_type
:
s
.
literal
('recipe'),
title
:
s
.
string
(),
}) type Recipe =
s
.
Infer
<typeof
Recipe
>
type Recipe = {
    _type: "recipe";
    _id?: string | undefined;
    _createdAt?: string | undefined;
    _updatedAt?: string | undefined;
    _rev?: string | undefined;
    title: string;
}

See Declaring a document.

array() ​

ts
array(element: Schema, annotations?): ObjectArraySchema | PrimitiveArraySchema
objectArray(element, annotations?): ObjectArraySchema
primitiveArray(element, annotations?): PrimitiveArraySchema
  • Inferred type: An array of the element type. Object-like elements get a required _key: string
  • Parsing: An array. parse checks each element with element. Anything that is not an array is INVALID_TYPE
  • Error codes: INVALID_TYPE, ARRAY_ELEMENT_NOT_KEYED_OBJECT, and the element's codes
  • Properties: element, the element schema, with _key added for object-like elements

array() picks the kind of array from the element:

  • Objects, references, images, files, blocks and unions of these make an objectArray. Sanity requires a _key on every object in an array, so array() adds _key to the element schema. parse reports an element without a _key as ARRAY_ELEMENT_NOT_KEYED_OBJECT, with the element's index as the path. Issues inside an element have a {_key, index} path segment.
  • Strings, numbers, booleans, dates, literals and unions of these make a primitiveArray, without keys. Issues inside an element have the index as their path segment.

objectArray() and primitiveArray() each accept only their own kind of element. Use them in custom creators that need one kind.

On an object array, element.unkeyed is the schema given to array(), without _key. Each member of a union element also has an unkeyed. Use element and element.unkeyed to get the item types of an array declared inline, as a field of another schema:

ts
import * as 
s
from '@sanity-labs/typed-schema'
const
Recipe
=
s
.
document
({
_type
:
s
.
literal
('recipe'),
ingredients
:
s
.
array
(
s
.
object
({
name
:
s
.
string
(),
grams
:
s
.
number
()})),
}) type IngredientItem =
s
.
Infer
<typeof
Recipe
.
shape
.
ingredients
.
element
>
type IngredientItem = {
    _key: string;
    name: string;
    grams: number;
}
const
Ingredient
=
Recipe
.
shape
.
ingredients
.
element
.
unkeyed
type Ingredient =
s
.
Infer
<typeof
Ingredient
>
type Ingredient = {
    name: string;
    grams: number;
}

Type errors:

  • An array as the element, as in array(array(...)). Sanity does not allow arrays of arrays; wrap the inner array in an object.
  • A union that mixes primitives and objects.
  • An optional() element.
  • A union() of documents. For references to several document types, use array(reference(union([...]))).
ts
import * as 
s
from '@sanity-labs/typed-schema'
const
Dish
=
s
.
object
({
_type
:
s
.
literal
('dish'),
name
:
s
.
string
()})
const
Mixed
=
s
.
array
(
s
.
union
([
s
.
string
(),
Dish
]))
const
Menu
=
s
.
array
(
s.array(Dish)
)

See Arrays and _key.

union() ​

ts
union(members: Schema[], annotations?): ObjectUnionSchema | DocumentUnionSchema | PrimitiveUnionSchema
  • Inferred type: A union of the members' types. Nested unions are flattened, and never() members left out
  • Properties: union, the members, without never()

All the members must be of one kind, which decides the kind of union:

MemberstypeNameParsing
Strings, numbers, booleans, dates, date-times and literalsprimitiveUnionThe first member that accepts the value. If none does, INVALID_PRIMITIVE_UNION followed by each member's issue
Typed objects, references, images, files and blocksunionThe member whose _type literal equals the value's _type. A value without a _type, or with one no member has, is INVALID_OBJECT_UNION
Documents, and lazy() documentsdocumentUnionAs for typed objects

A union of documents is the target of a reference to several document types. It can also parse a value that can be one of several documents, such as a query result: parse(union([Author, Book]), doc). It can't be an array element.

Every reference value has _type: 'reference', so a union cannot tell several references apart by _type. union() combines several reference() members into one reference to a union of their documents: union([reference(Author), reference(Book), Link]) is union([reference(union([Author, Book])), Link]). It throws if one of those references, or a nested union() that contains one, has annotations; annotate a single reference(union([...])) instead.

For the inferred type to match, union() needs to count the references. It can for members written inline, but not for a variable, which TypeScript types as an array of unknown length. Pass members that include a reference() inline, or declare the variable with as const:

ts
import * as 
s
from '@sanity-labs/typed-schema'
const
Author
=
s
.
document
({
_type
:
s
.
literal
('author'),
name
:
s
.
string
()})
const
Link
=
s
.
object
({
_type
:
s
.
literal
('link'),
href
:
s
.
string
()})
s
.
union
([
s
.
reference
(
Author
),
Link
])
const
members
= [
s
.
reference
(
Author
),
Link
]
s
.
union
(
members
)
const
tuple
= [
s
.
reference
(
Author
),
Link
] as
const
s
.
union
(
tuple
)

Type errors:

  • Members of different kinds, such as a string() and an object, or a document and an object.
  • An object member without a _type literal.
  • A lazy() member that is not a document.
  • A member typed as Schema<Value>. Type it as DocumentSchemaOf<Value> or ObjectSchemaOf<Value>.
  • References in a variable that is not as const, as above.
ts
import * as 
s
from '@sanity-labs/typed-schema'
const
Content
=
s
.
union
([
s
.
object
({
_type
:
s
.
literal
('quote'),
text
:
s
.
string
()}),
s
.
object
({
_type
:
s
.
literal
('link'),
href
:
s
.
string
()}),
]) type Content =
s
.
Infer
<typeof
Content
>
type Content = {
    _type: "quote";
    text: string;
} | {
    _type: "link";
    href: string;
}
const
Id
=
s
.
union
([
s
.
string
(),
s
.
number
()])
type Id =
s
.
Infer
<typeof
Id
>
type Id = string | number

reference() ​

ts
reference(target: DocumentSchema | DocumentUnionSchema | LazySchema, annotations?): ReferenceSchema
  • Inferred type: {_type: 'reference'; _ref: string; _weak?: boolean}
  • Parsing: As for object(), with the fields above. Other reference fields, such as _strengthenOnPublish, are left out
  • Properties: referenceType, the target, and shape, the reference fields

target is a document, a lazy() document, or a union() of documents. reference(union([Author, Book])) can point to either type, and is the same as union([reference(Author), reference(Book)]). For documents that reference each other, declare the field with a getter; see Recursive schemas.

The value parse returns has the target under a non-enumerable $refSchema key, which loadReference uses to parse the referenced document. Parsed<> includes it and Infer<> leaves it out.

Type errors:

  • A target that is not a document or a union of documents.

reference() also throws when it is created, for:

  • An empty union([]).
  • A target typed as Schema<Value> that is an object at runtime.
  • A union with two documents of the same _type.
ts
import * as 
s
from '@sanity-labs/typed-schema'
const
Author
=
s
.
document
({
_type
:
s
.
literal
('author'),
name
:
s
.
string
()})
const
Book
=
s
.
document
({
_type
:
s
.
literal
('book'),
title
:
s
.
string
()})
const
Review
=
s
.
document
({
_type
:
s
.
literal
('review'),
author
:
s
.
reference
(
Author
),
subject
:
s
.
reference
(
s
.
union
([
Author
,
Book
])),
}) type Review =
s
.
Infer
<typeof
Review
>
type Review = {
    _type: "review";
    _id?: string | undefined;
    _createdAt?: string | undefined;
    _updatedAt?: string | undefined;
    _rev?: string | undefined;
    author: {
        _type: "reference";
        _ref: string;
        _weak?: boolean | undefined;
    };
    subject: {
        _type: "reference";
        _ref: string;
        _weak?: boolean | undefined;
    };
}

See Loading documents and references.

image() ​

ts
image(shape?, annotations?): ImageSchema
  • Inferred type: {_type: 'image'; asset: reference to imageAsset} plus the fields in shape
  • Parsing: As for object(). asset is required. hotspot and crop are not declared yet, so they are left out of the parsed value
  • Properties: shape, with _type, asset and your fields

shape takes extra fields, such as alt, with the same rules as object().

ts
import * as 
s
from '@sanity-labs/typed-schema'
const
Photo
=
s
.
image
({
alt
:
s
.
optional
(
s
.
string
())})
type Photo =
s
.
Infer
<typeof
Photo
>
type Photo = {
    _type: "image";
    asset: {
        _type: "reference";
        _ref: string;
        _weak?: boolean | undefined;
    };
    alt?: string | undefined;
}

file() ​

ts
file(shape?, annotations?): FileSchema
  • Inferred type: {_type: 'file'; asset: reference to fileAsset} plus the fields in shape
  • Parsing: As for object(). asset is required
  • Properties: shape, with _type, asset and your fields

imageAsset() and fileAsset() ​

ts
imageAsset(annotations?): DocumentSchema<ImageAssetShape>
fileAsset(annotations?): DocumentSchema<FileAssetShape>

The sanity.imageAsset and sanity.fileAsset documents, which the asset reference of an image() or file() points to. Both have url, path, assetId, extension, mimeType, sha1hash and size. Their optional fields are originalFilename, uploadId, label, title, description, altText, creditLine and source.

An image asset also has metadata, with dimensions, hasAlpha and isOpaque. Its location, palette, lqip and blurHash are optional, since Sanity can't extract them from every image (an SVG has no palette, for example).

Use them to parse asset documents, or as reference targets.

geopoint() ​

ts
geopoint(annotations?)
  • Inferred type: {_type: 'geopoint'; lat: number; lng: number; alt?: number}
  • Parsing: As for object()

block() ​

ts
block(options: BlockOptions, annotations?): BlockSchema
  • Inferred type: One Portable Text block: {_type: 'block'; children; style?; listItem?; level?; markDefs?}
  • Parsing: As for object(), with the fields above
  • Properties: shape, with _type, style, listItem, level, children and markDefs
OptionTakesWhat it allows
styleA string literal() or a union of themBlock styles, such as normal and h2. Any string if left out
listA string literal() or a union of themList item types for listItem, such as bullet and number. Any string if left out
decoratorA string literal() or a union of themDecorators, such as strong and em. Spans don't check them: marks stays string[]
inlineA typed object, reference, image, file or a union of theseObjects that can sit among the spans in children, such as mentions
annotationA typed object, reference, image, file or a union of theseObjects in markDefs, such as links, which spans refer to by _key in marks
_typeA string literal()The block's type name, block by default

children holds spans, {_type: 'span'; _key: string; text: string; marks: string[]}, and the inline objects. marks holds both decorator names and the _keys of entries in markDefs, so it is a string[]. Pass {} for a block with no options.

Sanity Studio's own schema types allow only objects and references for inline and annotation.

Type errors:

  • An option not in the table, or a value of the wrong kind for an option.
ts
import * as 
s
from '@sanity-labs/typed-schema'
const
ArticleBlock
=
s
.
block
({
style
:
s
.
union
([
s
.
literal
('normal'),
s
.
literal
('h2')]),
list
:
s
.
literal
('bullet'),
decorator
:
s
.
union
([
s
.
literal
('strong'),
s
.
literal
('em')]),
annotation
:
s
.
object
({
_type
:
s
.
literal
('link'),
href
:
s
.
string
()}),
}) type ArticleBlock =
s
.
Infer
<typeof
ArticleBlock
>
type ArticleBlock = {
    _type: "block";
    children: {
        _type: "span";
        _key: string;
        marks: string[];
        text: string;
    }[];
    style?: "normal" | "h2" | undefined;
    listItem?: "bullet" | undefined;
    level?: number | undefined;
    markDefs?: {
        _type: "link";
        _key: string;
        href: string;
    }[] | undefined;
}

See Portable Text.

portableText() ​

ts
portableText({block, element}, annotations?): ObjectArraySchema
  • Inferred type: An array of blocks and element objects, each with _key
  • Parsing: As for array() of a union() of typed objects
  • Properties: As for array()
OptionTakes
blockA block() schema
elementThe objects allowed between blocks: a typed object, reference, image, file, a union of these, or never() for none

portableText({block, element}) is array(union([block, element])). With nothing between the blocks, array(block) is the same.

ts
import * as 
s
from '@sanity-labs/typed-schema'
const
Body
=
s
.
portableText
({
block
:
s
.
block
({
style
:
s
.
union
([
s
.
literal
('normal'),
s
.
literal
('h2')])}),
element
:
s
.
union
([
s
.
image
({
alt
:
s
.
string
()}),
s
.
object
({
_type
:
s
.
literal
('codeBlock'),
code
:
s
.
string
()}),
]), }) type Body =
s
.
Infer
<typeof
Body
>
type Body = ({
    _type: "block";
    _key: string;
    children: {
        _type: "span";
        _key: string;
        marks: string[];
        text: string;
    }[];
    style?: "normal" | "h2" | undefined;
    listItem?: string | undefined;
    level?: number | undefined;
    markDefs?: never[] | undefined;
} | {
    _type: "image";
    _key: string;
    alt: string;
    asset: {
        _type: "reference";
        _ref: string;
        _weak?: boolean | undefined;
    };
} | {
    _type: "codeBlock";
    _key: string;
    code: string;
})[]

optional() ​

ts
optional(schema: Schema): OptionalSchema
  • Inferred type: The type of schema, or undefined. In an object, the field is an optional property
  • Parsing: undefined or a missing field, or a value schema accepts. null is not accepted
  • Properties: type, the inner schema

optional() takes no annotations; annotate the inner schema. required() removes it. An array element can't be optional.

lazy() ​

ts
lazy(getter: () => Schema, annotations?): LazySchema
  • Inferred type: The type of the schema getter returns
  • Parsing: Calls getter the first time the schema is parsed, and parses with the schema it returns
  • Properties: get, the getter

A field declared with a getter is stored as lazy(). A recursive schema written with lazy() needs a type annotation and a getter does not, so prefer a getter. See Recursive schemas.

never() ​

ts
never(): NeverSchema
  • Inferred type: never
  • Parsing: Every value is INVALID_TYPE

union() leaves never() members out, so it works as a placeholder member, as in portableText({block, element: never()}). It takes no annotations.