Understanding the JSON format for importing PortableText into Sanity
I found what you're looking for! When importing Portable Text via the Sanity CLI, understanding the JSON format is definitely one of the key challenges. Let me break down what you need to know.
Basic Portable Text Structure
Portable Text is stored as an array of block objects. Here's the essential structure you need for your NDJSON import:
{
"_id": "post-123",
"_type": "post",
"content": [
{
"_type": "block",
"_key": "abc123",
"style": "normal",
"children": [
{
"_type": "span",
"_key": "span1",
"text": "Your text content here",
"marks": []
}
],
"markDefs": []
}
]
}The key components:
_type: "block": Identifies this as a text blockstyle: Defines the block style (normal,h1,h2,blockquote, etc.)children: Array of text spansmarkDefs: Defines complex annotations like links
The _key Requirement
This is critical: every object in an array requires a _key property. When you create content in Sanity Studio, these are auto-generated, but when importing via CLI, you must provide them yourself.
The _key must be unique within its parent array (not globally unique like _id). Use a UUID library to generate them:
import {uuid} from '@sanity/uuid'
const block = {
_key: uuid(),
_type: "block",
style: "normal",
children: [
{
_key: uuid(),
_type: "span",
text: "Hello world",
marks: []
}
],
markDefs: []
}Example with Links and Formatting
Here's a more complex example showing bold text and a link:
{
"_type": "block",
"_key": "block1",
"style": "normal",
"children": [
{
"_type": "span",
"_key": "span1",
"text": "This is ",
"marks": []
},
{
"_type": "span",
"_key": "span2",
"text": "bold text",
"marks": ["strong"]
},
{
"_type": "span",
"_key": "span3",
"text": " and ",
"marks": []
},
{
"_type": "span",
"_key": "span4",
"text": "a link",
"marks": ["link1"]
}
],
"markDefs": [
{
"_key": "link1",
"_type": "link",
"href": "https://example.com"
}
]
}Notice how:
- Simple decorators like
"strong"go directly in themarksarray - Complex annotations like links are referenced by their
_keyinmarks, with the full definition inmarkDefs
Converting HTML to Portable Text
If you're migrating from HTML-based content, don't manually construct this JSON! Use the @portabletext/block-tools package:
npm install @portabletext/block-tools jsdomThen use the htmlToBlocks function:
import {htmlToBlocks} from '@portabletext/block-tools'
import {JSDOM} from 'jsdom'
import {Schema} from '@sanity/schema'
const blocks = htmlToBlocks(
'<p>Your HTML here</p>',
blockContentSchema,
{
parseHtml: (html) => new JSDOM(html).window.document
}
)This will handle the conversion and generate all the required _key values for you.
Pro Tip: Use the Inspect Button
The fastest way to see the exact JSON structure Sanity expects:
- Create a sample document in Sanity Studio with your desired Portable Text content
- Click the Inspect button (in the document menu, looks like
<>) - Copy the JSON structure as your template
This shows you exactly what your import data should look like, including all the _key values and nested structure.
CLI Import Format
When using sanity dataset import, your NDJSON file should have one document per line:
{"_id":"post-1","_type":"post","content":[{"_type":"block","_key":"abc","children":[{"_type":"span","_key":"def","text":"Hello","marks":[]}],"markDefs":[]}]}
{"_id":"post-2","_type":"post","content":[{"_type":"block","_key":"ghi","children":[{"_type":"span","_key":"jkl","text":"World","marks":[]}],"markDefs":[]}]}Then import with:
sanity dataset import data.ndjson productionYou're absolutely right that this is a common challenge when getting data into Sanity—the content migration documentation covers these patterns in depth for more complex scenarios.
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.