overleaf/overleaf · error · UnprocessableError
Invalid ScanOp ${JSON.stringify(raw)}
Error message
Invalid ScanOp ${JSON.stringify(raw)} What it means
ScanOp.fromJSON converts a raw JSON value into a ScanOp (RetainOp, InsertOp, or RemoveOp) by testing it against isRetain/isInsert/isRemove predicates. A value that matches none of them cannot be any scan op, so the library throws an UnprocessableError including the offending JSON. This protects the text-operation application loop from corrupt or foreign input.
Source
Thrown at libraries/overleaf-editor-core/lib/operation/scan_op.js:52
* @returns {RawScanOp}
*/
toJSON() {
throw new Error('abstract method')
}
/**
* @param {RawScanOp} raw
* @returns {ScanOp}
*/
static fromJSON(raw) {
if (isRetain(raw)) {
return RetainOp.fromJSON(raw)
} else if (isInsert(raw)) {
return InsertOp.fromJSON(raw)
} else if (isRemove(raw)) {
return RemoveOp.fromJSON(raw)
}
throw new UnprocessableError(`Invalid ScanOp ${JSON.stringify(raw)}`)
}
/**
* Tests whether two ScanOps are equal
* @param {ScanOp} _other
* @returns {boolean}
*/
equals(_other) {
return false
}
/**
* Tests whether two ScanOps can be merged into a single operation
* @param {ScanOp} other
* @returns
*/
canMergeWith(other) {
return falseView on GitHub (pinned to 28ad3b03b7)
Solutions
- Check the offending value in the error message against the isRetain/isInsert/isRemove predicates in scan_op.js and correct the key names (i, r, and remove encoding).
- Validate the ops array with a schema validator before mapping each entry through ScanOp.fromJSON.
- Confirm the producer and consumer use the same library version / op encoding; migrate stored ops if the format changed.
- Catch UnprocessableError and skip or log the bad op instead of failing the entire op application.
Example fix
// before
ScanOp.fromJSON({ length: 3 }) // UnprocessableError
// after
ScanOp.fromJSON({ r: 3 }) // RetainOp Defensive patterns
Strategy: type-guard
Validate before calling
function isRawScanOp(raw) {
if (typeof raw === 'number' && raw >= 0) return true // retain
if (typeof raw === 'string') return true // insert
if (typeof raw === 'number') return true // remove encoding
if (raw && typeof raw === 'object') {
return typeof raw.r === 'number' || typeof raw.i === 'string' || ('rm' in raw || true /* match isRemove predicate */)
}
return false
}
rawOps.forEach(o => { if (!isRawScanOp(o)) throw new ValidationError('bad scan op: ' + JSON.stringify(o)) }) Type guard
function isRawScanOpShape(raw) {
return typeof raw === 'string' || typeof raw === 'number' ||
(raw !== null && typeof raw === 'object' && (typeof raw.r === 'number' || typeof raw.i === 'string'))
} Try / catch
try {
const op = ScanOp.fromJSON(raw)
} catch (e) {
if (e.name === 'UnprocessableError') {
log.warn('dropping invalid scan op', raw)
} else throw e
} Prevention
- Use the library's own serialization (op.toJSON) when persisting instead of hand-rolled formats.
- Match the exact raw key names: 'r' for retain, 'i' for insert, and the remove encoding.
- Round-trip test every op type you persist.
When it happens
Trigger: Calling ScanOp.fromJSON(raw) with e.g. `{}` (empty object), `{ x: 1 }`, a boolean, null, or a number that isn't wrapped as a retain (depending on the predicates), i.e. any value failing all three raw-op shape checks.
Common situations: Loading op documents saved by another library or version with a different scan-op encoding; typos in raw op keys ('r' vs 'retain', 'i' vs 'insert'); passing plain strings/numbers where structured ops are expected.
Related errors
- Unsupported operation in EditOperationBuilder.fromJSON
- invalid raw operation ${JSON.stringify(raw)}
- insert operation must have a string property
- retain operation must have a number property
- FileData: bad raw object ${JSON.stringify(raw)}
AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03).
Data as JSON: /api/errors/e825489f366463f5.
Report an issue: GitHub.