TryGhost/Ghost · error · ValidationError
Lexical must be a valid JSON string
Error message
Lexical must be a valid JSON string
What it means
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.
Source
Thrown at ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js:94
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'
});
}
try {
JSON.parse(lexical);
} catch (e) {
throw new ValidationError({
message: tpl(messages.invalidLexical),
property: 'lexical'
});
}
};
module.exports = {
async add(apiConfig, frame) {
await validateAutomatedEmail(frame);
},
async edit(apiConfig, frame) {
await validateAutomatedEmail(frame);
},
editSenders(apiConfig, frame) {
const senderName = frame.data.sender_name;
const senderEmail = frame.data.sender_email;
const senderReplyTo = frame.data.sender_reply_to;
View on GitHub (pinned to 47d8b0e2ad)
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).
Example fix
// before
await api.emailPreview.send({data: {subject: 'Hi', lexical: '<p>hello</p>', email: 'a@b.com'}}); // HTML not JSON
// after
await api.emailPreview.send({data: {subject: 'Hi', lexical: JSON.stringify({root: {children: []}}), email: 'a@b.com'}}); Defensive patterns
Strategy: validation
Validate before calling
function isValidLexicalJson(s: unknown): boolean {
if (typeof s !== 'string' || !s.trim()) return false;
try { JSON.parse(s); return true; } catch { return false; }
}
if (!isValidLexicalJson(payload.lexical)) {
throw new Error('lexical must be a valid JSON string');
} Type guard
function isJsonString(s: unknown): s is string {
if (typeof s !== 'string' || !s.trim()) return false;
try { JSON.parse(s); return true; } catch { return false; }
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Email content is required
- Subject is required
- Failed to convert HTML to Lexical
- Failed to convert HTML to Lexical
- Token is required
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/d8affe0f14fc6ceb.
Report an issue: GitHub.