TryGhost/Ghost · error · ValidationError
Lexical must be a well-formed Lexical document
Error message
Lexical must be a well-formed Lexical document
What it means
A ValidationError thrown when `frame.data.lexical` is a non-empty string but fails `lexicalLib.validate()`, meaning it cannot be parsed as a well-formed Lexical editor document. This is a structural/schema failure distinct from the empty-content check — the string exists but is not a valid Lexical JSON tree. It protects the email renderer from malformed input that would crash or render nothing.
Source
Thrown at ghost/core/core/server/api/endpoints/utils/validators/input/automation_email_previews.js:35
const subject = frame.data.subject;
const lexical = frame.data.lexical;
if (typeof subject !== 'string' || !subject.trim()) {
throw new ValidationError({
message: tpl(messages.subjectRequired),
property: 'subject'
});
}
if (typeof lexical !== 'string' || !lexical.trim()) {
throw new ValidationError({
message: tpl(messages.lexicalRequired),
property: 'lexical'
});
}
if (!await lexicalLib.validate(lexical)) {
throw new ValidationError({
message: tpl(messages.invalidLexical),
property: 'lexical'
});
}
};
module.exports = {
async preview(apiConfig, frame) {
await validatePreviewData(frame);
},
async sendTestEmail(apiConfig, frame) {
const email = frame.data.email;
if (typeof email !== 'string' || !validator.isEmail(email)) {
throw new ValidationError({
message: tpl(messages.invalidEmailReceived),
property: 'email'View on GitHub (pinned to 47d8b0e2ad)
Solutions
- Always obtain `lexical` from the editor's own serializer: `JSON.stringify(editor.getEditorState().toJSON())` — do not hand-build the JSON.
- Round-trip validate locally before sending: parse the string and assert it has a `root` node with a non-empty `children` array.
- If migrating from Mobiledoc/HTML, run it through Ghost's converter (`@tryghost/kg-lexical-converter` or the renderer's converter) to produce a valid Lexical doc.
- Confirm the Lexical schema version of the producing editor matches what the Ghost server version expects.
Example fix
// before
const lexical = JSON.stringify({type: 'doc', content: [...]}); // wrong schema
await api.preview({data: {subject, lexical}});
// after
const lexical = JSON.stringify(editor.getEditorState().toJSON());
const parsed = JSON.parse(lexical);
if (!parsed?.root?.children?.length) throw new Error('Invalid Lexical doc');
await api.preview({data: {subject, lexical}}); Defensive patterns
Strategy: validation
Validate before calling
function assertValidLexicalDoc(lexicalStr) {
let doc;
try { doc = JSON.parse(lexicalStr); } catch { throw new Error('Lexical is not valid JSON'); }
if (!doc || typeof doc !== 'object' || !Array.isArray(doc?.root?.children) || doc.root.children.length === 0) {
throw new Error('Not a well-formed Lexical document');
}
return doc;
} Type guard
const isLexicalDocument = (v) => typeof v === 'object' && v !== null && v.root && Array.isArray(v.root.children) && v.root.children.length > 0;
Try / catch
try {
await api.preview({data: {subject, lexical}});
} catch (err) {
if (err.type === 'ValidationError' && /well-formed/i.test(err.message)) rebuildFromEditor();
else throw err;
} Prevention
- Always serialize via the editor's own serializer; never hand-build Lexical JSON.
- Round-trip validate the JSON (parse + assert root.children) before sending.
- Keep the producing editor's Lexical schema version aligned with the Ghost server version.
When it happens
Trigger: Sending a `lexical` string that is valid JSON but not a Lexical document (e.g. `{"foo": 1}`), hand-built JSON missing required `root`/`children` nodes, a truncated/corrupted Lexical payload, or passing rendered HTML/Mobiledoc text in the `lexical` field.
Common situations: Manually constructing Lexical JSON without the editor's serializer; an older client producing a Lexical schema version the server rejects; copy-pasting a Mobiledoc or HTML string into `lexical`; a network truncation corrupting the JSON; using Lexical nodes from a newer/older editor version with an incompatible schema.
Related errors
- Email content is required
- Subject is required
- The server did not receive a valid email
- Failed to convert HTML to Lexical
- Failed to convert HTML to Lexical
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/6a944ea0ec282ec1.
Report an issue: GitHub.