Lexical Migration
Migrating from Slate
While both Slate and Lexical save the editor state in JSON, the structure of the JSON is different. Payload provides a two-phase migration approach that allows you to safely migrate from Slate to Lexical:
- Preview Phase: Test the migration with an
afterReadhook that converts data on-the-fly - Migration Phase: Run a script to permanently migrate all data in the database
Phase 1: Preview & Test
First, add the SlateToLexicalFeature to every richText field you want to migrate. By default, this feature converts your data from Slate to Lexical format on-the-fly through an afterRead hook. If the data is already in Lexical format, it passes through unchanged.
This allows you to test the migration without modifying your database. The on-the-fly conversion happens server-side through the afterRead hook, which means:
- In the Admin Panel: Preview how your content will look in the Lexical editor
- In your API: All read operations (REST, GraphQL, Local API) return converted Lexical data instead of Slate data
- In your application: Your frontend receives Lexical data, allowing you to test if your app correctly handles the new format
You can verify that:
- All content converts correctly
- Custom nodes are handled properly
- Formatting is preserved
- Your application displays the Lexical data as expected
- Any custom converters work as expected
Example:
Important: In preview mode, if you save a document in the Admin Panel, it will overwrite the Slate data with the converted Lexical data in the database. Only save if you've verified the conversion is correct.
Each richText field has its own SlateToLexicalFeature instance because each field may require different converters. For example, one field might contain custom Slate nodes that need custom converters.
Phase 2: Running the Migration Script
Once you've tested the migration in preview mode and verified the results, you can permanently migrate all data in your database.
Why Run the Migration Script?
While the SlateToLexicalFeature works well for testing, running the migration script has important benefits:
- Performance: The
afterReadhook converts data on-the-fly, adding overhead to every read operation - Database Consistency: Direct database operations (e.g.,
payload.db.findinstead ofpayload.find) bypass hooks and return unconverted Slate data - Production Ready: After migration, your data is fully converted and you can remove the migration feature
Migration Prerequisites
CRITICAL: This will permanently overwrite all Slate data. Follow these steps carefully:
- Backup Your Database: Create a complete backup of your database before proceeding. If anything goes wrong without a backup, data recovery may not be possible.
- Convert All richText Fields: Update your config to use
lexicalEditor()for all richText fields. The script only converts fields that:
- Use the Lexical editor
- Have
SlateToLexicalFeatureadded - Contain Slate data in the database
- Test the Preview: Add
SlateToLexicalFeatureto every richText field (as shown in Phase 1) and thoroughly test in the Admin Panel. Build custom converters for any custom Slate nodes before proceeding. - Disable Hooks: Once testing is complete, add
disableHooks: trueto allSlateToLexicalFeatureinstances:
This prevents the afterRead hook from running during migration, improving performance and ensuring clean data writes.
Running the Migration
Create a migration script and run it:
The migration will:
- Process all collections and globals
- Handle all locales (if localization is enabled)
- Migrate both published and draft documents
- Recursively process nested fields (arrays, blocks, tabs, groups)
- Log progress for each collection and document
- Collect and report any errors at the end
Depending on your database size, this may take considerable time. The script provides detailed progress updates as it runs.
Converting Custom Slate Nodes
If your Slate editor includes custom nodes, you'll need to create custom converters for them. A converter transforms a Slate node structure into its Lexical equivalent.
How Converters Work
Each converter receives the Slate node and returns the corresponding Lexical node. The converter also specifies which Slate node types it handles via the nodeTypes array.
Example: Simple Node Converter
Here's the built-in Upload converter as an example:
Example: Node with Children
For nodes that contain child nodes (like links), recursively convert the children:
Converter API
Your converter function receives these parameters:
Adding Custom Converters
You can add custom converters by passing an array of converters to the converters property of the SlateToLexicalFeature props:
Unknown Node Handling
If the migration encounters a Slate node without a converter, it:
- Logs a warning to the console
- Wraps it in an
unknownConvertednode that preserves the original data - Continues migration without failing
This ensures your migration completes even if some converters are missing, allowing you to handle edge cases later.
Migrating lexical data from old version to new version
Each lexical node has a version property which is saved in the database. Every time we make a breaking change to the node's data, we increment the version. This way, we can detect an old version and automatically convert old data to the new format once you open up the editor.
The problem is, this migration only happens when you open the editor, modify the richText field (so that the field's setValue function is called) and save the document. Until you do that for all documents, some documents will still have the old data.
To solve this, we export an upgradeLexicalData function which goes through every single document in your Payload app and re-saves it, if it has a lexical editor. This way, the data is automatically converted to the new format, and that automatic conversion gets applied to every single document in your app.
IMPORTANT: Take a backup of your entire database. If anything goes wrong and you do not have a backup, you are on your own and will not receive any support.
Migrating from payload-plugin-lexical
Migrating from payload-plugin-lexical works similar to migrating from Slate.
Instead of a SlateToLexicalFeature there is a LexicalPluginToLexicalFeature you can use. And instead of convertSlateToLexical you can use convertLexicalPluginToLexical.