Automattic/mongoose · error · CastError

Cast to Decimal128 failed for value "${value}" (type ${value

Error message

Cast to Decimal128 failed for value "${value}" (type ${valueType}) at path "${path}"

What it means

SchemaDecimal128.cast converts failures of the Decimal128 caster (lib/cast/decimal128.js) into this CastError. The caster accepts Decimal128 instances, strings via Decimal128.fromString (strict decimal-literal parsing), numbers, `{ $numberDecimal: '...' }` Extended JSON, and 16-byte Buffer/Uint8Array values. It rejects booleans, plain objects, arrays, and strings Decimal128.fromString cannot parse ('abc', '1,5', '$10.00').

Source

Thrown at lib/schema/decimal128.js:206

      return value;
    }

    return this._castRef(value, doc, init, options);
  }

  let castDecimal128;
  if (typeof this._castFunction === 'function') {
    castDecimal128 = this._castFunction;
  } else if (typeof this.constructor.cast === 'function') {
    castDecimal128 = this.constructor.cast();
  } else {
    castDecimal128 = SchemaDecimal128.cast();
  }

  try {
    return castDecimal128(value);
  } catch (error) {
    throw new CastError('Decimal128', value, this.path, error, this);
  }
};

/*!
 * ignore
 */

function handleSingle(val) {
  return this.cast(val);
}

const $conditionalHandlers = {
  ...SchemaType.prototype.$conditionalHandlers,
  $gt: handleSingle,
  $gte: handleSingle,
  $lt: handleSingle,
  $lte: handleSingle
};

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Sanitize to a plain decimal string before assigning: strip currency symbols/thousands separators, then validate against `/^[+-]?\d+(\.\d+)?$/`
  2. Assign `mongoose.Types.Decimal128.fromString(cleanValue)` yourself inside a try/catch at the boundary
  3. Pass numbers directly (`doc.price = 10.5`) when precision loss is acceptable
  4. If input formats vary wildly, register a custom caster with `mongoose.Schema.Types.Decimal128.cast(...)`

Example fix

// before
product.price = req.body.price; // '$1,234.56'

// after
const clean = String(req.body.price).replace(/[$,]/g, '');
if (!/^[+-]?\d+(\.\d+)?$/.test(clean)) throw new ValidationError('bad price');
product.price = mongoose.Types.Decimal128.fromString(clean);
Defensive patterns

Strategy: validation

Validate before calling

const DECIMAL_RE = /^[+-]?\d+(\.\d+)?$/;
function toDecimal128(v) {
  if (v == null) return v;
  const s = typeof v === 'object' && typeof v.$numberDecimal === 'string' ? v.$numberDecimal : String(v);
  const clean = s.replace(/[$,\s]/g, '');
  if (!DECIMAL_RE.test(clean)) throw new Error(`not a decimal: ${s}`);
  return mongoose.Types.Decimal128.fromString(clean);
}

Type guard

function isDecimalLike(v) {
  if (v == null || typeof v === 'number') return true;
  const s = typeof v === 'string' ? v : (v && v.$numberDecimal);
  return typeof s === 'string' && /^[+-]?\d+(\.\d+)?$/.test(s.replace(/[$,]/g, ''));
}

Try / catch

try { doc.price = value; } catch (err) { if (err.name === 'CastError' && err.kind === 'Decimal128') { return badRequest('price must be a decimal number'); } throw err; }

Prevention

When it happens

Trigger: `doc.price = 'abc'`, `doc.price = { value: '1.5' }` (object without $numberDecimal), `doc.price = '1.234,56'` (comma decimal), `doc.price = '$10.00'` (currency symbol), or passing an array. Query side: `{ price: { $gt: 'free' } }` via handleSingle.

Common situations: Money fields fed unsanitized user input; locale-formatted numbers from European formats; currency strings from scraping or spreadsheets; JSON where the decimal arrives inside a wrapper object from another serializer.

Related errors


AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21). Data as JSON: /api/errors/f12ee55a585daced. Report an issue: GitHub.