Issue with required validation field in Sanity schema extraction and type generation.

11 replies
Last updated: Apr 2, 2024
User
user Y
i'll move this convo here: for reference: https://sanity-io-land.slack.com/archives/C9Z7RC3V1/p1712014795716969
in short though it looks like when i extract the schema it looks like it might be ignoring the required validation field? or im misunderstanding the reason i get
pageData: _PageData_ | _null_;
on a field that is required
AI Update

Looking at your issue, this is a common point of confusion with Sanity TypeGen. When you mark a field as required in your schema using validation: rule => rule.required(), TypeGen will still generate the field as optional (with | null) by default.

This happens because TypeGen is designed to be conservative - even if a field is required for published documents, draft documents can be in an "invalid" state while being edited. If you're querying drafts or using perspectives that include unpublished content, those fields might legitimately be undefined or null.

The Solution

To make TypeGen respect your required validation rules and generate non-optional types, you need to use the --enforce-required-fields flag when extracting your schema:

npx sanity schema extract --enforce-required-fields
npx sanity typegen generate

This will generate types where fields marked with .required() validation are not optional (no ? or | null).

Important Caveat

However, be aware that if you're working with draft documents or using the previewDrafts perspective, values can still be undefined or null even for "required" fields, since validation is only enforced on published documents. So you may need runtime checks even with this flag enabled.

How This Relates to GROQ Queries

When you write GROQ queries and use TypeGen to generate types for query results, the same principle applies. If you project a field in your GROQ query:

const query = defineQuery(`*[_type == "page"]{ pageData }`)

TypeGen will look at your schema definition for pageData. If it's marked as required and you used --enforce-required-fields, it won't be nullable in the generated query result type. Without that flag, it will be pageData: PageData | null even if the schema says it's required.

The key understanding is that TypeGen generates types based on what could exist in your Content Lake, not just what's valid for published documents. This is documented in the Sanity TypeGen documentation under the "Required field validation and non-optional fields" section.

user V
What's the output of
sanity schema extract --enforce-required-fields
, just tested it locally and it worked 🤔
so here is my groq

    const getPageData = groq`*[_type in ['product', 'page', 'blogPost'] && pageData.slug.current == $slug][0]{
    _type == 'product' => {
      ...,
      pageData
    },
    _type == 'page' => {
      ...,
      pageData,
      
    },
    _type == 'blogPost' => {
      ...,
      pageData
    }
  }`;

    const pageData = await client.fetch<GetPageDataResult>(getPageData, {
      slug,
    });
and here is the full type


export type GetPageDataResult = {
  _id: string;
  _type: "blogPost";
  _createdAt: string;
  _updatedAt: string;
  _rev: string;
  pageData: PageData | null;
  prismicData?: PrismicData;
  author?: {
    _ref: string;
    _type: "reference";
    _weak?: boolean;
    [internalGroqTypeReferenceTo]?: "blogAuthor";
  };
  category?: {
    _ref: string;
    _type: "reference";
    _weak?: boolean;
    [internalGroqTypeReferenceTo]?: "blogCategory";
  };
  publishedAt?: string;
  content?: RichText;
} | {
  _id: string;
  _type: "page";
  _createdAt: string;
  _updatedAt: string;
  _rev: string;
  pageData: PageData | null;
  prismicData?: PrismicData;
  content?: Array<({
    _key: string;
  } & CodeComponent) | ({
    _key: string;
  } & CollectionComponent) | ({
    _key: string;
  } & CtaImageComponent) | ({
    _key: string;
  } & CtaTemplateComponent) | ({
    _key: string;
  } & CtaVideoComponent) | ({
    _key: string;
  } & FaqComponent) | ({
    _key: string;
  } & HeaderComponent) | ({
    _key: string;
  } & HeroComponent) | ({
    _key: string;
  } & ImageGalleryComponent) | ({
    _key: string;
  } & LinkComponent) | ({
    _key: string;
  } & ListsComponent) | ({
    _key: string;
  } & ProductHeroComponent) | ({
    _key: string;
  } & ReviewsComponent) | ({
    _key: string;
  } & RichTextComponent) | ({
    _key: string;
  } & RootCollectionComponent) | ({
    _key: string;
  } & YotpoImageGalleryComponent) | ({
    _key: string;
  } & YoutubeVideoComponent)>;
} | {
  _id: string;
  _type: "product";
  _createdAt: string;
  _updatedAt: string;
  _rev: string;
  pageData: PageData | null;
  prismicData?: PrismicData;
  linkedProduct?: LinkedProduct;
  description?: Array<{
    children?: Array<{
      marks?: Array<string>;
      text?: string;
      _type: "span";
      _key: string;
    }>;
    style?: "blockquote" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "normal";
    listItem?: "bullet" | "number";
    markDefs?: Array<{
      href?: string;
      _type: "link";
      _key: string;
    }>;
    level?: number;
    _type: "block";
    _key: string;
  }>;
  details?: Array<{
    children?: Array<{
      marks?: Array<string>;
      text?: string;
      _type: "span";
      _key: string;
    }>;
    style?: "blockquote" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "normal";
    listItem?: "bullet" | "number";
    markDefs?: Array<{
      href?: string;
      _type: "link";
      _key: string;
    }>;
    level?: number;
    _type: "block";
    _key: string;
  }>;
  templateId?: string;
  hasFrontImage?: boolean;
  hasBackImage?: boolean;
  hasSideImage?: boolean;
  productImages?: Array<{
    asset?: {
      _ref: string;
      _type: "reference";
      _weak?: boolean;
      [internalGroqTypeReferenceTo]?: "sanity.imageAsset";
    };
    hotspot?: SanityImageHotspot;
    crop?: SanityImageCrop;
    _type: "image";
    _key: string;
  }>;
  productVariants?: Array<{
    _ref: string;
    _type: "reference";
    _weak?: boolean;
    _key: string;
    [internalGroqTypeReferenceTo]?: "productVariant";
  }>;
} | null;
oh, wait. Mixing bugs/discussions here!
You need to run
sanity schema extract
with the
--enforce-required-fields
flag, then you can run
sanity typegen generate
ah let me give that a go, might need to clear that up in the docs, unless i read them wrong (which is a possibility lol)
oh, the docs are wrong! I'll get that sorted
yup, that fixed it and makes more sense now
i was wondering if i misunderstood how the schema was working lol
seemed weird that field was set as optional
 _rev: string;
  pageData: PageData;
  prismicData?: PrismicData;
ok we are cooking with fire, thanks! this is so much nicer than manually creating this with zod lol
Great to hear 😄

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?