Guides / falsy value zero false string columns

JSON to CSV: Distinguish Number 0, Boolean false, and String "0"

When the same JSON field is the number 0 in one record, the boolean false in the next, the string "0" in a third, and the string "false" in a fourth, a naive CSV exporter writes identical or near-identical cells for all of them. An explicit falsy-value column policy records the kind, JSON type, value and original JSON serialization so a reader can tell them apart and reconstruct the exact source value.

Short answer

A field like flag that is the JSON number 0 in R01, the JSON boolean false in R02, the JSON string "0" in R03, and the JSON string "false" in R04 has four different JSON values that produce only two distinct CSV cells (0 and false) in a naive export. Do not write all four into one bare flag column. Instead, expand the field into four columns: flag_kind ("number_zero", "boolean_false", "string_zero", "string_false", or "string_other"), flag_json_type ("number", "boolean", or "string"), flag_value (the textual representation), and flag_json (the exact JSON serialization). This lets any CSV reader distinguish a JSON number from a JSON boolean from a JSON string that happens to spell the same text, verify the original type, and reconstruct the source value exactly.

This guide demonstrates a measured seven-record fixture exported locally on 2026-11-04 with Python 3.13.14. The source has two number-zero records (0), one boolean-false record (false), one string-zero record ("0"), two string-false records ("false"), and one other string record ("active"). The explicit exporter writes seven CSV rows with five columns; every field was read back and matched exactly (35/35 checks). A naive exporter that writes a single flag column produces identical cells for the number 0 and the string "0", and identical cells for the boolean false and the string "false", making four different JSON types collapse into two indistinguishable CSV cells.

The seven-record fixture

The source array contains seven records. The flag field is the JSON number 0 in R01 and R05, the JSON boolean false in R02, the JSON string "0" in R03, the JSON string "false" in R04 and R06, and the JSON string "active" in R07. This alternation is the core problem: JSON falsy values of different types produce identical CSV cells in a naive export.

[
  {"id": "R01", "flag": 0},
  {"id": "R02", "flag": false},
  {"id": "R03", "flag": "0"},
  {"id": "R04", "flag": "false"},
  {"id": "R05", "flag": 0},
  {"id": "R06", "flag": "false"},
  {"id": "R07", "flag": "active"}
]

Download the original JSON sample. Per RFC 8259 section 3, the JSON data model defines six structured types: object, array, number, string, boolean, and null. The number 0 is a numeric value representing zero. The boolean false is a typed value representing logical falsehood. The string "0" is a sequence of one Unicode character (U+0030) that happens to look like the digit zero. The string "false" is a sequence of five Unicode characters that happens to spell the same word as the boolean literal. CSV cells are always text, so the number 0, the boolean false, the string "0", and the string "false" become text cells that a naive exporter cannot distinguish by type.

What a naive exporter gets wrong

A naive exporter writes a single flag column. For the JSON number 0 it writes the text 0. For the JSON string "0" it also writes the text 0. For the JSON boolean false and the string "false" it writes the text false. This creates four measurable problems:

ProblemWhat happensMeasured result
R01 and R03 collapseNumber 0 and string "0" both produce the cell 0Reader cannot tell which was a typed number and which was a text literal
R02, R04 and R06 collapseBoolean false and string "false" both produce the cell falseReader cannot tell which was a typed boolean and which was a text literal
Type identity lost5 of 7 records have their JSON type erased in the CSVDownstream filters like flag=0 match both numbers and string lookalikes
Reconstruction is ambiguousThe CSV cell 0 could mean 0 or "0"; the cell false could mean false or "false"A reader reconstructing JSON cannot pick the correct type without guessing

The naive CSV output shows these problems concretely. R01 and R03 are both 0; R02, R04 and R06 are all false. A downstream system that filters flag=0 would match both R01 (the number) and R03 (the string), even though only R01 was a typed number in the source:

id,flag
R01,0
R02,false
R03,0
R04,false
R05,0
R06,false
R07,active

This is particularly dangerous in data pipelines where a numeric zero means "the counter is zero" and a boolean false means "the feature is disabled", while a string "0" might mean "a human user typed the digit 0 into a free-text field". Treating all three the same can cause incorrect aggregations, wrong feature flags, or mismatched records in downstream databases. A filtering operation WHERE flag = 0 would silently include string lookalikes; a filtering operation WHERE flag = false would silently include string lookalikes of the boolean.

The explicit falsy-value column policy

Instead of one bare column, expand the field into four columns that together carry the full type information:

SCHEMA = ["id", "flag_kind", "flag_json_type", "flag_value", "flag_json"]

The flag_kind column classifies the value into one of five kinds: "number_zero" (the value is the JSON number 0), "boolean_false" (the value is the JSON boolean false), "string_zero" (the value is the JSON string "0"), "string_false" (the value is the JSON string "false"), or "string_other" (the value is any other JSON string). The flag_json_type column records the JSON type name: "number" for numbers, "boolean" for booleans, "string" for all string values. The flag_value column contains the textual representation (0, false, active). The flag_json column stores the exact JSON serialization: 0 for the number, false for the boolean, "0" (with quotes) for the string.

The flag_json column is the definitive source of truth. A downstream reader can parse it as JSON to recover the exact type and value. The flag_kind and flag_json_type columns provide a human-readable type label that a spreadsheet user can filter on without parsing JSON. For example, filtering flag_kind=number_zero returns only the two records that had the JSON number 0; filtering flag_kind=boolean_false returns only the one record that had the JSON boolean false; filtering flag_kind=string_zero returns only the one record that had the string "0". Python's csv module documentation describes the DictWriter used to write the output; our code supplies the schema before iterating over the input.

Measured export results

The exporter was run locally on 2026-11-04 with Python 3.13.14. Source SHA-256: 1460c5e9…9825 (224 bytes). Output SHA-256: 1ccf7427…0c71 (310 bytes). The CSV was read back with Python's csv.DictReader and every field compared against the parsed source.

RowIDSource valueflag_kindjson_typeflag_jsonRead-back
0R010number_zeronumber0✓ all 5 fields
1R02falseboolean_falsebooleanfalse✓ all 5 fields
2R03"0"string_zerostring"0"✓ all 5 fields
3R04"false"string_falsestring"false"✓ all 5 fields
4R050number_zeronumber0✓ all 5 fields
5R06"false"string_falsestring"false"✓ all 5 fields
6R07"active"string_otherstring"active"✓ all 5 fields

All seven rows passed all five checks (35/35). The CSV contains 7 data rows and 5 columns. The flag_json column is the key differentiator: for R01 it is the unquoted token 0 (a JSON number), for R02 it is the unquoted token false (a JSON boolean), for R03 it is the quoted string "0" (a JSON string), and for R04 it is the quoted string "false" (a JSON string). The CSV writer quotes cells that contain double-quote characters. A reader can parse the flag_json column with json.loads to recover the exact type and value.

The exporter code

import json, csv, os
from pathlib import Path

SCHEMA = ["id", "flag_kind", "flag_json_type",
           "flag_value", "flag_json"]

def classify_value(val):
    if isinstance(val, bool):
        if val is False:
            kind = "boolean_false"
        else:
            kind = "boolean_true"
        json_type = "boolean"
        value = "true" if val else "false"
    elif isinstance(val, int):
        if val == 0:
            kind = "number_zero"
        else:
            kind = "number_nonzero"
        json_type = "number"
        value = json.dumps(val)
    elif isinstance(val, str):
        if val == "0":
            kind = "string_zero"
        elif val == "false":
            kind = "string_false"
        elif val == "true":
            kind = "string_true"
        else:
            kind = "string_other"
        json_type = "string"
        value = val
    elif val is None:
        kind = "null"
        json_type = "null"
        value = "null"
    else:
        kind = type(val).__name__
        json_type = type(val).__name__
        value = json.dumps(val, ensure_ascii=False)
    return kind, json_type, value

def export_explicit(source_path, csv_path, naive_path):
    with open(source_path, "r", encoding="utf-8") as f:
        raw_text = f.read()
    records = json.loads(raw_text)

    rows = []
    for rec in records:
        row = {col: "" for col in SCHEMA}
        row["id"] = rec["id"]
        val = rec["flag"]
        kind, json_type, value = classify_value(val)
        row["flag_kind"] = kind
        row["flag_json_type"] = json_type
        row["flag_value"] = value
        row["flag_json"] = json.dumps(val, ensure_ascii=False)
        rows.append(row)

    with open(csv_path, "x", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=SCHEMA)
        writer.writeheader()
        writer.writerows(rows)
    return rows, raw_text, records

