Skip to main content
Lexical supports multiple serialization formats to save, load, and transfer editor content.

JSON Serialization

JSON is Lexical’s native serialization format, providing lossless conversion to and from EditorState.

Exporting to JSON

1
Get Editor State
2
const editorState = editor.getEditorState();
3
Convert to JSON
4
const json = editorState.toJSON();
const jsonString = JSON.stringify(json);
5
Save to Storage
6
// LocalStorage
localStorage.setItem('editorContent', jsonString);

// Send to server
await fetch('/api/save', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: jsonString,
});

Importing from JSON

1
Load JSON String
2
const jsonString = localStorage.getItem('editorContent');
3
Parse and Load
4
if (jsonString) {
  const editorState = editor.parseEditorState(jsonString);
  editor.setEditorState(editorState);
}

React Example

import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { useEffect } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';

function SavePlugin() {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    // Auto-save on changes
    return editor.registerUpdateListener(({ editorState }) => {
      const json = editorState.toJSON();
      localStorage.setItem('content', JSON.stringify(json));
    });
  }, [editor]);

  return null;
}

function Editor() {
  const initialConfig = {
    namespace: 'MyEditor',
    editorState: localStorage.getItem('content') || undefined,
    onError: (error: Error) => console.error(error),
  };

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <RichTextPlugin ... />
      <SavePlugin />
    </LexicalComposer>
  );
}

JSON Structure

JSON serialization preserves the complete node tree:
{
  "root": {
    "children": [
      {
        "children": [
          {
            "detail": 0,
            "format": 1,
            "mode": "normal",
            "style": "",
            "text": "Hello world",
            "type": "text",
            "version": 1
          }
        ],
        "direction": "ltr",
        "format": "",
        "indent": 0,
        "type": "paragraph",
        "version": 1
      }
    ],
    "direction": "ltr",
    "format": "",
    "indent": 0,
    "type": "root",
    "version": 1
  }
}

Markdown Serialization

The @lexical/markdown package provides Markdown import/export.

Setup

npm install @lexical/markdown

Exporting to Markdown

import { $convertToMarkdownString, TRANSFORMERS } from '@lexical/markdown';

editor.update(() => {
  const markdown = $convertToMarkdownString(TRANSFORMERS);
  console.log(markdown);
});
With custom transformers:
import {
  $convertToMarkdownString,
  BOLD_ITALIC_STAR,
  BOLD_STAR,
  HEADING,
  ITALIC_STAR,
  LINK,
  QUOTE,
  UNORDERED_LIST,
} from '@lexical/markdown';

const markdown = $convertToMarkdownString([
  BOLD_ITALIC_STAR,
  BOLD_STAR,
  ITALIC_STAR,
  HEADING,
  QUOTE,
  LINK,
  UNORDERED_LIST,
]);

Importing from Markdown

import { $convertFromMarkdownString, TRANSFORMERS } from '@lexical/markdown';

const markdown = '# Hello\n\nThis is **bold** text.';

editor.update(() => {
  $convertFromMarkdownString(markdown, TRANSFORMERS);
});

Markdown Shortcuts Plugin

Enable Markdown shortcuts as you type:
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';
import { TRANSFORMERS } from '@lexical/markdown';

function Editor() {
  return (
    <LexicalComposer initialConfig={config}>
      <RichTextPlugin ... />
      <MarkdownShortcutPlugin transformers={TRANSFORMERS} />
    </LexicalComposer>
  );
}
Now users can type Markdown syntax:
  • # for headings
  • **text** for bold
  • *text* for italic
  • - for lists
  • > for quotes

Available Transformers

import {
  // Text format transformers
  BOLD_STAR,              // **bold**
  BOLD_UNDERSCORE,        // __bold__
  ITALIC_STAR,            // *italic*
  ITALIC_UNDERSCORE,      // _italic_
  BOLD_ITALIC_STAR,       // ***bold italic***
  BOLD_ITALIC_UNDERSCORE, // ___bold italic___
  INLINE_CODE,            // `code`
  STRIKETHROUGH,          // ~~strikethrough~~
  HIGHLIGHT,              // ==highlight==
  
  // Element transformers
  HEADING,                // # Heading
  QUOTE,                  // > Quote
  CODE,                   // ```code```
  UNORDERED_LIST,         // - List
  ORDERED_LIST,           // 1. List
  CHECK_LIST,             // - [ ] Checklist
  
  // Text match transformers
  LINK,                   // [text](url)
  
  // All transformers
  TRANSFORMERS,
} from '@lexical/markdown';
See packages/lexical-markdown/src/index.ts for complete API.

HTML Serialization

The @lexical/html package provides HTML import/export.

