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:

bundle = self.ccda.to_fhir(document)

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
SectionResources
Document headerPatient, the authoring Practitioner and Organization, and a Composition listing the sections
ProblemsA Condition per problem, with its SNOMED or ICD-10 code, status and onset
MedicationsA MedicationStatement per medication, with its RxNorm code, dose and route
AllergiesAn AllergyIntolerance per allergy, with the substance and the reactions
ImmunizationsAn Immunization per vaccine given, with its CVX code and date
ResultsAn Observation per laboratory result, with its LOINC code, value and reference range, grouped under a DiagnosticReport
Vital signsAn Observation per measurement - blood pressure, heart rate, weight, and the rest
ProceduresA Procedure per procedure performed
EncountersAn Encounter per visit, with its dates and location
Social historyAn Observation per entry - smoking status among them
Plan of treatmentA CarePlan with the planned activities
The document itselfA 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:

  1. 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.input is the document.
  2. 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.data is the document.
  3. 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.
  4. An HL7 v2 message. An MDM^T02 message carries the document in its OBX-5 field, base64-encoded, and the MLLP channel delivers the parsed message to the service.
  5. 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

PageWhat it covers
HL7 v2 to FHIRThe same one-call conversion for HL7 v2 messages
Sending bundlesPosting a bundle to a FHIR server and reading the reply
FHIR connectionsThe outgoing connection that stores the bundle
ResourcesReading and writing the resources a document becomes
Receiving filesPicking 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