Everything *[NYC] 2026: see what we announced. →
Skip to content

Auto Dereferencing

Projections resolver for auto dereferencing

By Artem Komarov


One of the page builder sections - section-text-media.ts

import { defineType, defineField } from '@sanity/types';

export default defineType(
  {
    name: 'sectionTextMedia',
    title: 'Text + Media',
    type: 'object',
    fields: [
      defineField({
        name: 'title',
        type: 'string',
      }),
      defineField({
        name: 'image',
        type: 'image',
      }),
      defineField({
        name: 'link',
        type: 'reference',
      }),
    ],
    preview: {
      select: {
        subtitle: 'title',
      },
      prepare: ({ subtitle }) => ({ title: 'Text + Media', subtitle }),
    },
  },
);

Group all page builder sections in one index file - sections.ts

import sectionTextMedia from '@/sanity/schemas/sections/section-text-media';
import sectionCta from '@/sanity/schemas/sections/sectionCta';

const sections = [sectionTextMedia, sectionCta];

export default sections;

Custom field for the page builder - page-builder.ts

import { defineType } from '@sanity/types';
import sections from '@/sanity/schemas/sections';

export default defineType(
  {
    name: 'pageBuilder',
    type: 'array',
    of: [...sections.map((section) => ({ type: section.name }))],
  }
);

A document with a title, slug, image, and page builder fields - page.ts

import { defineType, defineField } from '@sanity/types';

export default defineType(
  {
    name: 'page',
    title: 'Pages',
    type: 'document',
    fields: [
      defineField({
        name: 'title',
        type: 'string',
      }),
      defineField({
        name: 'slug',
        type: 'slug',
        options: {
          source: 'title',
        },
      }),
      defineField({
        name: 'image',
        type: 'image',
      }),
      defineField({
        name: 'sections',
        type: 'pageBuilder',
      }),
  }
)

Combine all documents in index file - documents.ts

import page from '@/sanity/schemas/documents/page';
import posts from '@/sanity/schemas/documents/posts';

const documents = [page, posts];

export default documents;

A function to parse all fields within given schema type - utils.ts

import documents from '@/sanity/schemas/documents';
import sections from '@/sanity/schemas/sections';

export const projectionsResolver = (type, toDeep = false) => {
  const schema = [...documents, ...sections].find((d) => d.name === type);

  let query = '...,';

  const getFieldQuery = (field) => {
    switch (field.type) {
      case 'slug':
        return `"${field.name}": ${field.name}.current,`;
      case 'pageBuilder':
        return `${field.name}[] {
          ${sections.map(
            (section) => `_type == "${section.name}" => {
              ${toDeep ? '...,' : projectionsResolver(section.name)}
            }`
          )}
          },`;
      case 'image':
        return `${field.name} { ..., asset-> },`;
      case 'object':
        return `${field.name} {
            ${field.fields.map((f) => getFieldQuery(f)).join('')}
        },`;
      case 'array':
        // in case array of references
        if (field.of[0].type === 'reference') {
          return `${field.name}[]-> {
            ${field.of[0].to.map(
              (to) => `_type=="${to.type}" => { ${toDeep ? '...,' : projectionsResolver(getDoc(to.type), true)} }`
            )}
          },`;
        }

        // in case array of objects
        if (field.of[0].type === 'object') {
          return `${field.name}[] {
            ...,
            ${
              field.of[0].name
                ? field.of.map(
                    // if object has name attribute filter by type
                    (of) => `_type == "${of.name}" => {
                      ${of.fields.map((f) => getFieldQuery(f)).join('')}
                    }`
                  )
                : // if object has no name reference all its types
                  field.of.map((of) => `${of.fields.map((f) => getFieldQuery(f)).join('')}`)
            }
          },`;
        }

        // in case array of images
        if (field.of[0].type === 'image') {
          return `${field.name}[] {
            ..., asset->
          },`;
        }

        return `${field.name}[],`;
      case 'reference':
        if (toDeep) {
          return `${field.name}->,`;
        }

        return `${field.name}-> {
              ${field.to.map((to) => `_type=="${to.type}" => { ${projectionsResolver(to.type, true)} }`)}
        },`;
      default:
        return `${field.name},`;
    }
  };

  if (schema.fields) {
    schema.fields.forEach((field) => {
      query += getFieldQuery(field);
    });
  }

  return query;
};

When request is made, we generate the query string based on the document type - queries.ts

import { client, projectionsResolver } from '@/sanity/utils';

export async function getDocument(type, slug) {
    return client.fetch(
        `*[_type=="${type}" && slug.current=="${slug}"][0] {
            ${projectionsResolver(type)}
        }`,
    );
}

As an example, the next request returns all dereferenced data for a homepage - page.tsx

import { getDocument } from '@/sanity/queries';

export default async function Home() {
  const page = await getDocument({
    type: 'page',
    slug: 'homepage',
  });

  return <pre>{JSON.stringify(page, null, 4)}</pre>;
}

Auto dereferencing for schemas is quite a topic. I want to share my solution for such an issue.

When building small or medium size projects we often do not have much data and usually, we need all fields within one query.

Let's say we have an array of objects like Page Builder for example, inside any of those objects, we could have an array of references and inside that reference a link to another document. To get this last document we would have to build a long GROQ query. So, to do this task manually would require lots of copypasting, keeping in mind that usually projects have around 3-6 documents and 10-20 page builder sections. This snippet will generate a GROQ query based on field types for a given schema, including deeper levels.

Contributor

Artem Komarov

Artem Komarov

Web dev + a bit of everything

New Zealand

Visit Artem Komarov's profile