Setup

npm install @lexical/html

Exporting to HTML

import { $generateHtmlFromNodes } from '@lexical/html';

editor.update(() => {
  const htmlString = $generateHtmlFromNodes(editor);
  console.log(htmlString);
});
With selection:
import { $generateHtmlFromNodes } from '@lexical/html';
import { $getSelection } from 'lexical';

editor.update(() => {
  const selection = $getSelection();
  const htmlString = $generateHtmlFromNodes(editor, selection);
  console.log(htmlString);
});

Importing from HTML

import { $generateNodesFromDOM } from '@lexical/html';
import { $getRoot, $insertNodes } from 'lexical';

const htmlString = '<p>Hello <strong>world</strong></p>';

editor.update(() => {
  // Parse HTML string to DOM
  const parser = new DOMParser();
  const dom = parser.parseFromString(htmlString, 'text/html');
  
  // Generate Lexical nodes
  const nodes = $generateNodesFromDOM(editor, dom);
  
  // Insert nodes
  $getRoot().clear();
  $getRoot().append(...nodes);
});

Custom HTML Export

Control HTML export for custom nodes:
import { DOMExportOutput, LexicalEditor } from 'lexical';

class CustomNode extends ElementNode {
  exportDOM(editor: LexicalEditor): DOMExportOutput {
    const element = document.createElement('div');
    element.className = 'custom-node';
    element.setAttribute('data-custom', 'true');
    return { element };
  }
}

Custom HTML Import

Handle HTML import for custom nodes:
import { DOMConversionMap, DOMConversionOutput } from 'lexical';

class CustomNode extends ElementNode {
  static importDOM(): DOMConversionMap | null {
    return {
      div: (node: Node) => {
        const div = node as HTMLDivElement;
        if (div.hasAttribute('data-custom')) {
          return {
            conversion: convertCustomElement,
            priority: 1,
          };
        }
        return null;
      },
    };
  }
}

function convertCustomElement(
  element: HTMLElement,
): DOMConversionOutput {
  const node = $createCustomNode();
  return { node };
}
See packages/lexical-html/src/index.ts for the complete API.

Copy & Paste

Lexical handles copy/paste automatically, using HTML as the primary format:

Custom Paste Handling

import { PASTE_COMMAND } from 'lexical';
import { COMMAND_PRIORITY_HIGH } from 'lexical';

editor.registerCommand(
  PASTE_COMMAND,
  (event: ClipboardEvent) => {
    const clipboardData = event.clipboardData;
    if (clipboardData) {
      const htmlData = clipboardData.getData('text/html');
      const textData = clipboardData.getData('text/plain');
      
      // Custom paste logic
      return true; // Prevent default
    }
    return false;
  },
  COMMAND_PRIORITY_HIGH,
);

Export on Copy

import { COPY_COMMAND } from 'lexical';
import { $generateHtmlFromNodes } from '@lexical/html';
import { $getSelection } from 'lexical';

editor.registerCommand(
  COPY_COMMAND,
  (event: ClipboardEvent) => {
    const selection = $getSelection();
    if (selection && event.clipboardData) {
      const htmlString = $generateHtmlFromNodes(editor, selection);
      event.clipboardData.setData('text/html', htmlString);
      event.clipboardData.setData('text/plain', selection.getTextContent());
      event.preventDefault();
      return true;
    }
    return false;
  },
  COMMAND_PRIORITY_HIGH,
);

File Export

Export content to downloadable files:
function exportToFile(content: string, filename: string, type: string) {
  const blob = new Blob([content], { type });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  link.click();
  URL.revokeObjectURL(url);
}

// Export as JSON
editor.getEditorState().read(() => {
  const json = editor.getEditorState().toJSON();
  exportToFile(
    JSON.stringify(json, null, 2),
    'document.json',
    'application/json',
  );
});

// Export as Markdown
editor.update(() => {
  const markdown = $convertToMarkdownString(TRANSFORMERS);
  exportToFile(markdown, 'document.md', 'text/markdown');
});

// Export as HTML
editor.update(() => {
  const html = $generateHtmlFromNodes(editor);
  exportToFile(html, 'document.html', 'text/html');
});

Best Practices

  • JSON for Persistence: Use JSON for database storage (lossless)
  • Markdown for Portability: Use Markdown for plain-text compatibility
  • HTML for Web: Use HTML for web integration and email
  • Custom Nodes: Implement exportJSON(), importJSON(), exportDOM(), and importDOM()
  • Version Control: Include version numbers in serialized data
  • Validation: Validate imported data before parsing
  • Error Handling: Wrap parsing in try-catch blocks

See Also

Build docs developers (and LLMs) love