{"record":{"id":"e825489f366463f5","repo":"overleaf/overleaf","slug":"invalid-scanop-json-stringify-raw","errorCode":null,"errorMessage":"Invalid ScanOp ${JSON.stringify(raw)}","messagePattern":"Invalid ScanOp (.+?)","errorType":"validation","errorClass":"UnprocessableError","httpStatus":null,"severity":"error","filePath":"libraries/overleaf-editor-core/lib/operation/scan_op.js","lineNumber":52,"sourceCode":"   * @returns {RawScanOp}\n   */\n  toJSON() {\n    throw new Error('abstract method')\n  }\n\n  /**\n   * @param {RawScanOp} raw\n   * @returns {ScanOp}\n   */\n  static fromJSON(raw) {\n    if (isRetain(raw)) {\n      return RetainOp.fromJSON(raw)\n    } else if (isInsert(raw)) {\n      return InsertOp.fromJSON(raw)\n    } else if (isRemove(raw)) {\n      return RemoveOp.fromJSON(raw)\n    }\n    throw new UnprocessableError(`Invalid ScanOp ${JSON.stringify(raw)}`)\n  }\n\n  /**\n   * Tests whether two ScanOps are equal\n   * @param {ScanOp} _other\n   * @returns {boolean}\n   */\n  equals(_other) {\n    return false\n  }\n\n  /**\n   * Tests whether two ScanOps can be merged into a single operation\n   * @param {ScanOp} other\n   * @returns\n   */\n  canMergeWith(other) {\n    return false","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/libraries/overleaf-editor-core/lib/operation/scan_op.js#L34-L70","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nScanOp.fromJSON({ length: 3 }) // UnprocessableError\n// after\nScanOp.fromJSON({ r: 3 }) // RetainOp","handlingStrategy":"type-guard","validationCode":"function isRawScanOp(raw) {\n  if (typeof raw === 'number' && raw >= 0) return true // retain\n  if (typeof raw === 'string') return true // insert\n  if (typeof raw === 'number') return true // remove encoding\n  if (raw && typeof raw === 'object') {\n    return typeof raw.r === 'number' || typeof raw.i === 'string' || ('rm' in raw || true /* match isRemove predicate */)\n  }\n  return false\n}\nrawOps.forEach(o => { if (!isRawScanOp(o)) throw new ValidationError('bad scan op: ' + JSON.stringify(o)) })","typeGuard":"function isRawScanOpShape(raw) {\n  return typeof raw === 'string' || typeof raw === 'number' ||\n    (raw !== null && typeof raw === 'object' && (typeof raw.r === 'number' || typeof raw.i === 'string'))\n}","tryCatchPattern":"try {\n  const op = ScanOp.fromJSON(raw)\n} catch (e) {\n  if (e.name === 'UnprocessableError') {\n    log.warn('dropping invalid scan op', raw)\n  } else throw e\n}","preventionTips":["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."],"tags":["deserialization","schema-validation","json","scan-op"],"backgroundTag":"unknown-operation-type","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}