- Published on
Data Corruption from Bad Serialization — When Your Data Silently Changes
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Serialization bugs are insidious because they don't throw errors. The data goes in, comes out, looks approximately right — but is subtly wrong. A price stored as a float accumulates rounding errors. A BigInt truncated to a Number loses precision. A Date serialized without timezone info becomes the wrong time. These bugs corrupt production data, silently, for months before anyone notices.
- Bug 1: Floating Point Money
- Bug 2: BigInt/Large Integer Truncation in JSON
- Bug 3: Date Timezone Corruption
- Bug 4: JSON with Prototype Pollution or Loss
- Bug 5: NaN and Infinity in JSON
- Bug 6: Undefined Values Dropped from JSON
- Serialization Safety Checklist
- Conclusion
Bug 1: Floating Point Money
// ❌ Never store money as float — floating point arithmetic is imprecise
const price = 19.99
const tax = price * 0.1
console.log(tax) // → 1.9999999999999998 (not 2.00!)
const total = price + tax
console.log(total) // → 21.988999999999997 (not 21.99!)
// After storing and retrieving: the stored value is wrong
await db.query('UPDATE products SET price = $1', [price * 1.1])
// price is now 21.988999999999997... stored in the database
// ✅ Use integer cents (multiply by 100)
const priceInCents = 1999 // $19.99
const taxInCents = Math.round(priceInCents * 0.1) // 200 cents = $2.00
const totalInCents = priceInCents + taxInCents // 2199 cents = $21.99
// Display: divide by 100 only for display
const display = (totalInCents / 100).toFixed(2) // "21.99"
// Or use a decimal library
import Decimal from 'decimal.js'
const price = new Decimal('19.99')
const tax = price.mul('0.1')
console.log(tax.toString()) // "1.999" → Math.round to "2.00"
const total = price.add(tax.toDecimalPlaces(2, Decimal.ROUND_HALF_UP))
console.log(total.toString()) // "21.99" — exact
Bug 2: BigInt/Large Integer Truncation in JSON
JavaScript's JSON.stringify cannot serialize BigInt — it throws. But even worse, large integers that exceed Number.MAX_SAFE_INTEGER (9,007,199,254,740,991) lose precision silently:
// Database returns a BIGINT: 9007199254740993 (> MAX_SAFE_INTEGER)
const id = 9007199254740993n // bigint in JS
// ❌ JSON.stringify BigInt throws:
JSON.stringify({ id }) // TypeError: Do not know how to serialize a BigInt
// ❌ Converting to Number loses precision:
Number(9007199254740993n) // → 9007199254740992 (WRONG — last digit changed!)
// ✅ Serialize as string — no precision loss
JSON.stringify({ id: id.toString() }) // { "id": "9007199254740993" }
// In Express response:
res.json({ id: row.id.toString() })
// pg (node-postgres) returns BIGINT as string by default — use it!
const result = await db.query('SELECT id FROM users WHERE email = $1', [email])
const userId = result.rows[0].id // "9007199254740993" — already a string ✅
// Never do this:
const userId = parseInt(result.rows[0].id) // loses precision for large IDs!
Bug 3: Date Timezone Corruption
// ❌ Parsing date without timezone — depends on system locale
const date = new Date('2026-03-15')
// On UTC server: midnight UTC
// On EST server: midnight EST (= 5 AM UTC)
// If you store this in DB as UTC, it's wrong
// ❌ toLocaleDateString() — produces non-parseable string
const stored = new Date().toLocaleDateString() // "3/15/2026" — no timezone info!
// When you try to parse this back:
new Date('3/15/2026') // locale-dependent, might even fail
// ✅ Always use ISO 8601 UTC
const stored = new Date().toISOString() // "2026-03-15T14:00:00.000Z"
// Parseable, timezone-aware, sortable as a string
// ✅ Parse with explicit timezone context
import { parseISO } from 'date-fns'
import { fromZonedTime } from 'date-fns-tz'
// User input "2026-03-15 14:00" in America/New_York:
const utc = fromZonedTime('2026-03-15T14:00:00', 'America/New_York')
const stored = utc.toISOString() // Always stored as UTC
Bug 4: JSON with Prototype Pollution or Loss
// ❌ Storing a class instance as JSON loses methods and type info
class Price {
constructor(public cents: number, public currency: string) {}
format() { return `${this.currency} ${(this.cents / 100).toFixed(2)}` }
}
const price = new Price(1999, 'USD')
const stored = JSON.stringify(price) // '{"cents":1999,"currency":"USD"}'
const retrieved = JSON.parse(stored) // plain object, not a Price instance
retrieved.format() // TypeError: retrieved.format is not a function
// ✅ Use plain data objects for serialization, reconstruct on retrieval
const serialized = { cents: price.cents, currency: price.currency }
const retrieved = new Price(serialized.cents, serialized.currency)
retrieved.format() // works
Bug 5: NaN and Infinity in JSON
// ❌ JSON.stringify silently converts NaN and Infinity to null
const data = { score: NaN, ratio: Infinity, rate: -Infinity }
JSON.stringify(data) // '{"score":null,"ratio":null,"rate":null}'
// ❌ You store null, retrieve null, think the value was missing
// But it was NaN/Infinity from a division error
// ✅ Validate before storing
function sanitizeNumber(value: number, fieldName: string): number {
if (!isFinite(value)) {
throw new Error(`Invalid numeric value for ${fieldName}: ${value}`)
}
return value
}
const score = calculateScore() // might return NaN if input was bad
const safeScore = sanitizeNumber(score, 'score') // throws instead of silently corrupting
Bug 6: Undefined Values Dropped from JSON
// ❌ JSON.stringify drops undefined properties
const user = {
id: '123',
name: 'Sanjeev',
middleName: undefined, // user has no middle name
}
JSON.stringify(user)
// '{"id":"123","name":"Sanjeev"}'
// middleName is completely gone — when you parse this, there's no way to distinguish
// "no middle name" from "middle name not loaded from DB"
// ✅ Use null explicitly for "no value"
const user = {
id: '123',
name: 'Sanjeev',
middleName: null, // explicit null
}
JSON.stringify(user)
// '{"id":"123","name":"Sanjeev","middleName":null}'
// Now you know the field exists and is empty
Serialization Safety Checklist
- ✅ Never store money as float — use integer cents or
decimal.js - ✅ Serialize large integers as strings — never convert BIGINT to Number
- ✅ Always store dates as ISO 8601 UTC (
toISOString()) - ✅ Validate for
NaN/Infinitybefore JSON serialization - ✅ Use
nullinstead ofundefinedfor "no value" in JSON - ✅ Reconstruct class instances after JSON.parse — don't call methods on plain objects
- ✅ Use
decimal.jsorbig.jsfor any arithmetic on decimal numbers
Conclusion
Serialization bugs are silent — they produce wrong data with no error message. The most damaging are floating-point money calculations (use cents), BigInt truncation (serialize as strings), and timezone-unaware date parsing (always use ISO 8601). These aren't obscure edge cases: they happen routinely in any app that handles money, large IDs, or dates. The fix for each is a simple rule applied consistently: money in cents, IDs as strings, dates in UTC ISO 8601, and null instead of undefined.