A postal code can look like a number without being one. If an importer converts "00123" to 123, the leading zeros have already disappeared. Converting the result back to a string does not recover them. Canadian-shaped values such as "A1A 1A1" also contain letters and a significant space.
For an address form test, preserving the postal string is only half the job. The street, locality, region and postal field should stay attached to the same record. A valid-looking code independently shuffled into a different locality can make a fixture misleading.
This tutorial builds a small offline check for an address export with a JSON addresses array and a CSV ZIP column. It uses Python's standard library. The included fixtures are invented: their codes and locality names are not assertions about real assigned postal codes or deliverable addresses. No customer records or third-party database is needed.
1. Make the round trip reproducible
Use Python 3.10 or newer. Save the following as postal_roundtrip.py. Run it in a new directory so the two demonstration output files have clear ownership.
import csv
import io
import json
from pathlib import Path
def check(document, csv_text):
addresses = document["addresses"]
for index, address in enumerate(addresses, 1):
if not isinstance(address.get("zip"), str):
raise ValueError(f"Row {index}: postal code must be a string")
if not address.get("sourceId") or not address.get("sourceRecordUrl"):
raise ValueError(f"Row {index}: preserve the source reference")
rows = list(csv.DictReader(io.StringIO(csv_text.lstrip("\ufeff"), newline="")))
if len(rows) != len(addresses):
raise ValueError("Different row counts")
fields = {"Street": "street", "City": "city", "State": "state",
"ZIP": "zip", "Country": "country", "FullAddress": "sourceFullAddress"}
for index, (address, row) in enumerate(zip(addresses, rows), 1):
for column, key in fields.items():
if row.get(column) != address[key]:
raise ValueError(f"Row {index}: {column} differs")
return addresses
document = {
"schemaVersion": 1,
"fixtureNotice": "Invented parser fixtures, not postal reference data",
"addresses": [
{"street": "1 Fixture Street", "city": "Example locality", "state": "MA",
"zip": "00123", "country": "United States", "sourceId": "demo/us-1",
"sourceRecordUrl": "https://example.invalid/fixtures/us-1",
"sourceFullAddress": "1 Fixture Street\nExample locality MA 00123"},
{"street": '2, Rue "Demo"', "city": "Démo", "state": "QC",
"zip": "A1A 1A1", "country": "Canada", "sourceId": "demo/ca-1",
"sourceRecordUrl": "https://example.invalid/fixtures/ca-1",
"sourceFullAddress": '2, Rue "Demo"\nDémo QC A1A 1A1'},
],
}
json_text = json.dumps(document, ensure_ascii=False, indent=2)
parsed = json.loads(json_text)
buffer = io.StringIO(newline="")
writer = csv.writer(buffer)
writer.writerow(["Street", "City", "State", "ZIP", "Country", "FullAddress"])
for address in parsed["addresses"]:
writer.writerow([address[key] for key in
["street", "city", "state", "zip", "country", "sourceFullAddress"]])
csv_text = "\ufeff" + buffer.getvalue()
records = check(parsed, csv_text)
assert [row["zip"] for row in records] == ["00123", "A1A 1A1"]
assert parsed["addresses"][1]["sourceId"] == "demo/ca-1"
Path("fixture-export.json").write_text(json_text, encoding="utf-8")
with open("fixture-export.csv", "w", encoding="utf-8", newline="") as handle:
handle.write(csv_text)
print("2 records agree; postal strings: 00123, A1A 1A1")
# A number that has lost its zeros must fail rather than be silently padded.
broken = json.loads(json_text)
broken["addresses"][0]["zip"] = 123
try:
check(broken, csv_text)
except ValueError:
print("Numeric postal field rejected")
else:
raise AssertionError("The numeric postal field was accepted")
# A different locality/code pairing must also fail, even with equal row counts.
broken = json.loads(json_text)
broken["addresses"][0]["zip"] = "99999"
try:
check(broken, csv_text)
except ValueError:
print("Mismatched CSV/JSON pair rejected")
else:
raise AssertionError("The mismatched pair was accepted")
Run:
python postal_roundtrip.py
Expected output:
2 records agree; postal strings: 00123, A1A 1A1
Numeric postal field rejected
Mismatched CSV/JSON pair rejected
2. Notice what a CSV row actually is
The second street has both a comma and quotation marks. Its full address contains a newline. csv.writer quotes those fields and doubles the embedded quotes; csv.DictReader reconstructs the logical record. A physical line is not necessarily a complete record.
This is why line.split(",") and text.splitlines() are the wrong way to parse exported addresses. The CSV conventions for commas, quotes and line breaks are described in RFC 4180. Python documents the relevant reader/writer behavior and newline handling in its csv documentation.
For a downloaded file, open it with encoding="utf-8-sig" and newline="". That encoding accepts UTF-8 with or without the optional byte-order mark. The example removes the mark before constructing an in-memory reader.
3. Treat source references as part of the fixture
The JSON round trip retains sourceId and sourceRecordUrl. They are illustrative identifiers here, not real source claims. In a real dataset, keep the actual source ID, source record URL, snapshot/version and license credits together with the row.
A readable CSV attribution column may not contain every machine-readable field from JSON. Keeping an untouched copy of the original JSON envelope avoids silently dropping that information when you map fields into your application's fixture shape. Do not rewrite the source metadata to make the test output look cleaner.
When applying this check to real downloads, export both formats from one batch before regenerating. Adapt the mapping to that export's documented contract: zip, ZIP and postalCode are names from different schemas, not interchangeable fields. This short tutorial assumes simple region codes and does not implement international address-writing variants or spreadsheet formula-prefix guards.
4. Separate data preservation from address validation
This check proves agreement between two representations. It does not prove that either record is geographically correct, that a code is assigned, or that a parcel can be delivered. Two equally incorrect files can pass a comparison.
It also does not license someone else's database. Use your own illustrative fixtures, or data whose terms permit the intended use, and preserve the required credits. A sourced building address can be a real location even when its attached profile name is fictional.
If you inspect CSV in a spreadsheet, choose Text for postal columns during import. Quoting a field in CSV is syntax, not a guarantee that the spreadsheet will keep a numeric-looking value as text. Keep JSON strings as your programmatic representation; Python's JSON conversion table explains how strings and numbers become different Python types.
For a more complete implementation with additional regression tests, see the companion offline CSV/JSON fixture checker on GitHub. The repository is maintained as part of the same AddressSample project as this account; it is not an independent review or a postal validation service.
Disclosure: This tutorial was prepared with AI assistance. The example and its two failure cases were tested locally during preparation and editorial review. It does not describe a production incident or customer outcome.
Top comments (0)