Content Modeling

Why Rich Text JSON Is Painful for React Developers

Rich text JSON needs a serializer per node type, forever. Here's the real cost, and the case MDX doesn't win.

SA
Samer Alsayegh
Founder
Published
4 min read
A branching JSON node tree with one unhandled red node next to a flowing text ribbon
Key takeaways

JSON rich text needs a serializer entry for every node type, forever, and that tax shows up in renderers, migrations, and payload size alike. MDX skips the node-type tax by trading a versioning problem for a compile-time one.

A rich text JSON tree looks safe. No compile step, no runtime evaluation, just structured data you walk and render. The catch shows up later. Every node type in that tree needs a matching case in every renderer that touches it, forever. That tax compounds in ways a first look at the format doesn't show.

The Node-Type Tax

Portable Text is Sanity's own rich text spec. It represents a heading, a paragraph, a link, and an inline image as separate typed objects in an array. That's a real strength for querying and transforming content. It's also why rendering it needs a serializer: a function that maps each block type and mark to actual markup. Add a new block type, like a callout, an embed, or a tagged code sample. Every consumer that renders Portable Text needs its serializer updated to handle it. Otherwise that block silently renders as nothing. The CMS doesn't break. The frontend does, quietly, the next time an editor uses a type nobody wired up.

That's not a Portable Text-specific flaw. It's structural to any JSON rich-text format. Lexical, Meta's editor framework, handles the same problem differently. Every node carries a version number. A breaking change to a node's shape bumps that version, so old data can convert automatically. Payload CMS's own migration guide documents exactly this pattern. It includes an upgradeLexicalData function that walks every stored document and re-saves it against the new node shape. That's real engineering effort spent solving a problem MDX doesn't have in the first place, because MDX has no node types to version. It's source text, compiled fresh every time.

When the Editor Itself Gets Deprecated

The clearest example of what this costs isn't a hypothetical. Payload shipped Slate as a rich text editor, then deprecated it in favor of Lexical. Slate support is slated for removal in a future major version. Every team that built on Slate now has a real migration. Convert stored Slate JSON into Lexical's node shape, across every document, before the old editor goes away. That's not a bug in Payload's execution. It's the standing cost of choosing a structured format tied to a specific editor's internal representation. The format and the editor aren't separable, so when the editor changes shape, the stored content has to follow.

What React Actually Has to Do With Each

Rendering Portable Text in React means importing @portabletext/react. That's the successor to the now-legacy @sanity/block-content-to-react, itself a sign of how these libraries drift. It also means supplying a serializer map. One entry per block type. One entry per mark. One entry per custom object your schema defines. Miss one and that content type has no rendering path until someone notices and adds it.

Rendering MDX in React means calling evaluate() once. There's no per-node-type map to maintain, because there's no closed set of node types to enumerate. A new component doesn't need a new case in a serializer. It needs a new entry in the components prop. It renders the moment an author references it in the content. No framework-level registration step at all.

What the API Response Actually Carries

The node-type tax shows up again at the network layer, not just in the renderer. A JSON rich-text tree ships as a full object graph: every heading, mark, and nested span is its own node in the payload. The client has to receive the whole tree before it can walk it, and the walk itself is the render step. There's no shortcut for a partial render, since the renderer needs the full structure to know what it's looking at.

A plain MDX string skips that step. The API returns text, not a tree, so there's nothing to walk on the way in. Draftbase's own delivery API works this way. The content field comes back as a string, same as title or slug. @draftbase/renderer compiles it in one evaluate() call on the way to the page. The payload is smaller because there's no per-node wrapper object multiplying the size of every paragraph. That's not a performance benchmark, just a structural difference: a tree needs nodes to describe its own shape, and a string doesn't.

The Case for JSON Anyway

None of this means JSON rich text is a mistake. A serializer map is also a safety net: an unhandled type fails predictably (nothing renders) instead of executing something unexpected. That's a real property MDX gives up in exchange for flexibility. A team that wants a locked-down set of content shapes gets that guarantee for free from a JSON tree, enforced right at the schema level. No chance of an editor slipping in markup, or a component reference the frontend doesn't expect. MDX has to build that guarantee by hand, through the components allowlist, rather than getting it as a structural property of the format.

The pain shows up for a React team maintaining a renderer over years. Across CMS platform changes, across editor library upgrades, watching a stored content format outlive the tool that produced it. If that's not your situation, the node-type tax barely registers. A small, stable set of content types, one editor, no plans to switch it. MDX trades a versioning problem for a compile-time one. Which trade is worse depends on how often your node types, or your editor, actually change.

Ship content that's built to be found

Draftbase generates schema, structured data, and a fast MDX editor for every post.

Frequently asked questions

Why is rich text JSON hard to render in React?

Each node type needs its own case in a serializer. Miss one, and that content silently renders as nothing.

What is Portable Text?

Sanity's own rich text format. It stores each heading, link, and block as a separate typed object.

Why did Payload move from Slate to Lexical?

Slate got deprecated. Every team on it now has to convert stored content to Lexical's node shape.

Is JSON rich text always worse than MDX?

No. A serializer map is also a safety net. An unknown type fails safe instead of running something unexpected.

Does MDX avoid the node-type problem?

Yes. There's no fixed set of node types to track, since MDX compiles source text fresh each time.

SA
Samer Alsayegh
Founder at Draftbase

Samer is a software engineer and entrepreneur, founder of Draftbase and Ezi Home Services, building technology that simplifies home services. Passionate about software, APIs, automation, and creating products that solve real-world problems.

rich-textmdxreactheadless-cms

Related posts

Draftbase is a headless CMS built for React devs.