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 afterRead hook that converts data on-the-fly
Migration Phase: Run a script to permanently migrate all data in the database
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
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.
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 SlateToLexicalFeature added
Contain Slate data in the database
Test the Preview: Add SlateToLexicalFeature to 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: true to all SlateToLexicalFeature instances:
1
SlateToLexicalFeature({ disableHooks:true})
This prevents the afterRead hook from running during migration, improving performance and ensuring clean data writes.
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.
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.
If the migration encounters a Slate node without a converter, it:
Logs a warning to the console
Wraps it in an unknownConverted node 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.
The migration script automatically traverses all collections and fields, retrieves converters from the SlateToLexicalFeature on each field, and converts the data using those converters.
If you're manually calling convertSlateToLexical, you can pass converters directly:
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.
Instead of a SlateToLexicalFeature there is a LexicalPluginToLexicalFeature you can use. And instead of convertSlateToLexical you can use convertLexicalPluginToLexical.