Discussion of the use of `_key` in block-tools tests and differences in output between local and CodeSandbox environments.
Great question! This confusion comes from the difference between the Portable Text specification (the open standard) and Sanity's implementation requirements for storing Portable Text.
When _key is Actually Required
According to the Sanity documentation on _key, the _key property is required for objects within arrays in Sanity documents. This is a Sanity-specific requirement for real-time collaborative editing and proper document tracking.
However, this doesn't mean _key is required on every object in Portable Text according to the spec itself. Let me clarify:
In the Portable Text Specification
Looking at the official Portable Text spec on GitHub, _key is primarily used in the markDefs array where it serves as a reference identifier. The spec shows span objects like this:
{
"_type": "span",
"marks": ["a-key", "emphasis"],
"text": "some text"
}Notice there's no _key on the span itself in the spec example.
In Sanity's Implementation
When you're storing Portable Text in Sanity's Content Lake, the broader array requirement kicks in. Since Portable Text blocks are stored as arrays, and those blocks contain arrays of children, Sanity requires _key on:
- Each block in the blocks array
- Each item in the
markDefsarray - Potentially on child spans (though the spec doesn't mandate this)
- Any custom inline objects
This is why the block-tools tests add randomKey everywhere - they're testing the Sanity-ready output format, not the minimal Portable Text spec.
Why Your CodeSandbox Has No Keys
The htmlToBlocks function from @sanity/block-tools may or may not generate _key properties depending on:
- Schema context: The function needs a proper Sanity schema to understand it should add
_keyproperties - Version differences: Different versions may have different default behaviors
- Configuration: You may need to explicitly configure key generation
Here's a proper setup that should generate keys:
import {htmlToBlocks, randomKey} from '@sanity/block-tools'
import {Schema} from '@sanity/schema'
const defaultSchema = Schema.compile({
name: 'default',
types: [
{
type: 'object',
name: 'blogPost',
fields: [
{
title: 'Body',
name: 'body',
type: 'array',
of: [{type: 'block'}]
}
]
}
]
})
const blockContentType = defaultSchema
.get('blogPost')
.fields.find(field => field.name === 'body').type
const blocks = htmlToBlocks(htmlString, blockContentType, {
parseHtml: html => new DOMParser().parseFromString(html, 'text/html')
})If keys still aren't being generated automatically, you can add them manually:
import {randomKey} from '@sanity/block-tools'
const ensureKeys = (blocks) => blocks.map(block => ({
...block,
_key: block._key || randomKey(12),
markDefs: block.markDefs?.map(def => ({
...def,
_key: def._key || randomKey(12)
})),
children: block.children?.map(child => ({
...child,
_key: child._key || randomKey(12)
}))
}))Bottom Line
- Portable Text spec:
_keyis mainly formarkDefsreferences - Sanity's implementation:
_keyis required on all objects in arrays for collaboration and tracking - block-tools tests: Show the Sanity-ready format with keys everywhere
- Your code: May need explicit schema context or manual key generation to match Sanity's requirements
The tests are correct for Sanity usage - if you're importing data into Sanity, you'll want those _key properties throughout.
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.