{"record":{"id":"ac3ad2432ccdb093","repo":"laurent22/joplin","slug":"validation-error-k-cannot-contain-a-null-byte","errorCode":null,"errorMessage":"Validation error: ${k} cannot contain a null byte","messagePattern":"Validation error: (.+?) cannot contain a null byte","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/lib/BaseModel.ts","lineNumber":631,"sourceCode":"\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\n\t\t// case, the output won't be up-to-date and would cause for example display issues with out-dated\n\t\t// notes being displayed. This was an issue when notes were being synchronised while being decrypted\n\t\t// at the same time.\n\n\t\tconst mutexRelease = await this.saveMutex(o).acquire();\n\n\t\tlet output = null;\n\n\t\t// The try must cover everything after the mutex acquire: a throw from userSideValidation","sourceCodeStart":613,"sourceCodeEnd":649,"githubUrl":"https://github.com/laurent22/joplin/blob/2654b33620775080d1d59c552259d41e33dad3d2/packages/lib/BaseModel.ts#L613-L649","documentation":"Thrown by BaseModel.userSideValidation() which iterates every key on the entity object and rejects any string value containing a NUL byte (String.fromCharCode(0)). The comment explains null bytes break Joplin's serialised note format and can silently truncate content in some HTTP clients (React Native on iOS). The key name is interpolated so the offender is identifiable.","triggerScenarios":"Any string field on the saved object — title, body, author, source_url, etc. — contains \\u0000. Common when importing data that originated from binary/CLOB columns or copy-paste of binary content.","commonSituations":"Importing notes from a database whose TEXT column preserved NULs; clipboard data with embedded NULs; corrupted file read into a string field.","solutions":["Strip NUL bytes from string fields before save: value.replace(/\\0/g, '').","If NULs are meaningful (binary), do not store them in a string field — encode as base64 in a dedicated field instead.","Sanitise imported data at the trust boundary (the importer), not at every save site."],"exampleFix":"// before\nawait Note.save({ body: rawTextWithNuls });\n// after\nawait Note.save({ body: rawTextWithNuls.replace(/\\0/g, '') });","handlingStrategy":"validation","validationCode":"const NUL = String.fromCharCode(0);\nfor (const [k, v] of Object.entries(entity)) {\n  if (typeof v === 'string' && v.includes(NUL)) throw new Error(`Refusing to save: ${k} contains a NUL byte`);\n}","typeGuard":"const isFreeOfNul = (v) => typeof v !== 'string' || !v.includes(String.fromCharCode(0));","tryCatchPattern":"try { BaseModel.userSideValidation(entity); }\ncatch (e) { if (/cannot contain a null byte/.test(e.message)) { /* strip NULs from the named key */ } else throw e; }","preventionTips":["Sanitise all imported text once at the trust boundary: value.replace(/\\0/g, '').","Never store raw binary in string fields — encode as base64 in a dedicated column."],"tags":["validation","data-integrity","security","typescript","joplin-core"],"backgroundTag":null,"analyzedSha":"2654b33620775080d1d59c552259d41e33dad3d2","analyzedAt":"2026-08-12T14:26:46.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}