How to Convert a FHIR Bundle to CSV Without Losing Data
Hl7 Tools

How to Convert a FHIR Bundle to CSV Without Losing Data

Why "Just Export It to CSV" Is Harder Than It Sounds

Someone hands you a FHIR Bundle and asks for a spreadsheet. It sounds like a five-minute job. Then you open the file and find a JSON document with an entry array holding forty resources of nine different types, each one a nested tree with arrays inside objects inside arrays. There is no obvious table in there โ€” and that is the whole problem. FHIR is a graph of typed, nested resources; CSV is a flat rectangle of rows and columns. Converting between them is not a format change, it is a modelling decision, and the quality of your spreadsheet depends entirely on the decisions you make along the way.

This guide walks through those decisions in the order you will actually face them: splitting the bundle by resource type, choosing how to handle repeating values, picking columns, and then opening the result in Excel without silently corrupting it. You can follow along in the FHIR Bundle Explorer, which does the flattening in your browser and never uploads the file. If you are not yet clear on what a Bundle is and what its entries contain, start with What Is a FHIR Bundle? and come back.

Decision One: One Table Per Resource Type

The first instinct โ€” one row per bundle entry โ€” produces a useless table. A Patient has birthDate and gender; an Observation has valueQuantity and effectiveDateTime; a MedicationRequest has intent and dosageInstruction. Put them in one sheet and you get a sparse grid where every row uses a different tenth of the columns, and any formula you write has to filter by type before it can do arithmetic.

The workable model is one table per resourceType. A bundle containing 12 Patients, 340 Observations and 28 Conditions becomes three tables with three column sets, and each one is immediately analysable: you can average the Observations, count the Conditions, and join them back to Patients on the subject reference. This is why the Bundle Explorer starts by tallying resources per type and lets you pick the type before it draws a table โ€” the type is the schema.

This also means a single bundle usually yields several CSVs, not one. That is not a limitation of the tooling; it is the honest shape of the data. Resist the temptation to force a merge at export time. Export the tables separately and join them afterwards in whatever tool is going to do the analysis, where you can control the join semantics.

Decision Two: What Happens to Repeating Values

This is where most naive converters lose data. FHIR is full of repeating elements. A Patient can have several name entries and each name several given parts. An Observation can carry multiple identifier values, multiple performer references, multiple category codings. A CSV cell holds one string. Something has to give.

The first step is deciding what a "column" even means when arrays are involved. If you index every position โ€” name[0].given[0], name[1].given[0], name[1].given[1] โ€” you get a table whose column set changes depending on which patient happened to have the most middle names. That is unusable: adding one record widens the sheet. The alternative, and the approach the Bundle Explorer takes, is to collapse array indices out of the path. Both name[0].given[0] and name[1].given[1] become the single path name.given. Every value that belongs to the same conceptual field lands in the same column, and the column set is stable no matter how many repeats any record has.

Once the paths are collapsed, one column can hold several values for one resource, and you choose how to render them.

Join Mode: One Row per Resource

In join mode, a resource always produces exactly one row, and repeated values in a cell are concatenated with a semicolon. A patient with given names "Maria" and "Elena" gets Maria;Elena in the given column. Nothing is discarded, the row count matches the resource count, and =COUNTA() over the id column tells you how many patients you have.

Use join mode when the spreadsheet is for reading, reconciliation, or counting records โ€” the everyday "how many, and which ones" questions. Its weakness is arithmetic: a cell reading 72;68;75 is text, not three numbers, so you cannot average it without splitting it back apart.

Expand Mode: One Row per Repeat

In expand mode, a resource with repeating values produces several rows โ€” as many as the widest multi-valued column it has. Each row takes the i-th value of every multi-valued column, and single-valued columns are repeated down all of them, so every row remains self-describing: the patient id, the effective date and the status still appear on each expanded row rather than leaving blanks under a merged-looking header.

Use expand mode when the numbers have to be numbers โ€” when you are going to pivot, chart, average, or feed the file into a statistics package that expects tidy data with one observation per row. Its cost is that the row count no longer equals the resource count, so counting records means counting distinct ids instead of counting rows. Neither mode discards data; they trade a packed cell against a multiplied row, and knowing which trade you want before you export saves a re-run.

How to Convert a FHIR Bundle to CSV Without Losing Data

Decision Three: Choosing Columns

A single Observation can expose several dozen leaf paths once you walk the whole tree. Exporting all of them gives you a sheet you have to scroll sideways through for a minute before finding the value column, and most of those paths will be empty for most rows. Column choice is therefore a real editorial act, not a default.

Three strategies cover almost every case. Curated presets ship for the resource types that dominate real bundles โ€” Patient, Observation, Condition, Encounter, MedicationRequest, Procedure, AllergyIntolerance, Immunization and DiagnosticReport โ€” and give you the fields a human actually wants: for Observation that is id, status, code and display, the quantity value and unit, the effective date and the subject reference. Presets are filtered against your actual data, so a bundle whose Patients carry no address does not show you an empty address column.

