laurent22/joplin · error · Error

Validation error: user_updated_time and user_created_time mu

Error message

Validation error: user_updated_time and user_created_time must be numbers greater than 0

What it means

Thrown by BaseModel.userSideValidation() for the keys user_updated_time and user_created_time. Each must be a number, not NaN, and not negative. These timestamps drive sync conflict resolution, so corrupt values would corrupt replication. The check uses `k in o` so presence (not value) triggers validation.

Source

Thrown at packages/lib/BaseModel.ts:617

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

View on GitHub (pinned to 2654b33620)

Solutions

  1. Ensure both timestamps are millisecond epoch numbers (Number type) before save.
  2. Coerce with `Number(value)` and validate with `!isNaN(v) && v >= 0` upstream.
  3. If you do not have a real timestamp, omit the key entirely (the `k in o` check only fires when present).

Example fix

// before
await Folder.save({ id, user_updated_time: '2024-01-01T00:00:00Z' });
// after
await Folder.save({ id, user_updated_time: Date.parse('2024-01-01T00:00:00Z') });
Defensive patterns

Strategy: validation

Validate before calling

for (const k of ['user_updated_time', 'user_created_time']) {
  if (k in entity) {
    const v = entity[k];
    if (typeof v !== 'number' || isNaN(v) || v < 0) throw new Error(`${k} must be a non-negative number`);
  }
}

Type guard

const isValidTimestamp = (v) => typeof v === 'number' && !isNaN(v) && v >= 0;

Try / catch

try { BaseModel.userSideValidation(entity); }
catch (e) { if (/user_updated_time and user_created_time/.test(e.message)) { /* coerce or drop the key */ } else throw e; }

Prevention

When it happens

Trigger: o contains user_updated_time or user_created_time that is a string, NaN, undefined-coerced, or a negative number — e.g. parsing a timestamp from JSON without Number() conversion.

Common situations: Importing data where timestamps are ISO strings; arithmetic that produced NaN; clock-skew producing negative deltas; deserialised payload with stringified numbers.

Related errors


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