Discussion of the use of `_key` in block-tools tests and differences in output between local and CodeSandbox environments.

15 replies
Last updated: Apr 17, 2020
Looking at your tests for `block-tools`https://github.com/sanity-io/sanity/tree/master/packages/%40sanity/block-tools/test/tests/HtmlDeserializer
What are the conditions that should create a
_key
? From this source https://github.com/portabletext/portabletext#markdefs-array it sounds like it is only used with
markDefs
, but in the test folder,
randomKey
s are inserted everywhere!Also I created this example
https://codesandbox.io/s/wizardly-dhawan-2iq4w?file=/input.html Where there are keys, but running this locally, there are no keys
AI Update

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 markDefs array
  • 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:

  1. Schema context: The function needs a proper Sanity schema to understand it should add _key properties
  2. Version differences: Different versions may have different default behaviors
  3. 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: _key is mainly for markDefs references
  • Sanity's implementation: _key is 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.

I believe we always place a random
_key
in all arrays with objects because of how JSONmatch work, aka it makes it possible/easier to patch values within an array in real-time settings. https://www.sanity.io/docs/json-match#arrays-v5QlpWPa
The portable text spec might not be completely up to par on this (we should fix that)
But why the different behaviour in Browser vs locally on my computer? The only difference is the use JSDOM locally.
Sorry, didn't catch that. How is the behaviour different?
output locally (with JSDOM):
[
  {
    "_type": "block",
    "markDefs": [],
    "style": "h3",
    "children": [
      {
        "_type": "span",
        "marks": [],
        "text": "Hello world"
      }
    ]
  }
]
output in CodeSandbox (no JSDOM):


[
  {
    "_type": "block",
    "markDefs": [],
    "style": "h3",
    "children": [
      {
        "_type": "span",
        "marks": [],
        "text": "Hello world",
        "_key": "07d8b8b47ba70"
      }
    ],
    "_key": "07d8b8b47ba7"
  }
]
Aha, gotcha. I don't actually know. I'll have to check with the team once they're up and about
user R
- try using JSDOM 12.
All our tests are written with that one and everything is passing.
Hmm. Tried deleting
node_modules
and the lock files, installed jsdom 12, still different results on my computer (no
_key
), for
<h3>Hello world</h3>
Hmm
That's so strange.
Did you import JSDOM like
import {JSDOM} from 'jsdom'
?
As a last resort, you could
import {normalizeBlock}
from blocktools and map the result through that.
yes. sorry, I have to take it back, I guess there is a mismatch in implementation between my local and CodeSandbox implementation, cause downloading the CodeSandbox as zip and simply adding JSDOM produces the
_key
s locally as well


I was looking at the wrong output file... 🤦‍♂️

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?