{"record":{"id":"fcb151c5753995d8","repo":"laurent22/joplin","slug":"validation-error-title-must-be-maxtitlelength","errorCode":null,"errorMessage":"Validation error: title must be ${maxTitleLength} characters or less","messagePattern":"Validation error: title must be (.+?) characters or less","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/lib/BaseModel.ts","lineNumber":622,"sourceCode":"\t\tquery.id = modelId;\n\t\tquery.modObject = o;\n\n\t\treturn query;\n\t}\n\n\tpublic static userSideValidation(o: Record<string, unknown>) {\n\t\tif (typeof o.id === 'string' && !o.id.match(/^[a-f0-9]{32}$/)) {\n\t\t\tthrow new Error('Validation error: ID must a 32-characters lowercase hexadecimal string');\n\t\t}\n\n\t\tconst timestamps = ['user_updated_time', 'user_created_time'] as const;\n\t\tfor (const k of timestamps) {\n\t\t\tif ((k in o) && (typeof o[k] !== 'number' || isNaN(o[k] as number) || (o[k] as number) < 0)) throw new Error('Validation error: user_updated_time and user_created_time must be numbers greater than 0');\n\t\t}\n\n\t\tconst maxTitleLength = 4096;\n\t\tif (typeof o.title === 'string' && o.title.length > maxTitleLength) {\n\t\t\tthrow new Error(`Validation error: title must be ${maxTitleLength} characters or less`);\n\t\t}\n\n\t\t// Null bytes break Joplin's serialised note format and can cause silent\n\t\t// truncation in some HTTP clients (notably React Native on iOS).\n\t\tconst nul = String.fromCharCode(0);\n\t\tfor (const k of Object.keys(o)) {\n\t\t\tconst v = o[k];\n\t\t\tif (typeof v === 'string' && v.includes(nul)) {\n\t\t\t\tthrow new Error(`Validation error: ${k} cannot contain a null byte`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any -- o is any BaseItemEntity subclass being saved; subclasses override save() with stricter per-entity types\n\tpublic static async save(o: any, options: SaveOptions = null) {\n\t\t// When saving, there's a mutex per model ID. This is because the model returned from this function\n\t\t// is basically its input `o` (instead of being read from the database, for performance reasons).\n\t\t// This works well in general except if that model is saved simultaneously in two places. In that","sourceCodeStart":604,"sourceCodeEnd":640,"githubUrl":"https://github.com/laurent22/joplin/blob/2654b33620775080d1d59c552259d41e33dad3d2/packages/lib/BaseModel.ts#L604-L640","documentation":"Thrown by BaseModel.userSideValidation() when o.title is a string longer than maxTitleLength (hardcoded 4096). Titles longer than this would exceed the SQLite column / sync payload limits Joplin relies on, so they are rejected at the validation boundary.","triggerScenarios":"userSideValidation(o) with typeof o.title === 'string' and o.title.length > 4096 — e.g. pasting a very long document as a note title, or an importer that puts the body in the title field.","commonSituations":"Programmatic note creation where body was accidentally assigned to title; importer mapping wrong field; concatenation that produced an oversized title.","solutions":["Truncate the title to <= 4096 chars before save (title.slice(0, 4096)).","Move long content into the note body, not the title.","For importer code, verify the field mapping so body content does not land in title."],"exampleFix":"// before\nawait Note.save({ title: veryLongDocumentText });\n// after\nawait Note.save({ title: veryLongDocumentText.slice(0, 4096), body: veryLongDocumentText });","handlingStrategy":"validation","validationCode":"const MAX_TITLE = 4096;\nif (typeof entity.title === 'string' && entity.title.length > MAX_TITLE) {\n  throw new Error(`Title too long (${entity.title.length} > ${MAX_TITLE})`);\n}","typeGuard":"const isWithinTitleLimit = (t, max = 4096) => typeof t !== 'string' || t.length <= max;","tryCatchPattern":"try { BaseModel.userSideValidation(entity); }\ncatch (e) { if (/title must be/.test(e.message)) { entity.title = entity.title.slice(0, 4096); } else throw e; }","preventionTips":["Truncate titles at the import boundary, not at save time.","Map long-form content to `body`, never `title`, when importing."],"tags":["validation","data-integrity","typescript","joplin-core"],"backgroundTag":null,"analyzedSha":"2654b33620775080d1d59c552259d41e33dad3d2","analyzedAt":"2026-08-12T14:26:46.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}