Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tabular-convert

tests

Convert between CSV and JSON without silently losing structure.

Single file, standard library only, no dependencies. Tested in CI on Python 3.9 through 3.13.

Why

Most CSV/JSON converters break on the same things: nested objects get stringified or dropped, ragged records lose columns, 007 becomes 7, and a field that is absent comes back as null. This one is built so that json2csv followed by csv2json --infer --nest gives you back what you started with.

Install

curl -O https://raw.githubusercontent.com/clawedassistant26/tabular-convert/main/tabular_convert.py
chmod +x tabular_convert.py

Usage

# CSV -> JSON
./tabular_convert.py csv2json input.csv -o out.json --infer --nest

# JSON -> CSV
./tabular_convert.py json2csv input.json -o out.csv

# Use - for stdin/stdout, so it pipes
cat in.csv | ./tabular_convert.py csv2json - -o - --infer

csv2json options

Flag Effect
--infer Turn numeric, boolean and null-looking cells into real JSON types
--nest Expand dotted column names (user.name) into nested objects
--string-columns a,b Exempt these columns from --infer
--delimiter ';' Input delimiter, default ,
--indent N Output indent, 0 for compact

json2csv options

Flag Effect
--delimiter ';' Output delimiter, default ,

What it handles

Nested objects and arrays flatten to dotted keys and rebuild exactly:

[{"user": {"name": "ann", "roles": ["admin", "dev"]}}]
user.name,user.roles.0,user.roles.1
ann,admin,dev

Ragged records share a union header, in first-seen column order, so no record loses a field.

Absent vs null are kept distinct. An empty cell means the record has no such field. An explicit JSON null is written as the literal null. This is the difference between {"a": 1} and {"a": 1, "b": null}, and both survive a round trip.

Empty containers survive. {} and [] have no leaf values to write, so they are encoded as those literals rather than vanishing.

Booleans and nulls use JSON spelling (true, false, null), not Python's True/False/None, so other tools can read the CSV.

Conservative type inference. --infer deliberately leaves anything ambiguous as a string: leading zeros (007), a leading +, underscores (1_000), surrounding whitespace, and nan/inf (which are not valid JSON). Zip codes and phone numbers stay intact.

Sparse list indices stay objects. Columns c.0 and c.2 with no c.1 become {"0": ..., "2": ...}, not a list with an invented middle element.

Real CSV quoting via the standard library: embedded commas, quotes, newlines and unicode all work.

Known limits

These are properties of CSV, not bugs, and it is better to know them up front.

  • --infer cannot recover the difference between the string "90210" and the number 90210. CSV carries no type information, so a numeric-looking string comes back as a number. Use --string-columns zip,phone to protect specific columns, or omit --infer to keep everything as strings.
  • Likewise for the literal strings "true", "false" and "null", which --infer reads as the corresponding JSON values.
  • A JSON key containing a literal dot collides with the nesting separator under --nest. Without --nest it is passed through untouched.
  • json2csv holds the parsed input in memory, because the whole array must be seen before the union header is known. csv2json reads lazily but buffers the record list to emit one valid JSON array. Both handle 200k rows / ~13MB comfortably; for much larger data use JSON Lines instead.
  • Top-level JSON must be an array of objects. Anything else is a clear error, not a guess.

Errors

Bad input exits 2 with a one-line message and no traceback. Detected cases include: duplicate header names, empty header names, a row with more fields than the header, invalid JSON, a non-array top level, a non-object array element, a --string-columns name that is not in the header, and a column that structurally conflicts with its own nested columns.

Tests

python3 -m pytest tests/ -q

64 tests covering type inference, flatten/nest round trips, CSV quoting edge cases, ragged records, absent-vs-null, error paths, and the CLI end to end via subprocess.

Licence

MIT

About

CSV <-> JSON converter that survives nested objects, ragged records and absent-vs-null. Stdlib only.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages