{"record":{"id":"6a944ea0ec282ec1","repo":"TryGhost/Ghost","slug":"lexical-must-be-a-well-formed-lexical-document","errorCode":null,"errorMessage":"Lexical must be a well-formed Lexical document","messagePattern":"Lexical must be a well-formed Lexical document","errorType":"validation","errorClass":"ValidationError","httpStatus":422,"severity":"error","filePath":"ghost/core/core/server/api/endpoints/utils/validators/input/automation_email_previews.js","lineNumber":35,"sourceCode":"    const subject = frame.data.subject;\n    const lexical = frame.data.lexical;\n\n    if (typeof subject !== 'string' || !subject.trim()) {\n        throw new ValidationError({\n            message: tpl(messages.subjectRequired),\n            property: 'subject'\n        });\n    }\n\n    if (typeof lexical !== 'string' || !lexical.trim()) {\n        throw new ValidationError({\n            message: tpl(messages.lexicalRequired),\n            property: 'lexical'\n        });\n    }\n\n    if (!await lexicalLib.validate(lexical)) {\n        throw new ValidationError({\n            message: tpl(messages.invalidLexical),\n            property: 'lexical'\n        });\n    }\n};\n\nmodule.exports = {\n    async preview(apiConfig, frame) {\n        await validatePreviewData(frame);\n    },\n\n    async sendTestEmail(apiConfig, frame) {\n        const email = frame.data.email;\n\n        if (typeof email !== 'string' || !validator.isEmail(email)) {\n            throw new ValidationError({\n                message: tpl(messages.invalidEmailReceived),\n                property: 'email'","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/ghost/core/core/server/api/endpoints/utils/validators/input/automation_email_previews.js#L17-L53","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst lexical = JSON.stringify({type: 'doc', content: [...]}); // wrong schema\nawait api.preview({data: {subject, lexical}});\n\n// after\nconst lexical = JSON.stringify(editor.getEditorState().toJSON());\nconst parsed = JSON.parse(lexical);\nif (!parsed?.root?.children?.length) throw new Error('Invalid Lexical doc');\nawait api.preview({data: {subject, lexical}});","handlingStrategy":"validation","validationCode":"function assertValidLexicalDoc(lexicalStr) {\n  let doc;\n  try { doc = JSON.parse(lexicalStr); } catch { throw new Error('Lexical is not valid JSON'); }\n  if (!doc || typeof doc !== 'object' || !Array.isArray(doc?.root?.children) || doc.root.children.length === 0) {\n    throw new Error('Not a well-formed Lexical document');\n  }\n  return doc;\n}","typeGuard":"const isLexicalDocument = (v) =>\n  typeof v === 'object' && v !== null &&\n  v.root && Array.isArray(v.root.children) && v.root.children.length > 0;","tryCatchPattern":"try {\n  await api.preview({data: {subject, lexical}});\n} catch (err) {\n  if (err.type === 'ValidationError' && /well-formed/i.test(err.message)) rebuildFromEditor();\n  else throw err;\n}","preventionTips":["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."],"tags":["validation","lexical","automation","email-preview","data-integrity"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}