Termio
Blog

database / 2026-05-12 / 6 min read

The mysql2 JSON-column auto-parse gotcha

mysql2 may return JSON columns as JavaScript values already, so unconditional JSON.parse can corrupt reads silently.

JSON columns look like strings when you think in SQL, but your Node driver may hand them to you as JavaScript values. With mysql2, JSON columns can arrive already parsed. If your read path blindly callsJSON.parse, the failure can be both surprising and quiet.

The bug pattern

A write path stores a JSON array. The database row is correct. The API response returns null or an empty list. The read code looks harmless:

const [rows] = await conn.execute(
  "select image_paths from messages where id = ?",
  [id],
);

const imagePaths = JSON.parse(rows[0].image_paths);

If image_paths is already an array, JavaScript coerces it to a string before parsing. An array like ["a","b"]becomes "a,b", which is not valid JSON. If the code catches that error and returns a default, the real data disappears without a useful signal.

Parse by shape

The safe reader accepts both shapes. That makes code robust across driver settings, migrations, tests, and future refactors.

function readJsonArray(value: unknown): string[] {
  if (Array.isArray(value)) return value;
  if (value == null || value === "") return [];
  if (typeof value === "string") return JSON.parse(value);
  throw new TypeError("expected JSON array column");
}

For object columns, branch on typeof value === "object"and exclude null. The key is to treat database values as unknown at the boundary, not as whatever the last driver returned.

Do not swallow parse errors silently

A catch block that returns null can turn a one-line type mismatch into a multi-day product bug. At minimum, log the original value type and error. Better yet, fail loudly in development and test.

Audit after driver changes

When adopting mysql2 or changing driver options, grep forJSON.parse(row. and JSON.parse(record.. Every match should either have a shape guard or be reading a column that is guaranteed to be a string.

JSON columns are convenient because they preserve structure. Let the boundary code preserve that structure too.