Guides / boolean vs string true false columns

JSON to CSV: Distinguish Boolean true/false From String "true"/"false"

When the same JSON field is the boolean true in one record and the string literal "true" in the next, a naive CSV exporter writes the identical cell true for both. An explicit boolean-type 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 active that is the JSON boolean true in R01 and the JSON string "true" in R02 has two different JSON values that produce the same CSV cell in a naive export. Do not write both into one active column. Instead, expand the field into four columns: active_kind ("boolean", "string_boolean_literal", or "string_other"), active_json_type ("boolean" or "string"), active_value (the textual representation), and active_json (the exact JSON serialization). This lets any CSV reader distinguish a JSON boolean from a string literal that happens to spell the same word, verify the original type, and reconstruct the source value exactly.

This guide demonstrates a measured six-record fixture exported locally on 2026-11-03 with Python 3.13.14. The source has three JSON boolean records (true, false, true), two string-boolean-literal records ("true", "false"), and one other string record ("maybe"). The explicit exporter writes six CSV rows with five columns; every field was read back and matched exactly (30/30 checks). A naive exporter that writes a single active column produces identical cells for booleans and their string lookalikes, making them indistinguishable.

The six-record fixture

The source array contains six records. The active field is the JSON boolean true in R01 and R05, the JSON boolean false in R03, the string "true" in R02, the string "false" in R04, and the string "maybe" in R06. This alternation is the core problem: JSON booleans and string literals that spell the same word produce identical CSV cells in a naive export.

[
  {"id": "R01", "active": true},
  {"id": "R02", "active": "true"},
  {"id": "R03", "active": false},
  {"id": "R04", "active": "false"},
  {"id": "R05", "active": true},
  {"id": "R06", "active": "maybe"}
]

Download the original JSON sample. Per RFC 8259 section 3, the JSON data model defines three literal name values: true, false and null. These are not strings: they are typed values with their own grammar. A JSON string "true" is a sequence of four Unicode characters, not the boolean true. CSV cells are always text, so both the boolean true and the string "true" become the four-character text true in a naive CSV cell. The exporter must carry the type distinction in companion columns.

What a naive exporter gets wrong

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

ProblemWhat happensMeasured result
R01 and R02 collapseBoolean true and string "true" both produce the cell trueReader cannot tell which was a typed boolean and which was a text literal
R03 and R04 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 lost4 of 6 records have their JSON type erased in the CSVDownstream filters like active=true match both booleans and string lookalikes
Reconstruction is ambiguousThe CSV cell true could mean true or "true"A reader reconstructing JSON cannot pick the correct type without guessing

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

id,active
R01,true
R02,true
R03,false
R04,false
R05,true
R06,maybe

This is particularly dangerous in data pipelines where a boolean field means "this feature is enabled" and a string "true" might mean "the user typed the word true into a free-text field". Treating both the same can cause incorrect feature flags, wrong audit trails, or mismatched records in downstream databases.

The explicit boolean-type column policy

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

SCHEMA = ["id", "active_kind", "active_json_type", "active_value", "active_json"]

The active_kind column classifies the value into one of three kinds: "boolean" (the value is a JSON boolean), "string_boolean_literal" (the value is a JSON string whose content is the word true or false), or "string_other" (the value is a JSON string with any other content). The active_json_type column records the JSON type name: "boolean" for JSON booleans, "string" for all string values. The active_value column contains the textual representation (true, false, maybe). The active_json column stores the exact JSON serialization: true for the boolean, "true" (with quotes) for the string literal.

The active_json column is the definitive source of truth. A downstream reader can parse it as JSON to recover the exact type and value. The active_kind and active_json_type columns provide a human-readable type label that a spreadsheet user can filter on without parsing JSON. For example, filtering active_kind=boolean returns only the three records that had JSON booleans; filtering active_kind=string_boolean_literal returns only the two records that had string lookalikes. 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-03 with Python 3.13.14. Source SHA-256: 7d719969…62a8 (210 bytes). Output SHA-256: 76c9a9fc…c186 (300 bytes). The CSV was read back with Python's csv.DictReader and every field compared against the parsed source.