The exporter classifies each value before writing. A JSON number 0 gets kind="number_zero" and json_type="number". A JSON boolean false gets kind="boolean_false" and json_type="boolean". A JSON string "0" gets kind="string_zero" and json_type="string". A JSON string "false" gets kind="string_false" and json_type="string". The flag_json column always stores json.dumps(val), which produces 0 for the number, false for the boolean, and "0" or "false" (with quotes) for the strings. The exporter uses open(csv_path, "x") for exclusive file creation, refusing to overwrite an existing output. Python's isinstance(val, bool) check comes before isinstance(val, int) because bool is a subclass of int in Python; without this ordering, False would be misclassified as the number 0, compounding the very problem this guide addresses.

Read-back verification

The verification step reads the CSV back with csv.DictReader and compares every column against the parsed source. For each row it checks that flag_kind matches the expected kind, flag_json_type matches the expected type, flag_value matches the expected text, and flag_json matches the JSON serialization. All seven rows passed all five checks (35/35).

def read_back_verify(csv_path, records):
    with open(csv_path, "r", encoding="utf-8", newline="") as f:
        reader = csv.DictReader(f)
        csv_rows = list(reader)

    for csv_row, src_rec in zip(csv_rows, records):
        val = src_rec["flag"]
        exp_kind, exp_type, exp_value = classify_value(val)
        exp_json = json.dumps(val, ensure_ascii=False)
        assert csv_row["flag_kind"] == exp_kind
        assert csv_row["flag_json_type"] == exp_type
        assert csv_row["flag_value"] == exp_value
        assert csv_row["flag_json"] == exp_json

Why JSON number 0 is not the same as JSON boolean false or string "0"

RFC 8259 section 3 defines the JSON data model with six structured types (object, array, number, string, boolean, null) and three literal name values: true, false and null. The number 0 is a numeric value that can participate in arithmetic: 0 + 1 = 1. The boolean false is a typed value representing logical falsehood: it can be used in conditional logic but not in arithmetic. The string "0" is a sequence of one Unicode character that happens to look like the digit zero but is text, not a number. The string "false" is a sequence of five Unicode characters that happens to spell the same word as the boolean literal but is text, not a boolean.

In a JSON to CSV converter pipeline, conflating these values can cause downstream systems to treat a text entry as a typed value or vice versa. For example, a record with "flag": 0 may mean "the counter value is zero", while "flag": false may mean "the feature is disabled", while "flag": "0" may mean "a human user typed the digit 0 into a form field", and "flag": "false" may mean "a human user typed the word false into a form field". The kind and type columns make that distinction travel with the data through the CSV handoff.

Distinct from the boolean vs string guide. The boolean vs string guide distinguishes the boolean true from the string "true". This guide adds the number 0 as a third falsy value that produces yet another collision: the number 0 and the string "0" both write 0 in a naive CSV cell. The boolean guide covers boolean-vs-string; this guide covers number-vs-boolean-vs-string for the specific case where all three spell falsy values that collapse in CSV.

Distinct from the null vs empty-object guide. The null vs empty object guide distinguishes null from {} for object-valued fields. This guide distinguishes the number 0 from the boolean false and from string lookalikes for scalar fields. The structural distinction is analogous but the column policy is different: falsy scalars need kind classification across three JSON types (number, boolean, string), while empty containers need state classification within object/array types.

Reproduce the local example

These are independent Python examples, not measurements of the browser converter. Download the input, exporter, saved CSV, naive CSV and execution evidence into one folder. Use Python 3.12 or later and a new output filename.

Original JSONPython exporterExplicit CSVNaive CSVExecution evidence
python falsy-value-to-csv.py falsy-value-fixture.json new-output.csv new-naive.csv new-evidence.json

The exporter reads at most 1,000,000 bytes and rejects inputs over that demonstration policy. That limit is a code policy, not a measured browser limit. The output uses UTF-8 and CRLF record endings. Exclusive file creation prevents overwriting an existing file. Faithful text conversion does not neutralize spreadsheet formulas, so use a suitable text import workflow for untrusted data.

Question: Can I filter falsy values in the CSV without the kind column?

You can filter flag_value=0 or flag_value=false in a single-column CSV, but that filter matches all JSON types that produce the same text. If you need to match only records where the flag was a typed number 0, you need the flag_kind column set to number_zero. If you need to match only records where the flag was a typed boolean false, you need flag_kind=boolean_false. Without the kind column, a filter for "falsy" values cannot distinguish a deliberate numeric zero from a user-typed text "0", or a programmatic boolean false from a user-typed text "false". The four-column policy provides the typed, directly comparable fields that a spreadsheet user can filter on without parsing JSON, while the flag_json column still carries the exact source value for reconstruction.