TryGhost/Ghost · error · ValidationError

Lexical must be a well-formed Lexical document

Error message

Lexical must be a well-formed Lexical document

What it means

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.

Source

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

    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);
    },

    async sendTestEmail(apiConfig, frame) {
        const email = frame.data.email;

        if (typeof email !== 'string' || !validator.isEmail(email)) {
            throw new ValidationError({
                message: tpl(messages.invalidEmailReceived),
                property: 'email'

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Always obtain `lexical` from the editor's own serializer: `JSON.stringify(editor.getEditorState().toJSON())` — do not hand-build the JSON.
  2. Round-trip validate locally before sending: parse the string and assert it has a `root` node with a non-empty `children` array.
  3. 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.
  4. Confirm the Lexical schema version of the producing editor matches what the Ghost server version expects.

Example fix

// before
const lexical = JSON.stringify({type: 'doc', content: [...]}); // wrong schema
await api.preview({data: {subject, lexical}});

// after
const lexical = JSON.stringify(editor.getEditorState().toJSON());
const parsed = JSON.parse(lexical);
if (!parsed?.root?.children?.length) throw new Error('Invalid Lexical doc');
await api.preview({data: {subject, lexical}});
Defensive patterns

Strategy: validation

Validate before calling

function assertValidLexicalDoc(lexicalStr) {
  let doc;
  try { doc = JSON.parse(lexicalStr); } catch { throw new Error('Lexical is not valid JSON'); }
  if (!doc || typeof doc !== 'object' || !Array.isArray(doc?.root?.children) || doc.root.children.length === 0) {
    throw new Error('Not a well-formed Lexical document');
  }
  return doc;
}

Type guard

const isLexicalDocument = (v) =>
  typeof v === 'object' && v !== null &&
  v.root && Array.isArray(v.root.children) && v.root.children.length > 0;

Try / catch

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

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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