{"record":{"id":"d8affe0f14fc6ceb","repo":"TryGhost/Ghost","slug":"lexical-must-be-a-valid-json-string","errorCode":null,"errorMessage":"Lexical must be a valid JSON string","messagePattern":"Lexical must be a valid JSON string","errorType":"validation","errorClass":"ValidationError","httpStatus":422,"severity":"error","filePath":"ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js","lineNumber":94,"sourceCode":"\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    try {\n        JSON.parse(lexical);\n    } catch (e) {\n        throw new ValidationError({\n            message: tpl(messages.invalidLexical),\n            property: 'lexical'\n        });\n    }\n};\n\nmodule.exports = {\n    async add(apiConfig, frame) {\n        await validateAutomatedEmail(frame);\n    },\n    async edit(apiConfig, frame) {\n        await validateAutomatedEmail(frame);\n    },\n    editSenders(apiConfig, frame) {\n        const senderName = frame.data.sender_name;\n        const senderEmail = frame.data.sender_email;\n        const senderReplyTo = frame.data.sender_reply_to;\n","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js#L76-L112","documentation":"Thrown by `validatePreviewData` (and the automated-email add/edit validator) when `frame.data.lexical` is a non-empty string but `JSON.parse(lexical)` throws. Ghost expects Lexical content as a JSON-serialized document; syntactically invalid JSON yields a 422 ValidationError on the `lexical` property.","triggerScenarios":"A preview/add/edit request sends `lexical` as a non-empty string that is not valid JSON — truncated JSON, double-serialized, plain text, or containing stray characters.","commonSituations":"Client sends a raw string instead of `JSON.stringify(lexicalDoc)`; double-encoding the value; truncation from a length limit; manually edited JSON with a syntax error; sending HTML where Lexical JSON is expected.","solutions":["Always serialize the Lexical document with `JSON.stringify` before sending.","Validate the string parses as JSON client-side before the request.","Ensure the value is not double-encoded (stringified twice)."],"exampleFix":"// before\nawait api.emailPreview.send({data: {subject: 'Hi', lexical: '<p>hello</p>', email: 'a@b.com'}}); // HTML not JSON\n\n// after\nawait api.emailPreview.send({data: {subject: 'Hi', lexical: JSON.stringify({root: {children: []}}), email: 'a@b.com'}});","handlingStrategy":"validation","validationCode":"function isValidLexicalJson(s: unknown): boolean {\n    if (typeof s !== 'string' || !s.trim()) return false;\n    try { JSON.parse(s); return true; } catch { return false; }\n}\nif (!isValidLexicalJson(payload.lexical)) {\n    throw new Error('lexical must be a valid JSON string');\n}","typeGuard":"function isJsonString(s: unknown): s is string {\n    if (typeof s !== 'string' || !s.trim()) return false;\n    try { JSON.parse(s); return true; } catch { return false; }\n}","tryCatchPattern":null,"preventionTips":["Always `JSON.stringify` the Lexical document before sending.","Validate the string parses as JSON client-side.","Avoid double-serializing or sending raw HTML/JSON-object values."],"tags":["api","validation","email","lexical","json","preview"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}