C-CDA to FHIR conversion
Turn C-CDA clinical documents - CCDs, discharge summaries, referral notes - into FHIR R4 bundles with one call, however the documents reach you.
A C-CDA document is an XML patient summary - a Continuity of Care Document (CCD), a discharge summary, a referral note - that EHRs, health information exchanges and Direct messaging send when a patient moves between providers. Zato converts such a document into a FHIR R4 bundle with one call, so the problems, medications, allergies and results it holds are read the same way as data from a FHIR server, and nothing in your code touches the XML:
The call takes the document as text or bytes, exactly as it arrived, and returns a typed bundle - a Patient from the document's header, a Condition for each problem, a MedicationStatement for each medication, an AllergyIntolerance for each allergy, an Observation for each result and vital sign, and so on for every clinical section, with the references between the resources already in place.
A complete service
A service is a Python class that Zato runs when something invokes it. The service below receives a CCD over REST, converts it and stores the result in a FHIR server through an outgoing FHIR connection:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class StoreCCD(Service):
""" Receives a C-CDA document and stores its contents in a FHIR server.
"""
name = 'demo.ccda.store'
def handle(self) -> 'None':
# The document, as it arrived ..
document = self.request.input
# .. one call converts it to a FHIR transaction bundle ..
bundle = self.ccda.to_fhir(document)
# .. and the bundle goes to the FHIR server as a whole.
client = self.fhir['FHIR.Registry']
response = client.execute('', method='post', data=bundle.to_dict())
self.logger.info('Stored %s resources -> %s', len(bundle.entry), response['type'])
The bundle is a transaction, so the server stores all of its resources together or none of them, and sending bundles explains how to read the server's reply.
What comes out of a document
Each section of the document becomes resources of its own kind. A CCD with the usual sections produces:
bundle = self.ccda.to_fhir(document)
for entry in bundle.entry:
self.logger.info(entry.resource.resource_type)
Composition
Patient
Practitioner
Organization
Condition
Condition
MedicationStatement
AllergyIntolerance
Immunization
Observation
Observation
Procedure
Encounter
DocumentReference
| Section | Resources |
|---|---|
| Document header | Patient, the authoring Practitioner and Organization, and a Composition listing the sections |
| Problems | A Condition per problem, with its SNOMED or ICD-10 code, status and onset |
| Medications | A MedicationStatement per medication, with its RxNorm code, dose and route |
| Allergies | An AllergyIntolerance per allergy, with the substance and the reactions |
| Immunizations | An Immunization per vaccine given, with its CVX code and date |
| Results | An Observation per laboratory result, with its LOINC code, value and reference range, grouped under a DiagnosticReport |
| Vital signs | An Observation per measurement - blood pressure, heart rate, weight, and the rest |
| Procedures | A Procedure per procedure performed |
| Encounters | An Encounter per visit, with its dates and location |
| Social history | An Observation per entry - smoking status among them |
| Plan of treatment | A CarePlan with the planned activities |
| The document itself | A DocumentReference holding the original XML, so the whole document is kept alongside the resources made from it |
The conversion covers the standard document types of the C-CDA specification - the CCD, Consultation Note, Discharge Summary, History and Physical, Operative Note, Procedure Note, Progress Note, Referral Note and Transfer Summary - and reads the standard sections wherever they appear in any of them.
A section the conversion does not know, one a vendor added of its own, produces no resources, and the human-readable text that every section carries is not turned into data either. Both stay in the document, which is why the DocumentReference keeps it in full - a service that needs them reads them from there.
Resources that need an ID receive one that is the same each time the same document is converted, so converting a document twice does not store its contents twice.
Read the result
The bundle's resources are typed, with attribute access to every field, and the path access of any FHIR resource works on them:
bundle = self.ccda.to_fhir(document)
for entry in bundle.entry:
resource = entry.resource
if resource.resource_type == 'Condition':
self.logger.info('Problem -> %s', resource.code.text)
elif resource.resource_type == 'MedicationStatement':
self.logger.info('Medication -> %s', resource.medicationCodeableConcept.text)
When plain data is wanted instead, bundle.to_dict() and bundle.to_json() return the bundle as a dictionary and as a JSON string, each in the shape FHIR servers accept.
Where documents come from
The document reaches the service in one of several ways, and in each the service hands what it received to the one call:
- A REST channel. A channel is an endpoint that invokes a service for each request it receives. An HIE or an EHR posts the document to it, and
self.request.inputis the document. - A file. A file transfer channel watches a directory, an SFTP server or an inbox that an EHR exports documents to, and
self.request.input.datais the document. - A FHIR server. Epic, Oracle Health and other EHRs publish each document as a DocumentReference pointing at a Binary resource that holds the XML - the next section shows how to read it.
- An HL7 v2 message. An
MDM^T02message carries the document in itsOBX-5field, base64-encoded, and the MLLP channel delivers the parsed message to the service. - Direct messaging. The document is an attachment of a Direct message that an IMAP connection to your HISP reads, on its own or inside an IHE XDM package, and
self.request.input.documents()returns it either way.
The same call converts the document in every one of them.
Convert a document from a FHIR server
An EHR that exposes its documents over FHIR lists them as DocumentReference resources, one per document, and each points at a Binary resource holding the document itself. Reading the Binary and converting it is three steps:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class ImportSummaries(Service):
""" Reads a patient's clinical summaries from an EHR and stores them in the registry.
"""
name = 'demo.ccda.import-summaries'
def handle(self) -> 'None':
source = self.fhir['FHIR.Epic']
registry = self.fhir['FHIR.Registry']
# Each DocumentReference describes one document of the patient ..
references = source.resources('DocumentReference').search(patient='erXuFYUfucBZaryVksYEcMg3').fetch_all()
for reference in references:
# .. its content points at the Binary that holds the document ..
binary_url = reference['content'][0]['attachment']['url']
binary = source.get('Binary', binary_url.split('/')[-1])
# .. and the Binary's data is the document, which one call converts.
bundle = self.ccda.to_fhir(binary['data'])
registry.execute('', method='post', data=bundle.to_dict())
A Binary's data is base64 text and to_fhir accepts it as it is, decoding it before the conversion, so there is no step between the read and the call.
Convert on arrival
An HL7 REST channel whose version is C-CDA converts each document before the service runs, so the service receives the bundle itself and there is no call at all:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class StoreCCD(Service):
""" Receives a C-CDA document that the channel has already converted.
"""
name = 'demo.ccda.store'
def handle(self) -> 'None':
bundle = self.request.input
client = self.fhir['FHIR.Registry']
client.execute('', method='post', data=bundle.to_dict())
A document the channel cannot read - one that is not XML, or not a CDA document - is rejected with an HTTP 400 response before the service is invoked.
The same channel in an enmasse file is a REST channel whose data_format is hl7-ccda:
channel_rest:
- name: ccda.store
service: demo.ccda.store
url_path: /ccda/store
data_format: hl7-ccda
Troubleshooting
The bundle has a Patient and little else. The document's sections carry text but no structured entries, which is how some systems export summaries. The text is in the DocumentReference and in the Composition's sections, and there is no data to make resources from.
A section of the document produced no resources. It is one the conversion does not know, usually a vendor's own, and its contents are kept in the DocumentReference.
The server rejected the bundle. The server's reply names the resource and the field it refused - typically a code system the server does not accept or a required field the document did not fill in. Validating the bundle's resources before sending, as sending bundles shows, finds the field in your own service first.
The call raised an exception. The input was not a CDA document - the usual cause is a document passed in still wrapped in the message or the envelope it arrived in, such as the whole MDM^T02 message rather than the contents of its OBX-5 field.
The attachment is a zip. It is an IHE XDM package, and the attachment's documents() returns the documents inside it, each ready for the call.
See also
| Page | What it covers |
|---|---|
| HL7 v2 to FHIR | The same one-call conversion for HL7 v2 messages |
| Sending bundles | Posting a bundle to a FHIR server and reading the reply |
| FHIR connections | The outgoing connection that stores the bundle |
| Resources | Reading and writing the resources a document becomes |
| Receiving files | Picking documents up from directories, SFTP and inboxes |
Learn more
Schedule a meaningful demo
Book a demo with an expert who will help you build meaningful systems that match your ambitions
"We evaluated 12 integration platforms and Zato was the only one to score 100%."
Philip Zuñiga, Assistant Professor, University of the Philippines