---
title: "Vendor-Specific FHIR Quirks We Found Doing Real EHR Integration"
date: 2026-04-19
category: vitalchain
source: VitalChain Health Research
draft: true
---

# Vendor-Specific FHIR Quirks We Found Doing Real EHR Integration

There is a story that interoperability people tell themselves: FHIR R4 is a standard, every major EHR speaks it, and if you build against the base spec you will be fine. It is a nice story. It is also wrong in ways that only reveal themselves when you leave the mocks behind and point your code at a real vendor sandbox.

We spent a day doing exactly that. We pointed our FHIR interop layer at Cerner's publicly accessible open sandbox, ran real reads against a real demo patient, and captured the actual bytes that came back. What we found is worth writing down, because it is the kind of thing that silently breaks a real integration on a real patient.

This post is about three specific quirks we hit. Cerner is the vendor we tested against because their open sandbox is the only one of the big three that does not require partnership paperwork to probe. Epic and Athena have similar deviations; we simply have not pointed our harness at those yet. The broader point is vendor-agnostic: every major EHR ships subtle deviations from base FHIR, and your interop layer either knows about them or it silently fails.

## Quirk 1: Cerner does not serve MedicationStatement

If you read the FHIR R4 spec, `MedicationStatement` is the observation-style resource that represents what a patient is actually taking. `MedicationRequest` is the order-style resource that represents what a provider wrote a prescription for. A reasonable interop engineer looking at those two definitions would conclude that `MedicationStatement` is what they want to sync.

Cerner does not agree. Here is what we got back when we asked for `MedicationStatement` on a real patient:

```
GET /MedicationStatement?patient=12724066
HTTP/1.1 404 Not Found

{
  "resourceType": "OperationOutcome",
  "issue": [{ "severity": "error", "code": "not-found" }]
}
```

The same query against `MedicationRequest` returned 25 orders with full dose, route, timing, and RxNorm codes. Cerner's CapabilityStatement confirms it: only `MedicationRequest` is listed among their medication resources.

The failure mode is the clinical safety problem. If your sync engine only knows to ask for `MedicationStatement`, every Cerner patient comes back with zero medications. Not an error, not a warning, just an empty list. A responder sees a patient with no recorded prescriptions and makes decisions accordingly. The patient is on warfarin.

Our fix was to teach the bridge to read `MedicationRequest` as a first-class medication source, map its fields (`medicationCodeableConcept.text`, `dosageInstruction[0].text`, `status`, `authoredOn`) into our internal `StructuredMedication`, and keep the `MedicationStatement` path for Epic and Athena which still serve it. The sync engine now picks whichever the vendor's CapabilityStatement advertises.

## Quirk 2: Cerner returns `criticality: "unable-to-assess"` on AllergyIntolerance

Base FHIR R4 defines exactly two values for `AllergyIntolerance.criticality`: `high` and `low`. A lot of interop layers, including ours before this exercise, map those two values straight to a severity bucket and call it done.

Here is what a real Cerner allergy record looks like:

```json
{
  "resourceType": "AllergyIntolerance",
  "code": { "text": "Eggs (edible)" },
  "criticality": "unable-to-assess",
  "patient": { "reference": "Patient/12724066" }
}
```

`unable-to-assess` is not in the base spec's criticality enum. Cerner emits it anyway, because real clinical data is messier than the spec: sometimes the charting nurse genuinely cannot determine criticality and the correct honest answer is "we do not know." Cerner preserves that honesty at the wire level.

If your mapper has a `switch` statement over `high` and `low`, `unable-to-assess` falls through to the default branch. The severity on the resulting record ends up `undefined`. Which means the CDSS engine does not prioritize it. Which means a responder looking at a severe anaphylaxis history sees it presented with no severity indicator, indistinguishable from a minor contact sensitivity.

There is a second FHIR field that saves you here: `reaction[0].severity`, which is a separate element defined in US Core with `mild`, `moderate`, and `severe` values. It lives on the reaction object rather than the resource itself. When criticality is missing or ambiguous, that is where the real severity signal often lives.

Our remediation was to add a fallback: if `criticality` is not `high` or `low`, read `reaction[0].severity` and map `severe` to SEVERE, `moderate` to MODERATE, `mild` to MILD. When both criticality and reaction severity are present, we let criticality win because when a clinician has made an explicit criticality call, that outranks an inferred reaction-level severity. When neither is populated we leave severity undefined and surface the ambiguity in the UI rather than defaulting to a wrong value.

## Quirk 3: Patient.contact with only `name.text` and no telecom

The third quirk is less dangerous than the other two but more common. FHIR's `Patient.contact` element represents the emergency contacts associated with a patient: relationship, name, telecom (phone, email), address. Most reverse mappers we have seen require a `telecom` entry before they will treat a `contact` as useful, on the reasonable theory that an emergency contact you cannot actually contact is not much of an emergency contact.

Cerner's data does not always cooperate with that theory. Their Patient resource on the demo patient carries 72 contacts. Two of them are labeled "Emergency Contact." Both look like this:

```json
{
  "relationship": [{ "text": "Emergency Contact" }],
  "name": { "text": "Jdoe, Johny Smith" }
}
```

No telecom block. No address. Just a name, a relationship label, and nothing else. A strict reverse mapper drops this contact on the floor because it fails the "has a phone number" check. From the responder's point of view, a patient who actually does have two documented emergency contacts in the source system arrives in VitalChain with zero.

Our fix was to loosen the filter. A contact with a name and a relationship is better than no contact at all. The responder can at least know a name to ask about, and the phone number can be filled in at the point of care. We keep the contact with empty-string telecom fields and let the UI render "no phone on file" rather than silently discarding the record.

## The general pattern

These three quirks are different on the surface but share a shape. Each one is a place where Cerner emits something that is technically consistent with the FHIR spec, or that extends the spec in a way a real clinical environment requires, but that a strict base-spec interpreter will drop or mishandle. None of them will fire a loud error. All of them will silently hand a responder or clinician an incomplete view of the patient.

Epic and Athena have their own versions of this. We have not pointed our harness at their sandboxes yet; both require partnership registration and we have not run that paperwork. We expect to find a different set of quirks with the same shape: values outside the documented enum, resources served under a different name, fields you need to fall back to when the obvious one is empty.

The takeaway for anyone building in this space is simple. Mock-driven FHIR development will pass its tests and fail on real patient data. The only way to know whether your interop layer actually works is to point it at the real thing, capture the real bytes, pin them as test fixtures, and remediate each deviation as a named, code-level fix with its own regression test. We now have five pinned Cerner-sourced assertions in our integration suite, all running against real captured payloads, and every one of them exists because we found a quirk we would not have guessed from reading the spec.

If you are evaluating an interop platform, that is the question worth asking: show me the vendor-specific test fixtures. Not the mocks. The real responses. Because that is where the failure modes live, and the patients do not care whether the spec technically said `unable-to-assess` was allowed.
