PoppyOS

Home › Guides › How to convert JSON to CSV without rounding long IDs

How to convert JSON to CSV without rounding long IDs

You convert a JSON export to CSV, open it, and an order ID that ended in ...993 now ends in ...992. Or a price written as 1.10 comes out as 1.1. Nothing warned you. The converter did what most software does with a number: it turned it into a 64-bit float and wrote that back out.

Why long IDs change

A 64-bit float holds integers exactly only up to 9,007,199,254,740,991 (253 - 1, the value JavaScript calls Number.MAX_SAFE_INTEGER). Above that, some whole numbers do not exist, so the nearest one is stored. JavaScript's JSON.parse works this way, and so does any converter built on it. The JSON file itself is not at fault: JSON only says a number is a run of digits. The damage happens when a program decides to store that run of digits as a float.

The same step explains the other symptom. 1.10, 2.50 and 1E5 are all valid JSON numbers, but a float has no memory of how it was written, so they come back as 1.1, 2.5 and 100000.0. The value is the same and the text is not, which matters when the text is a SKU, a version number or an amount that a later step compares as a string.

print(float(9007199254740993))
print(float(12345678901234567890))
print(repr(float("1.10")), repr(float("1E5")))
9007199254740992.0
1.2345678901234567e+19
1.1 100000.0

Those are the values a float-based tool would carry forward. The first two lines are the long IDs and the last line shows what happens to the written form of a decimal and an exponent.

Check which numbers in your file would change

Python's json module can hand you every number as the exact text from the file, so you can ask which ones a float would damage. This script does that for one file's worth of text:

import json

text = """[
 {"id": 12345678901234567890, "order": 9007199254740993, "price": 1.10, "qty": 1E5, "sku": "007", "ship": {"city": "Leeds"}},
 {"id": 12345678901234567891, "order": 9007199254740995, "price": 2.50, "qty": 3, "sku": "008", "ship": {"city": "Hull"}}
]"""

risky = []
def check_int(s):
    if int(float(s)) != int(s):
        risky.append(s)
    return s
def check_float(s):
    if repr(float(s)) != s:
        risky.append(s)
    return s

json.loads(text, parse_int=check_int, parse_float=check_float)
print(len(risky), "number(s) a float would change:")
for s in risky:
    print(" ", s, "->", repr(float(s)) if "." in s or "E" in s.upper() else int(float(s)))
7 number(s) a float would change:
  12345678901234567890 -> 12345678901234567168
  9007199254740993 -> 9007199254740992
  1.10 -> 1.1
  1E5 -> 100000.0
  12345678901234567891 -> 12345678901234567168
  9007199254740995 -> 9007199254740996
  2.50 -> 2.5

Strings such as "007" are not on the list. They are text in JSON and stay text, which is why they are safe. If the list is empty for your file, any converter will do.

Convert it without touching the digits

Python can do the whole job if you tell it not to build numbers at all. parse_int=str and parse_float=str keep each number as the text it had in the file, and the csv module writes text as text. This script also flattens nested objects into columns such as ship.city and joins a list into one cell with semicolons. It handles a JSON array of objects, not every shape JSON can take.

import csv, io, json

text = """[
 {"id": 12345678901234567890, "order": 9007199254740993, "price": 1.10, "qty": 1E5, "sku": "007", "ship": {"city": "Leeds"}, "tags": ["a", "b"], "paid": true},
 {"id": 12345678901234567891, "order": 9007199254740995, "price": 2.50, "qty": 3, "sku": "008", "ship": {"city": "Hull"}, "tags": [], "paid": null}
]"""

def cell(v):
    if v is None:
        return ""
    if isinstance(v, bool):
        return "true" if v else "false"
    if isinstance(v, list):
        return "; ".join(cell(x) for x in v)
    return v

def flat(obj, prefix=""):
    for k, v in obj.items():
        if isinstance(v, dict):
            yield from flat(v, prefix + k + ".")
        else:
            yield prefix + k, cell(v)

records = json.loads(text, parse_int=str, parse_float=str)
rows = [dict(flat(r)) for r in records]
columns = list(dict.fromkeys(k for r in rows for k in r))
out = io.StringIO()
writer = csv.DictWriter(out, fieldnames=columns, lineterminator="\n")
writer.writeheader()
writer.writerows(rows)
print(out.getvalue())
id,order,price,qty,sku,ship.city,tags,paid
12345678901234567890,9007199254740993,1.10,1E5,007,Leeds,a; b,true
12345678901234567891,9007199254740995,2.50,3,008,Hull,,

Every ID, price and exponent is the same text that went in. To write the result to a file, replace the print with open("out.csv", "w", newline="", encoding="utf-8-sig").write(out.getvalue()). The utf-8-sig part adds the marker Excel needs to read accented letters correctly (see why accented letters look garbled).

The second place the digits can break: Excel

A correct CSV can still lose digits when you double-click it. Excel keeps 15 significant digits (Microsoft's specifications page), so a 17 or 20 digit ID is rounded the moment Excel treats the column as numbers, and saving makes it permanent. Import the file and mark the ID columns as text instead. The steps are in the leading-zeros guide; we have not run them on this page's output in Excel itself.

When a script is not enough

The script above covers a flat list of records. It does not repair broken JSON, handle JSON Lines, let you pick a list nested inside an object, or offer other ways to write arrays, and a very large file will want streaming rather than reading everything at once. If you do this often and would rather have a window to drop the file into, the converter below keeps numbers exactly as written in both directions. Everything above works without it.

Last updated 2026-11-01. Python on this page was run on that date and the output shown is the real output. Excel behaviour is from Microsoft's documentation; we have not run it in Excel.

More guides

All guides