You Asked for an Export and Received a .ndjson File
A bulk export lands in your inbox or your object store, and instead of one tidy JSON document you have several files with an .ndjson extension, some of them hundreds of megabytes. You double-click one and your editor either refuses, hangs, or opens a single line of text that runs for a kilometre. Nothing about the experience suggests you are holding well-formed data โ but you are. NDJSON is exactly the right format for what a bulk export does; it is just an uncooperative one to look at with everyday tools.
This guide explains what NDJSON is, why editors struggle with it, how to recover when one line is broken, and how to work through the per-resource-type files a bulk export actually produces. Everything here can be done in the browser with the FHIR Bundle Explorer, which auto-detects NDJSON and reads it without uploading anything. For the wider picture of how and why an export is requested in the first place, FHIR Bulk Data Export Explained covers the workflow end to end.
What NDJSON Actually Is
NDJSON โ newline-delimited JSON โ is a container format with one rule: each line is a complete, independent JSON value, and lines are separated by a newline. That is the whole specification. There is no wrapping array, no commas between records, no closing bracket at the end. A ten-million-record NDJSON file and a one-record NDJSON file have identical structure; the second is just shorter.
The critical consequence is that the file as a whole is not valid JSON. Paste it into a JSON validator and it fails immediately, because after the first closing brace it finds another opening brace where JSON expects end-of-document. This trips up almost everyone the first time. The file is not corrupt; you are validating it under the wrong grammar. Each line is valid JSON. The file is a sequence of them.
The FHIR Bulk Data Access implementation guide, published by HL7 at hl7.org/fhir/uv/bulkdata/, specifies NDJSON as the output format for exported resources. The reasoning is streaming: a producer can write records one at a time and flush, and a consumer can process records one at a time without ever holding the whole dataset in memory. With a wrapping JSON array, neither side can start until the other finishes, and a consumer must parse the entire document before it sees the first record. For a dataset covering a hospital's whole patient population, that difference is the difference between feasible and not.
NDJSON Is Not a Bundle
It is worth being precise about the distinction, because the two get conflated. A Bundle is a FHIR resource in its own right โ resourceType: "Bundle", with a type, optional metadata, and an entry array whose members wrap the actual resources and can carry a fullUrl. NDJSON has none of that. It is a bare sequence of resources with no envelope, no bundle-level metadata, and no fullUrl per entry. If you are unsure which one you have, look at the first character of the second line: a Bundle is one JSON document, so its second line is indented content, whereas NDJSON's second line starts a fresh { at column one. What Is a FHIR Bundle? lays out the Bundle structure in full if you need the comparison.
Why a Text Editor Struggles
Three separate problems compound when you open a bulk export in a general-purpose editor.
Line length. A single FHIR resource serialised without pretty-printing is one very long line โ commonly several kilobytes, and much more for a resource with a large text narrative or a long contained array. Editors are built around the assumption that lines are short; syntax highlighting, bracket matching and soft wrapping all get expensive per character on a line. A file of ten thousand multi-kilobyte lines can hit that pathological case ten thousand times over.
File size. Many editors load the entire file into memory and build an index over it. A 400 MB export becomes multiple gigabytes of editor state. On a laptop that means swapping, then a beachball, then a force quit.
Formatting tools make it worse. The instinct is to reach for "Format Document" or a JSON prettifier to make the wall of text legible. That fails for the reason above โ the file is not valid JSON โ and the failure message is usually an unhelpful complaint about an unexpected token at some enormous character offset, which reads like corruption rather than a grammar mismatch.
What you want instead is a reader that treats the file the way the format intends: line by line, parsing each record independently, keeping only what it needs to display. That is what a purpose-built NDJSON viewer does, and it is why the same file that stalls an editor opens quickly in one.

