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
- Ensure both timestamps are millisecond epoch numbers (Number type) before save.
- Coerce with `Number(value)` and validate with `!isNaN(v) && v >= 0` upstream.
- 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
- Always store timestamps as millisecond epoch numbers, never ISO strings.
- If a timestamp is unknown, omit the key rather than passing 0/-1/string.
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
- Validation error: ID must a 32-characters lowercase hexadeci
- 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/48da8c9103fdf107.
Report an issue: GitHub.