
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeI 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.
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_key RequirementThis 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: []
}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:
"strong" go directly in the marks array_key in marks, with the full definition in markDefsIf 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.
The fastest way to see the exact JSON structure Sanity expects:
<>)This shows you exactly what your import data should look like, including all the _key values and nested structure.
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 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.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store