Handling a Broken Line
Because records are independent, NDJSON degrades far more gracefully than JSON โ but only if your reader is built to exploit that. In a single JSON document, one misplaced character invalidates everything; there is no partial parse. In NDJSON, one malformed line is one bad record among thousands of good ones.
Corrupted lines are not exotic. A transfer interrupted mid-write leaves a final truncated line. A file assembled by concatenating chunks can end up with a missing newline where two records collided. A well-meaning script that filtered the file with the wrong tool can leave a stray fragment. In every case the correct behaviour is the same: report the bad line, keep the rest.
The Bundle Explorer takes exactly that approach. Parsing never throws on bad content โ problems become reported issues rather than a total failure, so a single unreadable line cannot discard the other nineteen thousand good ones. Two distinct problems are reported separately, and the difference matters for diagnosis. "Line is not valid JSON" means the line could not be parsed at all โ usually truncation or a broken chunk boundary. "Line is JSON but not a FHIR resource (no resourceType)" means the line parsed fine but is not a resource; this is what you see when a manifest, a log entry, or an OperationOutcome has been concatenated into the same file. Each issue carries its 1-based line number, so you can go to that line in your editor and look at it in isolation instead of hunting.
Two practical habits follow. First, always check the reported issue count against the resource count before you draw conclusions from the data โ a file that parsed 4,812 resources with 3 errors is fine to explore but not fine to reconcile against a source system until you know what those three were. Second, if the only bad line is the last one, suspect a truncated download and re-fetch the file before you spend time on the contents.
Per-Resource-Type Files, and Why That Helps
A bulk export does not hand you one enormous file. The Bulk Data Access IG has the server produce a set of files, and each file contains resources of a single type; the completion manifest lists them with their type and a url to download from. So a typical export is a directory like Patient, Observation, Condition, Encounter, MedicationRequest โ one or more files each. Large types are often split across several files, so seeing three Observation files is normal and does not mean something went wrong.
This is genuinely helpful rather than an inconvenience, for two reasons. It matches how you want to analyse the data anyway: each file already has a single schema, so it maps directly onto one table with one column set. And it gives you a natural unit of work. Rather than fighting a 2 GB monolith, you open the Patient file to check the cohort, then the Observation file to check the measurements. If a single file is still too big for a browser-based reader, the sensible split is by line count โ because every line is independent, cutting a NDJSON file at any newline yields two valid NDJSON files, which is not true of any other JSON container.
A word on limits: the Bundle Explorer accepts input up to 25 MB and parses up to 20,000 resources. Hitting either is not a silent event โ you get an explicit count of how many resources were dropped, so you always know whether the table in front of you is the whole file or a prefix of it. That honesty matters more than a bigger cap, because the failure mode you cannot afford is a truncated dataset you believed was complete.
Reading the Export Once It Opens
With the file parsed, the first useful view is the tally per resource type โ how many of each you actually received. Even in a per-type export this is worth checking, because it immediately catches a truncated download or a file that turned out to contain something other than what its name claimed. From there, the same table and CSV workflow applies to NDJSON as to a Bundle: pick the resource type, take the preset columns or choose your own, and export. The mechanics of that step โ join versus expand for repeating values, column choice, and the Excel import traps โ are covered in How to Convert a FHIR Bundle to CSV Without Losing Data.
References behave differently in NDJSON than in a Bundle, and this catches people out. Bundle entries can carry a fullUrl, which is what makes urn:uuid: references resolvable inside the document. NDJSON has no envelope and therefore no fullUrl, so resources are addressable only by their resourceType/id. In a per-type export that means an Observation's subject.reference pointing at Patient/123 will not resolve while you are looking at the Observation file alone โ the Patient lives in a different file. This is expected, not a defect in the data. How FHIR References Work explains the reference forms and when resolution is legitimately impossible.
Keep the File on Your Machine
A bulk export is, by definition, a large volume of protected health information โ often an entire patient population's clinical history. Uploading it to a general-purpose online JSON viewer to see what is inside is a disclosure, and one that is very hard to justify after the fact. Local, in-browser parsing removes the question entirely: the file is read by JavaScript in your tab, nothing is transmitted, and you can verify that in the network panel. The same reasoning applies to the reference resolution the explorer performs โ it resolves references only within the data you loaded and never fetches a referenced resource from an external server, because doing so would leak the identifiers it was asked to look up.
Conclusion
NDJSON is one complete JSON value per line, nothing more, which is why the file as a whole never validates as JSON and why your editor chokes on it. Read it with something that parses line by line: you get the resource tally, a real table, and precise line numbers for any record that failed instead of an all-or-nothing error. Treat per-type files as the natural unit of work, split large ones at any newline, and confirm the reported issue count before trusting a reconciliation. Open your next export in the FHIR Bundle Explorer and the file stays on your machine while you do it.