laurent22/joplin · error · Error

Validation error: ${k} cannot contain a null byte

Error message

Validation error: ${k} cannot contain a null byte

What it means

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.

Source

Thrown at packages/lib/BaseModel.ts:631

		}

		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)) {
				throw new Error(`Validation error: ${k} cannot contain a null byte`);
			}
		}
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- o is any BaseItemEntity subclass being saved; subclasses override save() with stricter per-entity types
	public static async save(o: any, options: SaveOptions = null) {
		// When saving, there's a mutex per model ID. This is because the model returned from this function
		// is basically its input `o` (instead of being read from the database, for performance reasons).
		// This works well in general except if that model is saved simultaneously in two places. In that
		// case, the output won't be up-to-date and would cause for example display issues with out-dated
		// notes being displayed. This was an issue when notes were being synchronised while being decrypted
		// at the same time.

		const mutexRelease = await this.saveMutex(o).acquire();

		let output = null;

		// The try must cover everything after the mutex acquire: a throw from userSideValidation

View on GitHub (pinned to 2654b33620)

Solutions

  1. Strip NUL bytes from string fields before save: value.replace(/\0/g, '').
  2. If NULs are meaningful (binary), do not store them in a string field — encode as base64 in a dedicated field instead.
  3. Sanitise imported data at the trust boundary (the importer), not at every save site.

Example fix

// before
await Note.save({ body: rawTextWithNuls });
// after
await Note.save({ body: rawTextWithNuls.replace(/\0/g, '') });
Defensive patterns

Strategy: validation

Validate before calling

const NUL = String.fromCharCode(0);
for (const [k, v] of Object.entries(entity)) {
  if (typeof v === 'string' && v.includes(NUL)) throw new Error(`Refusing to save: ${k} contains a NUL byte`);
}

Type guard

const isFreeOfNul = (v) => typeof v !== 'string' || !v.includes(String.fromCharCode(0));

Try / catch

try { BaseModel.userSideValidation(entity); }
catch (e) { if (/cannot contain a null byte/.test(e.message)) { /* strip NULs from the named key */ } else throw e; }

Prevention

When it happens

Trigger: 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.

Common situations: Importing notes from a database whose TEXT column preserved NULs; clipboard data with embedded NULs; corrupted file read into a string field.

Related errors


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