laurent22/joplin · error · Error
Validation error: ID must a 32-characters lowercase hexadeci
Error message
Validation error: ID must a 32-characters lowercase hexadecimal string
What it means
Thrown by BaseModel.userSideValidation() when validating an entity before save. Joplin item IDs are 32-character lowercase hex strings (MD5-style). The regex `/^[a-f0-9]{32}$/` rejects anything with uppercase, wrong length, or non-hex characters. This guard runs for every BaseItem save path that calls userSideValidation.
Source
Thrown at packages/lib/BaseModel.ts:612
query = Database.insertQuery(this.tableName(), o);
} else {
const where = { id: o.id };
const temp = { ...o };
delete temp.id;
query = Database.updateQuery(this.tableName(), temp, where);
}
query.id = modelId;
query.modObject = o;
return query;
}
public static userSideValidation(o: Record<string, unknown>) {
if (typeof o.id === 'string' && !o.id.match(/^[a-f0-9]{32}$/)) {
throw new Error('Validation error: ID must a 32-characters lowercase hexadecimal string');
}
const timestamps = ['user_updated_time', 'user_created_time'] as const;
for (const k of timestamps) {
if ((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');
}
const maxTitleLength = 4096;
if (typeof o.title === 'string' && o.title.length > maxTitleLength) {
throw new Error(`Validation error: title must be ${maxTitleLength} characters or less`);
}
// Null bytes break Joplin's serialised note format and can cause silent
// truncation in some HTTP clients (notably React Native on iOS).
const nul = String.fromCharCode(0);
for (const k of Object.keys(o)) {
const v = o[k];
if (typeof v === 'string' && v.includes(nul)) {View on GitHub (pinned to 2654b33620)
Solutions
- Generate IDs using Joplin's own id utilities (e.g. md5 of content, or the shim's uuid helper that produces a 32-char lowercase hex).
- If importing external data, normalize ids to lowercase and strip non-hex characters before save.
- Confirm the id is exactly 32 chars with no dashes (unlike a canonical UUID).
Example fix
// before
await Note.save({ id: 'ABC123-4567', title: 'x' });
// after
import { uuid } from '@joplin/lib/uuid';
await Note.save({ id: uuid(), title: 'x' }); Defensive patterns
Strategy: validation
Validate before calling
const isValidId = (id) => typeof id === 'string' && /^[a-f0-9]{32}$/.test(id);
if (!isValidId(entity.id)) throw new Error('Refusing to save: id is not a 32-char lowercase hex'); Type guard
const isJoplinId = (id) => typeof id === 'string' && /^[a-f0-9]{32}$/.test(id); Try / catch
try { BaseModel.userSideValidation(entity); }
catch (e) { if (/ID must a 32-characters/.test(e.message)) { /* regenerate id with uuid() */ } else throw e; } Prevention
- Always generate IDs through Joplin's uuid helper rather than hand-rolling.
- In importers, normalise external IDs to lowercase 32-char hex (or generate fresh IDs and keep a mapping).
- Add a unit test fixture that asserts every saved entity passes userSideValidation.
When it happens
Trigger: userSideValidation(o) is called with o.id being a string that does not match /^[a-f0-9]{32}$/ — e.g. a UUID with dashes, an uppercase hex, a 31- or 33-char string, or a random string.
Common situations: External importer generating its own IDs; test fixture with a hand-written id; data migration that left malformed IDs; uppercase hex from another system.
Related errors
- Validation error: user_updated_time and user_created_time mu
- Validation error: title must be ${maxTitleLength} characters
- Validation error: ${k} cannot contain a null byte
- Cannot change encrypted item
- Cannot find "%s".
AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12).
Data as JSON: /api/errors/9432ee89082a04c0.
Report an issue: GitHub.