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

  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.