RowIDSource valueactive_kindjson_typeactive_jsonRead-back
0R01truebooleanbooleantrue✓ all 5 fields
1R02"true"string_boolean_literalstring"true"✓ all 5 fields
2R03falsebooleanbooleanfalse✓ all 5 fields
3R04"false"string_boolean_literalstring"false"✓ all 5 fields
4R05truebooleanbooleantrue✓ all 5 fields
5R06"maybe"string_otherstring"maybe"✓ all 5 fields

All six rows passed all five checks (30/30). The CSV contains 6 data rows and 5 columns. The active_json column is the key differentiator: for R01 it is the unquoted token true (a JSON boolean), while for R02 it is the quoted string "true" (a JSON string). The CSV writer quotes this cell because it contains double-quote characters. A reader can parse the active_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", "active_kind", "active_json_type",
           "active_value", "active_json"]

def classify_value(val):
    if isinstance(val, bool):
        kind = "boolean"
        json_type = "boolean"
        value = "true" if val else "false"
    elif isinstance(val, str):
        if val in ("true", "false"):
            kind = "string_boolean_literal"
            json_type = "string"
            value = val
        else:
            kind = "string_other"
            json_type = "string"
            value = val
    elif val is None:
        kind = "null"
        json_type = "null"
        value = "null"
    elif isinstance(val, (int, float)):
        kind = "number"
        json_type = "number"
        value = json.dumps(val)
    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["active"]
        kind, json_type, value = classify_value(val)
        row["active_kind"] = kind
        row["active_json_type"] = json_type
        row["active_value"] = value
        row["active_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 boolean gets kind="boolean" and json_type="boolean". A JSON string "true" or "false" gets kind="string_boolean_literal" and json_type="string". Any other string gets kind="string_other". The active_json column always stores json.dumps(val), which produces true for the boolean and "true" for the string. 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, True would be misclassified as the number 1.

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 active_kind matches the expected kind, active_json_type matches the expected type, active_value matches the expected text, and active_json matches the JSON serialization. All six rows passed all five checks (30/30).

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["active"]
        exp_kind, exp_type, exp_value = classify_value(val)
        exp_json = json.dumps(val, ensure_ascii=False)
        assert csv_row["active_kind"] == exp_kind
        assert csv_row["active_json_type"] == exp_type
        assert csv_row["active_value"] == exp_value
        assert csv_row["active_json"] == exp_json

Why JSON boolean true is not the same as JSON string "true"

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 boolean true is a typed value representing logical truth. The string "true" is a sequence of four Unicode characters (U+0074, U+0072, U+0075, U+0065) that happens to spell the same word. In a JSON to CSV converter pipeline, conflating them can cause downstream systems to treat a free-text entry as a feature flag or vice versa. For example, a record with "active": true may mean "the account is programmatically active", while "active": "true" may mean "a human user typed the word true into a form field". The kind and type columns make that distinction travel with the data.

Distinct from the NaN and Infinity guide. The NaN rejection guide handles bare nonstandard constants (NaN, Infinity) that are not valid JSON at all. This guide handles valid JSON values—the boolean true and the string "true"—that are both legal but structurally different. The two guides cover non-overlapping problems.

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 boolean true from the string "true" for scalar fields. The structural distinction is analogous but the column policy is different: booleans need kind classification, objects need key counts.

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 boolean-vs-string-to-csv.py boolean-vs-string-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 just use the JSON serialization of the value as the CSV cell?

You can write json.dumps(active) into a single CSV cell, and that preserves the distinction: true stays true (the boolean) and "true" stays "true" (the string, with quotes). However, this approach pushes the burden of parsing JSON to every downstream reader, including spreadsheets and simple CSV tools that cannot parse JSON natively. The four-column policy provides typed, directly comparable fields (active_kind, active_json_type) that a spreadsheet user can filter and aggregate without parsing JSON, while the active_json column still carries the exact source value for reconstruction.