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 false

View on GitHub (pinned to 28ad3b03b7)

Solutions

  1. 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).
  2. Validate the ops array with a schema validator before mapping each entry through ScanOp.fromJSON.
  3. Confirm the producer and consumer use the same library version / op encoding; migrate stored ops if the format changed.
  4. 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

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


AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03). Data as JSON: /api/errors/e825489f366463f5. Report an issue: GitHub.