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

  1. Always serialize the Lexical document with `JSON.stringify` before sending.
  2. Validate the string parses as JSON client-side before the request.
  3. 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

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


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/d8affe0f14fc6ceb. Report an issue: GitHub.