TryGhost/Ghost · error · ValidationError

Email content is required

Error message

Email content is required

What it means

A ValidationError thrown by the automation email preview validator when `frame.data.lexical` is missing, not a string, or whitespace-only. The validator treats `lexical` (the Lexical editor JSON serialized as a string) as mandatory for both the `preview` and `sendTestEmail` operations. It fires before the Lexical structural validation, so it only catches empty/absent content.

Source

Thrown at ghost/core/core/server/api/endpoints/utils/validators/input/automation_email_previews.js:28

    invalidEmailReceived: 'The server did not receive a valid email',
    invalidLexical: 'Lexical must be a well-formed Lexical document',
    subjectRequired: 'Subject is required',
    lexicalRequired: 'Email content is required'
};

const validatePreviewData = async (frame) => {
    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);
    },

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Stringify the Lexical editor state and pass it as `data.lexical`: `JSON.stringify(editorState.toJSON())`.
  2. Verify the post actually has content before triggering the preview — guard with a non-empty check upstream.
  3. Use the correct field name `lexical` (not `mobiledoc` or `html`) for this endpoint.
  4. If the source content is HTML/Mobiledoc, convert it to a Lexical document first via the Ghost lexical converter.

Example fix

// before
await api.sendTestEmail({data: {email, subject, lexical: editorState.toJSON()}});

// after
const lexical = JSON.stringify(editorState.toJSON());
if (!lexical.trim()) throw new Error('Editor has no content');
await api.sendTestEmail({data: {email, subject, lexical}});
Defensive patterns

Strategy: validation

Validate before calling

function ensureLexicalString(lexical) {
  if (typeof lexical !== 'string' || !lexical.trim()) {
    throw new Error('Email content is required');
  }
  return lexical;
}

Type guard

const isLexicalString = (v) => typeof v === 'string' && v.trim().length > 0;

Try / catch

try {
  await api.preview({data: {subject, lexical}});
} catch (err) {
  if (err.type === 'ValidationError' && err.property === 'lexical' && /required/i.test(err.message)) promptForContent();
  else throw err;
}

Prevention

When it happens

Trigger: Calling the automation preview/send-test-email endpoint with no `lexical` field, with `lexical: null`, or with `lexical: ""`. Also when the caller passes a Lexical object instead of its JSON-serialized string form and the field ends up undefined after frame parsing.

Common situations: A post has no Lexical document (e.g. a brand-new/draft post with empty content); the editor's `serialize()` output was not stringified before being sent; the wrong key name is used (e.g. `mobiledoc` or `html` instead of `lexical`); a migration left posts with null content.

Related errors


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