How to get referenced document data in custom block serializer?

27 replies
Last updated: Nov 29, 2025
Simple question but looks tricky: I created a custom Block in the Content Editor and used a “Reference” type for this (referencing to a Video schema which interacts with Mux). So i can easily pick an already uploaded video to appear right inside my blog. Problem is, in my serializer for this custom type, i only get back a _ref pointing to a document and not the needed infos like playbackId and other related things i get when i query the referred document itself. Is there an easy way to get back the referenced doc attributes from inside my render function?
AI Update

The issue you're experiencing is a common one when working with references in Portable Text. When you query Portable Text that contains custom blocks with references, you only get the _ref ID, not the actual document data. The solution is to dereference the reference in your GROQ query using special syntax for nested arrays.

Since your reference is inside a custom block within your Portable Text array, you need to use GROQ's dereferencing operator (->) combined with array projection syntax.

Solution: Dereference in Your GROQ Query

In your GROQ query where you fetch the Portable Text content, you need to explicitly dereference the reference:

*[_type == "post"][0] {
  title,
  content[]{
    ...,
    // For your custom video block
    _type == "videoBlock" => {
      ...,
      "video": videoReference->{
        _id,
        title,
        "playbackId": asset->playbackId,
        "assetId": asset->assetId,
        // any other Mux fields you need
      }
    }
  }
}

The key parts here are:

  • videoReference-> - The -> operator dereferences the reference and fetches the referenced document
  • You can chain dereferences, like asset->playbackId to get nested reference data
  • Wrap this in a conditional (_type == "videoBlock" =>) to only apply it to your specific block type

For References in Annotations (markDefs)

If your video reference is actually an annotation rather than a block, the syntax is slightly different because it's nested inside the markDefs array:

content[]{
  ...,
  markDefs[]{
    ...,
    _type == "videoReference" => {
      ...,
      "videoData": @.reference->{
        _id,
        title,
        "playbackId": asset->playbackId,
        "assetId": asset->assetId
      }
    }
  }
}

The @ symbol refers to the current item in the array context, which is necessary when working with nested arrays like markDefs.

In Your Serializer

Once you've dereferenced in the query, your serializer will receive the full data:

import { PortableText } from '@portabletext/react'

const components = {
  types: {
    videoBlock: ({value}) => {
      // Now value.video contains all the fields you queried
      const { playbackId, title } = value.video
      
      return (
        <div>
          <h3>{title}</h3>
          <MuxPlayer playbackId={playbackId} />
        </div>
      )
    }
  }
}

// Usage
<PortableText value={content} components={components} />

This approach ensures you get all the referenced document data in a single query, avoiding the need to fetch it separately in your render function. The dereferencing happens at query time, so your serializer receives complete data objects rather than just reference IDs.

Show original thread
27 replies

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?