laurent22/joplin · error · Error

Validation error: ID must a 32-characters lowercase…

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.

Solutions

  1. 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).
  2. If importing external data, normalize ids to lowercase and strip non-hex characters before save.
  3. 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

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


AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12). Data as JSON: /api/errors/9432ee89082a04c0. Report an issue: GitHub.

Appendix: 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)