Home / Blog / Transliteration & conversion

Text and CSV conversion

Convert Structured Records to CSV with an Explicit Schema

Map structured records into CSV columns with explicit field names, types and missing-value rules, using a tested JSON-record export as an example.

Convert Structured Records to CSV with an Explicit Schema

Converting data to CSV starts with a table design: one record per row and a defined set of columns. Decide what each field means, how missing values are represented and which nested values need a separate table. A CSV writer can quote strings, but it cannot make those modeling decisions for you.

Specify the record shape

The worked example accepts a JSON array of records. Every record must contain exactly id, name and active. The first two fields are strings; active is a JSON boolean. The CSV output uses the same column order and writes the boolean as the text true or false.

An identifier such as 00123 is deliberately a string. Treating it as a number would discard its leading zeros before CSV conversion. A name may contain a comma, quotation mark or newline; csv.writer handles those characters according to CSV quoting rules.

Validate before opening the destination

import csv, json

def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError("Duplicate JSON member: " + key)
        result[key] = value
    return result

def reject_constant(value):
    raise ValueError("Not a JSON number: " + value)

with open("input.json", encoding="utf-8-sig") as source:
    data = json.load(source, object_pairs_hook=unique_object,
                     parse_constant=reject_constant)
if not isinstance(data, list):
    raise ValueError("Expected an array of records")
rows = [["id", "name", "active"]]
for record in data:
    if not isinstance(record, dict) or set(record) != {"id", "name", "active"}:
        raise ValueError("Each record needs exactly id, name and active")
    if not isinstance(record["id"], str) or not isinstance(record["name"], str):
        raise ValueError("id and name must be strings")
    if not isinstance(record["active"], bool):
        raise ValueError("active must be a boolean")
    rows.append([record["id"], record["name"],
                 "true" if record["active"] else "false"])

with open("output.csv", "w", encoding="utf-8", newline="") as target:
    csv.writer(target, lineterminator="\n").writerows(rows)

The duplicate-member check prevents a repeated JSON name from silently replacing an earlier value. The constant check rejects NaN and Infinity spellings that are outside standard JSON. Output is written only after every record meets the declared schema.

Distinguish missing, null and empty

A missing id is rejected. A null id is also rejected, because this contract requires a string. An empty string is accepted and becomes an empty CSV field. Those decisions are intentional; another receiving system may require a nonempty identifier and should add that rule.

CSV has no single universal null marker. An empty field, the text null and a missing column can mean different things to different importers. Agree on the destination’s rule before converting nullable data instead of assuming that one representation will work everywhere.

Do not flatten nested data by accident

The script rejects additional fields rather than silently dropping them. If a record has several addresses or a nested settings object, redesign the export. A separate related table may represent repeated items more clearly than placing an arbitrary object string into one cell.

If a receiver specifically expects JSON text inside a CSV field, serialize that nested value with a JSON serializer and let the CSV writer quote the resulting string. Document both layers. A hand-built comma-separated string is not a reliable replacement for either serializer.

Review the receiving types

The active column contains the literal strings true and false. The importing application must decide whether to interpret them as booleans. Similarly, CSV does not force a spreadsheet to retain 00123 as text. Use the destination’s import controls or schema to preserve that identifier.

Decimal quantities need their own precision and formatting policy. This example intentionally contains no decimal column. Extending it with prices or measurements requires deciding how exact values, units and missing entries are represented before adding another field to the writer.

Check records and rejected inputs

Read the generated CSV with a CSV reader and compare its three fields per record against the input under the documented boolean mapping. Include a comma in a name and a leading-zero identifier. A second record with active set to false should produce false, not a blank field or an omitted row.

Test a missing field, an extra field, an integer identifier, a null name, a string "true" in active and duplicate JSON members. Each should stop the conversion. Testing only a clean record would leave the most important schema decisions unverified.

References

The CSV module supplies the writer, and the JSON module documents parsing hooks. The application-specific schema above is an explicit example, not a universal CSV requirement.