Synthetic patient data for testing
Synthea, personas and model-generated data compared, what good test data contains, and what happened when we loaded Synthea patients into a FHIR server.
Synthetic patient data is invented medical records — patients, diagnoses, medications, lab results, visits — generated to look and behave like real ones, so you can build, test and demo healthcare software without touching anyone's health information. It is the default for development: real patient data in a test environment is a privacy risk and usually a legal problem. This page covers the main ways to get synthetic data, what each is good and bad at, how to design tests around it, and how to load it into a FHIR server — including what happened when we loaded Synthea patients into Anpheros.
Three ways to get synthetic patients
| Approach | Example | Good for | Watch out for |
|---|---|---|---|
| Rule-based simulation | Synthea (open source, from MITRE): simulates lives from birth with disease modules and exports FHIR R4 bundles, C-CDA and CSV | Volume, full histories, standard formats | US-centred by default (demographics, billing resources, codes); patterns are only as realistic as the modules |
| Curated personas | Hand-designed patients with specific conditions | Demos, edge cases, repeatable tests | Few patients; someone has to keep them coherent |
| Statistical or ML generation from real data | Models trained on a real dataset | Research realism | Can leak real people if done badly; needs privacy evaluation, not just "it's synthetic" |
Text written by a language model on request is a fourth, risky source: it reads well but often contradicts itself (a medication without the condition it treats, impossible lab values) and should not be the basis of tests.
What good test data contains
Realistic averages are not enough. Code breaks on the edges, so make sure your data, synthetic or not, includes:
- Missing and partial data: a lab result without a reference range, a medication without a dose, a patient without a birth date.
- Units and formats: the same test in mg/dL and mmol/L, decimal commas, times with and without time zones.
- Corrections and history: an amended result, a deleted (
entered-in-error) entry, a condition that resolved. - Duplicates: the same report sent twice.
- Long and short histories: a newborn, someone with ten years of data, someone with one visit.
- Several languages and countries if you serve more than one market.
- Consent changes: an app whose access is revoked in the middle of a session.
Loading Synthea patients into a FHIR server
Synthea writes one FHIR R4 transaction Bundle per patient. We generated two patients (java -jar synthea-with-dependencies.jar -p 2 -s 4242 Massachusetts) and loaded them into a local instance of Anpheros. The first patient's bundle had 221 entries:
| Kept (types Anpheros stores) | Removed (types Anpheros does not store) |
|---|---|
| Observation 61, Procedure 48, DiagnosticReport 24, Condition 15, Encounter 12, DocumentReference 12, Immunization 6, Patient, CareTeam, CarePlan, Provenance | Claim 13, ExplanationOfBenefit 13, SupplyDelivery 9, Device 2, ImagingStudy 1, MedicationRequest 1 |
The loader asks the server which types it supports, keeps only those and posts the rest as one transaction:
import { readFile } from 'node:fs/promises';
import { Anpheros, apiKey } from '@anpheros/sdk';
const anpheros = new Anpheros({ auth: apiKey(process.env.ANPHEROS_KEY!) }); // sk_test_… key
const SUPPORTED = new Set(((await anpheros.fhir.metadata()) as any).rest[0].resource.map((r: any) => r.type));
const bundle = JSON.parse(await readFile(process.argv[2], 'utf8')); // one Synthea patient file
const kept = bundle.entry.filter((e: any) => SUPPORTED.has(e.resource.resourceType));
const out: any = await anpheros.fhir.transaction({ ...bundle, entry: kept } as any);
console.log('created', out.entry.length);
Result: 182 resources created in one transaction. The urn:uuid references inside the bundle were resolved to server ids (a DiagnosticReport's result points to an Observation that exists), and Patient/{id}/$summary produced an International Patient Summary with 15 problems, 6 immunizations, 15 results and 28 vital signs. $everything returned 363 entries, including the provenance the platform adds to each write.
Three things to know before you rely on it:
- Conditional references stay as written. Synthea points to practitioners, organisations and locations with conditional references such as
Practitioner?identifier=http://hl7.org/fhir/sid/us-npi|9999967398. Anpheros does not resolve conditional references in a transaction; they are stored as written, with theirdisplaytext, and do not resolve to a resource. If your code follows those links, load practitioners and organisations separately and rewrite the references. - Billing and supply data is gone. Claims and explanations of benefit are central to Synthea's US cost model and are not part of a clinical record in Anpheros.
- Prescriptions become absent, not converted.
MedicationRequestis removed; if you need the medications in the record, convert them toMedicationStatementbefore loading.
Names in Synthea include numbers (Marcelo725 Gulgowski816) on purpose, so nobody mistakes them for real people.
Synthetic patients in the Anpheros sandbox
Every Anpheros sandbox project gets its own copy of 30 synthetic patients, generated from hand-designed personas: ages 3 to 85, several EU countries and languages, conditions coded in ICD-10, medications in ATC, labs and vitals in LOINC with UCUM units and reference ranges, six months of history and documents. A freshly seeded project holds 30 patients, 43 conditions, 51 medication statements, 1,811 observations, 57 documents and 6 allergies. The data is deterministic — every project sees the same people with the same values, only different ids — and a reset restores it. Each persona keeps a stable identifier, so Patient?identifier=https://anpheros.com/fhir/sid/sandbox-persona|p02 always finds the same person. See Free FHIR database and sandbox.
Use the personas when you want small, coherent records that cover European languages and the platform's own features; load Synthea when you need volume or US-style histories.
Frequently asked questions
What is synthetic patient data?
Invented health records that resemble real ones in structure and patterns but describe no real person. It lets developers build and test without using protected health information.
Is synthetic data anonymous by definition?
Data generated from rules, like Synthea's, contains no real patients. Data generated by models trained on real records can still reveal real people and needs a privacy evaluation before it is shared.
Can I import Synthea data into a FHIR server?
Yes. Synthea exports FHIR R4 transaction bundles. Remove resource types the server does not support, check how it handles conditional references, and post each bundle as a transaction.
Does the Anpheros sandbox include synthetic patients?
Yes: 30 per project, with six months of coded history, plus up to 500 test patients you create yourself.