For any resource type without a curated preset, columns are derived from the data: every leaf path is counted across the resources of that type and the twelve most populated paths become the columns. This is a deliberately simple heuristic, and it works because the fields that matter are the fields most records fill in. Finally, show all columns is there for the archival export where you would rather have everything and hide columns later.

A few things are excluded from flattening on purpose, and you should know why so you are not surprised by their absence. meta holds server bookkeeping โ€” version ids and last-updated stamps โ€” that says nothing about the patient. text is the human-readable narrative, an XHTML blob that would blow out a cell and duplicate structured data you already have in columns. extension is arbitrary, profile-specific and irregularly shaped, so including it produces a long tail of columns populated for a handful of rows. If you need any of those, treat the JSON as the source of truth rather than forcing them into a table.

Opening the CSV in Excel Without Wrecking It

A correct CSV can still be mangled by the spreadsheet that opens it. This is where healthcare data suffers more than most, because so many of its fields look like something else to an autoformatter.

The first hazard is formula injection. Spreadsheet applications treat a cell beginning with =, +, - or @ as a formula to evaluate rather than text to display. FHIR string fields are user-influenced โ€” a note, a display name, a free-text address line โ€” so a value beginning with one of those characters can execute on open. The Bundle Explorer neutralises this by prefixing such cells with a tab character before quoting, which keeps the text readable while stopping evaluation. If you build your own exporter, do the same; it is a genuine security control, not cosmetic.

The second hazard is autoformatting. A medical record number like 0012345 loses its leading zeros. A long numeric identifier becomes scientific notation. A code that looks like a date โ€” and clinical code systems are full of things that look like dates โ€” gets silently rewritten. The robust habit is not to double-click the file at all. Open Excel first, then use Data โ†’ From Text/CSV, and in the import preview set the column data type to Text for every identifier and code column before loading. In Google Sheets, use File โ†’ Import and turn off "Convert text to numbers, dates, and formulas". These take fifteen seconds and prevent the class of bug where a spreadsheet has been quietly wrong for months.

The third is quoting and encoding. RFC 4180 is the reference definition of the CSV format: fields containing a comma, a double quote or a line break must be enclosed in double quotes, and an embedded double quote is escaped by doubling it. Clinical free text contains all three regularly, so an exporter that only quotes "when it looks necessary" will eventually emit a broken row. Quoting every field unconditionally is simpler and always valid. On encoding, patient names carry accents and non-Latin characters; the file should be UTF-8, and if your Excel build still misreads UTF-8 on a plain open, the Data โ†’ From Text/CSV path lets you set the encoding explicitly.

Sanity-Check the Table Before You Trust It

Before the CSV goes anywhere, run three quick checks. Does the row count match expectations โ€” resource count in join mode, or something larger and explicable in expand mode? Does the resource tally the explorer reports match what the sender claimed to send? And are the reference columns, such as subject.reference, populated with values that actually point at resources in the same bundle? That last one is the most common silent defect in a delivered bundle, and it is worth understanding properly; How FHIR References Work covers relative references, urn:uuid: entries and why some references cannot be resolved locally.

It is also worth being explicit about what a flattening tool is not. It reads and reshapes; it does not judge conformance. A resource missing a required element will flatten perfectly happily into a row with an empty cell. If you need to know whether the data is valid against a profile, that is a separate job โ€” see Understanding FHIR Validator Cardinality and Slicing Errors for what those checks actually catch.

Scale, and Why Local Processing Matters Here

Conversion work is iterative. You export, look at the sheet, realise you wanted a different column set or the other repeat mode, and export again. Every one of those passes involves real protected health information โ€” names, birth dates, identifiers, diagnoses. Routing that loop through a hosted converter means transmitting identifiable patient data to a third party on every iteration, which is a HIPAA and GDPR concern before it is a technical one. A browser-based converter keeps the whole loop on your machine; you can confirm in the network tab that nothing leaves.

Local processing does impose honest limits. The Bundle Explorer accepts input up to 25 MB and parses up to 20,000 resources, and when a cap is hit it says so explicitly with the number of resources dropped rather than quietly truncating your table. If your export is larger, split it โ€” bulk exports arrive as separate files per resource type anyway, which is exactly the split you want. FHIR Bulk Data Export Explained covers how those files arrive and how to work through them file by file.

Conclusion

Converting FHIR to CSV comes down to four decisions made deliberately: one table per resource type, join or expand for repeating values, a column set chosen for the question you are answering, and an import path into Excel that does not autoformat your identifiers into nonsense. Collapse array indices so your columns stay stable, quote every field per RFC 4180, neutralise leading =, +, - and @, and check your reference columns before you trust the join. Do all of that in the browser with the FHIR Bundle Explorer and the spreadsheet you hand over will be one that survives contact with a real analyst โ€” with the patient data never having left your machine.

← Back to Blog