Skip to content

Mixed-Format Bridge (CSV batch + JSON batch → one dataset)

View on GitHub

What

Combine records that arrive in two different file formats — an old CSV batch and a new JSON batch of the same kind of data — into one unified table.

Synthetic / teaching example

The data here is constructed, not sourced. legacy.csv (US dates MM/DD/YYYY, US-grouped amounts "1,250.50") and modern.in.json (different key names, ISO dates). The problem class — a feed that migrated from CSV to JSON while the old files still matter — is universal; the rows are not.

Why interesting

A template reads one input format (CSV or JSON), so a mixed-format pile can't be done in a single pass. The fix is one pass per format into the same output_schema, then a fan-in pass to stack the normalised results — bridging formats without leaving bxp or hand-writing a merge script.

flowchart TD
    C["legacy.csv<br/><small>US MM/DD/YYYY · &quot;1,250.50&quot;</small>"] --> B1["bridge_csv<br/><small>→ id,date,amount</small>"]
    J["modern.in.json<br/><small>order_id/ts/total · ISO</small>"] --> B2["bridge_json<br/><small>→ id,date,amount</small>"]
    B1 --> U1["*.unified.csv"]
    B2 --> U2["*.unified.csv"]
    U1 --> CB["combine_unified<br/><small>combined_output</small>"]
    U2 --> CB
    CB --> R["1-combine_unified-combined.csvx"]

Problem class documented in. (sources for the problem class — not for the data)

  • Feed/API format migrations (CSV→JSON, v1→v2) are routine; historical batches in the old format have to be bridged to the new pipeline schema — a standard data-engineering backfill task.

The trick — one pass per format, then a fan-in

Three templates, run all at once with bxp-cli --config ./sample.json. Each writes a real file, and each is pinned by a golden, so the three snippets below are exactly what the run produces. Each pass uses a distinct file_pattern_in, which is what stops the three from picking up one another's output.

Pass 1 · bridge_csv — the old CSV batch

Reads legacy.csv and normalises it to the target schema: US MM/DD/YYYY becomes ISO, and the , thousands separator is stripped so the amount is a real number.

legacy.unified.csv
id,date,amount
A-1,2024-03-15,1250.5
A-2,2024-03-16,980

Pass 2 · bridge_json — the new JSON batch

Reads modern.in.json, whose keys are named differently (order_id / ts / total) and whose dates are already ISO. Different input, different work — but the identical output_schema:

modern.unified.csv
id,date,amount
B-1,2024-03-20,540
B-2,2024-03-21,3120.75

Put those two side by side and the bridge is done: two formats, one shape. A template reads one input format, so this is the step that could not have been a single pass.

Pass 3 · combine_unified — stack them

combined_output: true over *.unified.csv concatenates the two normalised batches into one file, header written once. That is Final result below.

Final result

A CSV batch and a JSON batch, different dates and number styles, land as one consistent dataset:

id,date,amount
A-1,2024-03-15,1250.5
A-2,2024-03-16,980
B-1,2024-03-20,540
B-2,2024-03-21,3120.75

Sample data

Run it with bxp-cli --config ./sample.json — the two inputs and the full commented template:

{
  // Teaching example — synthetic data. The same kind of records arrive in two
  // formats: an old CSV batch (legacy.csv — US dates, US-grouped numbers) and a
  // new JSON batch (modern.in.json — different key names, ISO dates). bxp reads
  // one FORMAT per template, so each format gets its own pass into the SAME
  // output_schema; a final combine pass fans the two normalised files into one
  // dataset. Run the whole pipeline with:  bxp-cli --config ./sample.json
  //
  // Glob safety: each template's file_pattern_in is chosen so the three passes
  // never pick up each other's files (`legacy.csv` vs `.in.json` vs `.unified.csv`).
  conversion_templates: {

    // PASS 1 — CSV batch -> unified schema.
    bridge_csv: {
      data_dir:          ".",
      file_pattern_in:   "legacy.csv",
      file_pattern_out:  "legacy.unified.csv",
      input_schema: {
        $id:     "[id]",
        $date:   "DATE_CONVERT([date], 'MM/DD/YYYY', 'YYYY-MM-DD')",
        $amount: "REPLACE([amount], ',', '') * 1"
      },
      row_rules: [ { when: "1 = 1", rows: [ {} ] } ],
      output_schema: { id: "$id", date: "$date", amount: "$amount" }
    },

    // PASS 2 — JSON batch -> the IDENTICAL unified schema (different key names).
    bridge_json: {
      data_dir:          ".",
      file_type_in:      "json",
      file_pattern_in:   ".in.json",
      file_type_out:     "csv",
      file_pattern_out:  ".unified.csv",
      input_schema: {
        $id:     "[order_id]",
        $date:   "[ts]",
        $amount: "[total] * 1"
      },
      row_rules: [ { when: "1 = 1", rows: [ {} ] } ],
      output_schema: { id: "$id", date: "$date", amount: "$amount" }
    },

    // PASS 3 — fan the two normalised files into ONE dataset.
    combine_unified: {
      data_dir:          ".",
      file_pattern_in:   ".unified.csv",
      file_pattern_out:  ".csvx",
      combined_output:   true,
      input_schema: {
        $id:     "[id]",
        $date:   "[date]",
        $amount: "[amount]"
      },
      row_rules: [ { when: "1 = 1", rows: [ {} ] } ],
      output_schema: { id: "$id", date: "$date", amount: "$amount" }
    }
  }
}
id,date,amount
A-1,03/15/2024,"1,250.50"
A-2,03/16/2024,"980.00"
[
  {"order_id":"B-1","ts":"2024-03-20","total":"540.00"},
  {"order_id":"B-2","ts":"2024-03-21","total":"3120.75"}
]
id,date,amount
A-1,2024-03-15,1250.5
A-2,2024-03-16,980
B-1,2024-03-20,540
B-2,2024-03-21,3120.75