PortableText JSON format for Sanity CLI import

20 replies
Last updated: Jan 28, 2026
Looking for any resources/examples about the JSON format needed to handle importing PortableText.
I’m using the Sanity CLI to import content (for the first time) and was surprised not to find anything in snippets, docs, StackOverflow, Googling, etc. I assume this one of the main challenges anyone faces when getting data into Sanity.
AI Update

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 block
  • style: Defines the block style (normal, h1, h2, blockquote, etc.)
  • children: Array of text spans
  • markDefs: 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: []
}

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 the marks array
  • Complex annotations like links are referenced by their _key in marks, with the full definition in markDefs

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 jsdom

Then 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:

  1. Create a sample document in Sanity Studio with your desired Portable Text content
  2. Click the Inspect button (in the document menu, looks like <>)
  3. 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 production

You'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.

Show original thread
20 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?