{"record":{"id":"df453ebb75207c51","repo":"TryGhost/Ghost","slug":"subject-is-required-df453e","errorCode":null,"errorMessage":"Subject is required","messagePattern":"Subject is required","errorType":"validation","errorClass":"ValidationError","httpStatus":422,"severity":"error","filePath":"ghost/core/core/server/api/endpoints/utils/validators/input/automation_email_previews.js","lineNumber":21,"sourceCode":"\nconst validator = require('@tryghost/validator');\nconst {ValidationError} = require('@tryghost/errors');\nconst tpl = require('@tryghost/tpl');\nconst lexicalLib = require('../../../../../lib/lexical');\n\nconst messages = {\n    invalidEmailReceived: 'The server did not receive a valid email',\n    invalidLexical: 'Lexical must be a well-formed Lexical document',\n    subjectRequired: 'Subject is required',\n    lexicalRequired: 'Email content is required'\n};\n\nconst validatePreviewData = async (frame) => {\n    const subject = frame.data.subject;\n    const lexical = frame.data.lexical;\n\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    if (!await lexicalLib.validate(lexical)) {\n        throw new ValidationError({\n            message: tpl(messages.invalidLexical),\n            property: 'lexical'\n        });\n    }","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/ghost/core/core/server/api/endpoints/utils/validators/input/automation_email_previews.js#L3-L39","documentation":"A ValidationError thrown by the automation email preview input validator when the request frame's `data.subject` is missing, not a string, or whitespace-only. The validator runs for both the `preview` and `sendTestEmail` automation endpoints before any email rendering happens, so a blank subject short-circuits the whole call. It is a client-side input error, not a server fault.","triggerScenarios":"POSTing to the automation email preview (or send-test-email) endpoint with a body where `subject` is omitted, null, a number/boolean, or an empty/whitespace string. For example `{\"lexical\": \"...\"}` with no `subject`, or `{\"subject\": \"   \"}`.","commonSituations":"An integration or Zapier-style automation submits a post's title as the subject but the title field is empty on the source post; a frontend form forgets to bind the subject input; the caller sends `subject` as a number (e.g. an ID) by mistake; trailing whitespace from a textarea is the only content.","solutions":["Ensure `frame.data.subject` (or the JSON body `subject`) is a non-empty trimmed string before submitting the request.","Validate on the client that `subject.trim().length > 0` and surface a form error instead of hitting the API.","If driving the preview from a post, map the post title into `subject` and default it to a placeholder when the title is blank.","Confirm the request `Content-Type` is `application/json` so the body parses into `frame.data` correctly."],"exampleFix":"// before\nawait api.preview({data: {lexical, subject: post.title}}); // post.title is undefined\n\n// after\nconst subject = (post.title || '').trim();\nif (!subject) throw new Error('Cannot preview: post has no title');\nawait api.preview({data: {lexical, subject}});","handlingStrategy":"validation","validationCode":"function validatePreviewPayload({subject, lexical, email} = {}) {\n  const errors = {};\n  if (typeof subject !== 'string' || !subject.trim()) errors.subject = 'Subject is required';\n  if (typeof lexical !== 'string' || !lexical.trim()) errors.lexical = 'Email content is required';\n  if (email !== undefined && (typeof email !== 'string' || !validator.isEmail(email))) errors.email = 'Invalid email';\n  return errors;\n}\n// const errs = validatePreviewPayload(payload); if (Object.keys(errs).length) throw new Error(JSON.stringify(errs));","typeGuard":"const isNonEmptyString = (v) => typeof v === 'string' && v.trim().length > 0;\nconst hasValidSubject = (p) => isNonEmptyString(p?.subject);","tryCatchPattern":"try {\n  await api.preview({data: {subject, lexical}});\n} catch (err) {\n  if (err.type === 'ValidationError' && err.property === 'subject') showFieldError('subject', err.message);\n  else throw err;\n}","preventionTips":["Bind the subject input to a required, trimmed form field on the client.","Run the same non-empty-string guard locally before any preview/send request.","Unit-test the request builder with empty/whitespace/undefined subjects."],"tags":["validation","automation","email-preview","api-input"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}