Skip to content

Formats — read one, write another

The question for this lesson: your data arrives as CSV, but the system downstream wants JSON. Or a partner sends a fixed-width mainframe extract, or an HL7 healthcare message. How does one pipeline deal with so many file formats?

The answer is small: a source or output node’s type: names its format, and everything between them is the same format-agnostic record model. So reading one format and writing another is a one-word change.

  • Change a pipeline’s input or output format by changing a node’s type:.
  • Name the formats Clinker reads and writes.
  • Avoid two format traps: CSV never infers types, and NDJSON is a json option rather than its own format.

A source’s type: is the format it reads; an output’s type: is the format it writes. Here a pipeline reads CSV and writes CSV. Run it, and read what comes out.

pipeline.yaml // editable

Now the format point, hands-on: in the runner above, change the output node’s type: csv to type: json and run again. Same rows, same pipeline, but the result comes out as JSON records instead of CSV lines. Nothing else changed, because the rows flowing between the nodes never depended on the format at either end.

🌱 New here? — format-agnostic records

Once a source reads a file, every row becomes the same internal record (named, typed cells, the schema from lesson 01). Transforms, routes, and aggregates work on that, never on raw bytes, which is exactly why swapping the input or output format doesn’t touch the rest of the pipeline.

Clinker reads and writes all of these; you select one with type::

type:FormatTypical use
csvcomma/delimited textthe everyday default
jsonJSON recordsAPIs, document stores (NDJSON is a json option)
xmlXML documentslegacy systems, configuration feeds
fixed_widthcolumns by character positionmainframe extracts (needs declared field widths)
x12, edifactEDI business documentsinvoices, purchase orders between companies
hl7HL7 messageshealthcare/clinical data

The EDI and HL7 formats are real and stable (with some charset/nesting limits) but specialized, so you’ll reach for them only in those industries. csv and json cover most day-to-day work. (There’s also a swift banking format, but it’s a preview, only message blocks 1–5, so treat it as not-yet-ready.)

Clinker’s real order_fulfillment pipeline shows the everyday case: one CSV source, and two outputs in different formats, a CSV of fulfilled orders and a JSON priority report:

- type: output
name: fulfilled_orders
input: route_priority
config: { name: fulfilled_orders, type: csv, path: ./output/fulfilled_orders.csv }
- type: output
name: priority_report
input: route_priority
config: { name: priority_report, type: json, path: ./output/priority_report.json }

Author the output stage yourself. Because records are format-agnostic once read, one source can feed several outputs in different formats, exactly what the order_fulfillment example does. Write two output nodes that read the same upstream node and emit the same rows as CSV and JSON:

- type: output
name: as_csv
input: customers
config: { name: as_csv, type: csv, path: ./output/customers.csv }
- type: output
name: as_json
input: customers
config: { name: as_json, type: json, path: ./output/customers.json }

Both read input: customers, so both receive every row; only the type: differs. The CSV lands as delimited rows; the JSON lands as an array of records:

[
{"status":"active","lifetime_value":"80000"},
{"status":"inactive","lifetime_value":"1200"},
{"status":"active","lifetime_value":"4000"}
]

Nothing upstream changes; the rows never depended on the format at either end. Verify with clinker run your_pipeline.yaml --dry-run (it resolves both output paths) and --explain.