FHIR R4 Result Publishing

Publishing a verified result as FHIR is a projection problem, not a serialization one: the canonical result the validation engine released is the source of truth, and the FHIR R4 Observation is a lossless, standards-bound view of it that an electronic health record can ingest without re-interpreting a single value. The hard part is not producing JSON that the FHIR validator accepts — that bar is low — but producing a payload that is structurally valid and semantically faithful: the LOINC identity survives, the number carries its UCUM unit, the status matches the report record, and the reference interval and interpretation the laboratory computed are the ones the clinician sees. This page specifies the stage contract, the canonical-field-to-FHIR-path mapping, the deterministic projection code, the status model, the error taxonomy peculiar to interoperability, and the tests that prove the projection never silently drifts from the result it claims to represent.

Context and Pipeline Position

Within Result Reporting, Release and Interoperability, the FHIR publisher is one of three composers fed by the release gate, running in parallel with the signed-report composer of Report Generation and Part 11 E-Signatures and the Audit Trail Export and Retention exporter. It consumes only results the Clinical Result Validation and Rule Engine Architecture marked releasable, and it reads every outbound code — analyte, specimen, unit — from the shared terminology service governed by Terminology and Code Mapping References. The publisher never decides clinical meaning; that decision was made upstream. Its job is to carry the meaning across the boundary to the EHR without loss.

Pipeline position of the FHIR R4 publisher A verified result flows from the validation engine through the release gate into the FHIR publisher, the highlighted stage, which reads analyte and unit codes from a versioned terminology service and emits a validated bundle onto the outbound interoperability queue for the electronic health record. Validation Engine releasable result Release Gate re-checks verdict FHIR Publisher Observation → bundle status · provenance kept Outbound Queue validated bundle EHR ingests result Terminology Service LOINC · UCUM · versioned

The publisher is stateless with respect to patient history and to prior publications: given the same canonical result and the same terminology version, it produces byte-identical FHIR. That determinism is what lets a payload be regenerated during an audit and compared against what was actually transmitted, and it is why the projection reads its codes from a versioned terminology service rather than resolving them ad hoc.

Stage Boundaries

The publisher exposes one ingress contract and one egress contract, with explicit failure semantics at each edge. Treat these as the only supported ways data enters or leaves the node.

Ingress — the releasable result. The node accepts a fully verified, canonicalized result: a numeric value, a UCUM unit, a resolved loinc, a patient_id, a specimen collected_at instant, the released status, the classification flag from range checking, and the reference interval that was applied. The result must already carry a releasable verdict; the publisher re-asserts that invariant at the boundary rather than trusting the flag, the same discipline the release gate applies one stage up. A payload missing any field required to build a conformant Observation is rejected at ingress, never partially projected.

Egress — the validated FHIR bundle. On success the node emits a FHIR R4 DiagnosticReport bundling one or more Observation resources, each already passed through both structural profile validation and the laboratory’s semantic-invariant checks, placed onto the outbound interoperability queue with the terminology version that produced it recorded alongside. The output is additive: the canonical result is never mutated, and the FHIR artifact is a derived view whose provenance points back to the released record.

Failure semantics. A transport failure (terminology service unreachable, queue publish rejected) is retryable and must never degrade into an unpublished result silently dropped; the projection is quarantined and re-driven with backoff. A structural failure (the assembled resource does not conform to the FHIR R4 Observation schema) is a defect in the projection code or the input contract and is not retryable — it routes to an engineering fix queue. A semantic failure (structurally valid but missing a UCUM code, or a status that disagrees with the report record) is the class this stage exists to catch: it blocks publication and raises, because a valid-but-wrong payload reaching an EHR is the worst outcome the tier can produce. The invariant across every edge: no result is published unless it is both conformant and faithful.

FHIR Resource Field Mapping

The core of the stage is a total, deterministic map from canonical result fields to FHIR R4 resource paths. The table below is the field contract; every released Observation is built from exactly these assignments, and the semantic-invariant checks described later assert that each one is present and well-formed.

Canonical field FHIR R4 path Contract
value Observation.valueQuantity.value Decimal numeric magnitude; never fused with the unit string.
unit (UCUM) Observation.valueQuantity.code + .system .system is fixed http://unitsofmeasure.org; .code is the case-sensitive UCUM code (e.g. mmol/L).
unit display Observation.valueQuantity.unit Human-readable rendering of the unit; must round-trip to the same UCUM code.
loinc Observation.code.coding[0] .system fixed http://loinc.org; .code the LOINC part number; .display the LOINC long name.
status Observation.status Projected from the report status: final, corrected, preliminary, or amended.
patient_id Observation.subject Reference of the form Patient/{id}; the patient the result belongs to.
collected_at Observation.effectiveDateTime ISO 8601 instant of specimen collection, timezone-aware.
reference_interval Observation.referenceRange[0].low / .high low.value/high.value with UCUM low.unit/high.unit mirroring valueQuantity.
flag Observation.interpretation[0].coding[0] .system fixed http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation; .code one of N, L, H, LL, HH.
specimen Observation.specimen / DiagnosticReport.specimen Reference to a SNOMED CT-coded Specimen.
verified_by Observation.performer Reference to the verifying practitioner; provenance of the release.
accession + grouping DiagnosticReport.result[] Each panel member Observation referenced from the report; DiagnosticReport.identifier carries the accession.

Two mappings deserve emphasis because they are where interoperability most often fails. First, the value and its unit are separate FHIR fields: valueQuantity.value holds the number and valueQuantity.code holds the UCUM code under the fixed system URI http://unitsofmeasure.org. Emitting "6.9 mmol/L" as a string, or dropping the system, produces a payload that validates structurally but is clinically ambiguous. Second, Observation.code must carry a LOINC coding with system set to http://loinc.org; a local analyte code in that slot is the single most common way a “valid” FHIR result is wrong. The mapping to LOINC is owned by the terminology service, connecting this stage to how LOINC codes are mapped to LIMS test panels at the platform layer.

Projection of a verified result into a FHIR DiagnosticReport bundle for the EHR A verified canonical result is mapped by a deterministic projection into a FHIR R4 Observation carrying valueQuantity, LOINC code, status, and interpretation. The Observation passes a validation gate that checks the FHIR profile and semantic invariants, is bundled into a DiagnosticReport, and is placed on the outbound queue for the electronic health record. A versioned terminology service supplies LOINC and UCUM codes to the projection. Terminology Service LOINC · UCUM · versioned Canonical Result value · unit · loinc status · flag · refRange Projection pure · deterministic field → FHIR path FHIR Observation valueQuantity · code status · interpretation Validation Gate profile + invariants UCUM · LOINC · status DiagnosticReport bundles result[] panel grouping Outbound Queue records term. version at-least-once EHR ingests DiagnosticReport
The verified result is projected into an Observation, validated for structure and semantics, bundled into a DiagnosticReport, and published to the EHR — every code drawn from the versioned terminology service.

Implementation Patterns

Model the ingress contract with Pydantic v2 so a malformed result fails at the boundary, and build the FHIR resources with typed fhir.resources R4 models so structural conformance is enforced by construction rather than asserted after the fact. Pin fhir.resources to its R4-generating major version so Observation and DiagnosticReport carry the R4 fields exactly.

python
from __future__ import annotations

from datetime import datetime
from decimal import Decimal
from enum import Enum

from pydantic import BaseModel, ConfigDict, Field

UCUM_SYSTEM = "http://unitsofmeasure.org"
LOINC_SYSTEM = "http://loinc.org"
INTERP_SYSTEM = "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation"


class ReportStatus(str, Enum):
    FINAL = "final"
    CORRECTED = "corrected"
    PRELIMINARY = "preliminary"
    AMENDED = "amended"


class Flag(str, Enum):
    NORMAL = "NORMAL"
    LOW = "LOW"
    HIGH = "HIGH"
    CRITICAL_LOW = "CRITICAL_LOW"
    CRITICAL_HIGH = "CRITICAL_HIGH"


class VerifiedResult(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    accession: str
    patient_id: str
    loinc: str
    loinc_display: str
    value: Decimal
    unit: str  # UCUM code, e.g. "mmol/L"
    reference_low: Decimal
    reference_high: Decimal
    flag: Flag
    status: ReportStatus
    collected_at: datetime
    verified_by: str
    terminology_version: str = Field(min_length=1)

The projection is a pure function of the validated result and a small, closed status/interpretation map. Keeping it pure is what makes the FHIR output reproducible during an audit: the same input and the same maps always yield the same resource.

python
INTERPRETATION_CODE: dict[Flag, str] = {
    Flag.NORMAL: "N",
    Flag.LOW: "L",
    Flag.HIGH: "H",
    Flag.CRITICAL_LOW: "LL",
    Flag.CRITICAL_HIGH: "HH",
}


def observation_status(status: ReportStatus) -> str:
    # Report status projects one-to-one onto Observation.status.
    # A value correction is `corrected`; a non-value edit (added
    # comment, demographic fix) is `amended`.
    return status.value


def project_observation(r: VerifiedResult) -> dict:
    """Deterministically build a FHIR R4 Observation payload."""
    if r.collected_at.tzinfo is None:
        raise ValueError("collected_at must be timezone-aware")
    return {
        "resourceType": "Observation",
        "status": observation_status(r.status),
        "code": {
            "coding": [
                {"system": LOINC_SYSTEM, "code": r.loinc, "display": r.loinc_display}
            ]
        },
        "subject": {"reference": f"Patient/{r.patient_id}"},
        "effectiveDateTime": r.collected_at.isoformat(),
        "performer": [{"reference": f"Practitioner/{r.verified_by}"}],
        "valueQuantity": {
            "value": float(r.value),
            "unit": r.unit,
            "system": UCUM_SYSTEM,
            "code": r.unit,
        },
        "referenceRange": [
            {
                "low": {"value": float(r.reference_low), "unit": r.unit,
                        "system": UCUM_SYSTEM, "code": r.unit},
                "high": {"value": float(r.reference_high), "unit": r.unit,
                         "system": UCUM_SYSTEM, "code": r.unit},
            }
        ],
        "interpretation": [
            {"coding": [{"system": INTERP_SYSTEM,
                         "code": INTERPRETATION_CODE[r.flag]}]}
        ],
    }

Validating the payload dictionary through the typed FHIR model turns any structural mistake into a ValidationError at construction time, before the resource can reach the queue:

python
from fhir.resources.observation import Observation
from fhir.resources.diagnosticreport import DiagnosticReport


def to_observation(r: VerifiedResult) -> Observation:
    return Observation.model_validate(project_observation(r))


def bundle_report(
    accession: str, patient_id: str, status: ReportStatus,
    issued: datetime, observations: list[Observation],
) -> DiagnosticReport:
    payload = {
        "resourceType": "DiagnosticReport",
        "status": status.value,
        "identifier": [{"system": "urn:lab:accession", "value": accession}],
        "subject": {"reference": f"Patient/{patient_id}"},
        "issued": issued.isoformat(),
        "code": {"text": "Laboratory report"},
        "result": [{"reference": f"Observation/{o.id}"} for o in observations],
    }
    return DiagnosticReport.model_validate(payload)

Publishing a panel fans out projection across the event loop with asyncio.TaskGroup, then bundles the resulting observations into a single DiagnosticReport under the accession. Because project_observation is pure and CPU-cheap, throughput is bounded by the terminology cache hit rate and the outbound queue, not by projection — the same bounded-concurrency discipline the async batch processing workers apply upstream.

Status and Provenance Projection

Observation.status is not cosmetic; an EHR keys clinical workflow off it, so a mismatch between the report record and the projected status is a patient-safety defect, not a formatting bug. The report status projects onto the FHIR value set as follows.

Report status Observation.status DiagnosticReport.status Meaning
Preliminary preliminary preliminary Verified but not final; may still change.
Final final final Released, complete, verified.
Corrected (value) corrected corrected A released value was changed to fix an error.
Amended (non-value) amended amended A released record was modified without changing the value (comment, demographic).

The distinction between corrected and amended matters: FHIR reserves corrected for a change that fixes an erroneous value and amended for a modification that does not alter the value. Collapsing the two — publishing every change as corrected — misleads a clinician into believing a result value changed when it did not. Because a correction is modeled upstream as a new superseding record rather than a mutation (the discipline established in Report Generation and Part 11 E-Signatures), the publisher projects each superseding record as its own Observation whose provenance — performer, effectiveDateTime, and the terminology version — points back to the released record that produced it. Provenance is preserved, not reconstructed: the FHIR artifact cites the same verifying practitioner and collection instant the audit record holds.

Error Classification and Handling

Interoperability failures are tiered so each class has one correct disposition. The dangerous class is unique to publishing: a payload that is structurally valid but semantically wrong. It passes the FHIR validator and is therefore invisible to structural checks alone, yet it carries a clinically incorrect meaning across the boundary.

Tier Example Disposition Retryable
Transport Terminology service timeout, queue publish rejected Quarantine, re-drive with backoff Yes
Structural Resource fails FHIR R4 Observation schema, missing required element Block, route to engineering fix queue No
Semantic valueQuantity without UCUM system, LOINC missing from code, status disagrees with report record Block publication, raise invariant failure No

Transport failures never degrade into a dropped publication; they raise, the orchestrator quarantines the projection, and a bounded exponential backoff re-drives it — the outbound queue is at-least-once, and consumers deduplicate on the accession-plus-observation identity. Structural failures are caught by fhir.resources construction: model_validate on a non-conformant dictionary raises before the resource exists. Semantic failures are caught by an explicit invariant check that runs after structural validation and asserts what the profile cannot: that valueQuantity.system equals http://unitsofmeasure.org, that code.coding contains a http://loinc.org entry, and that Observation.status equals the status on the source report record.

python
def assert_semantic_invariants(obs: Observation, source: VerifiedResult) -> None:
    q = obs.valueQuantity
    if q is None or q.system != UCUM_SYSTEM or not q.code:
        raise ValueError("valueQuantity must carry a UCUM system and code")
    codings = obs.code.coding or []
    if not any(c.system == LOINC_SYSTEM and c.code for c in codings):
        raise ValueError("Observation.code must include a LOINC coding")
    if obs.status != source.status.value:
        raise ValueError(
            f"status drift: observation={obs.status} report={source.status.value}"
        )

The controlling invariant is the fidelity default: a result is published only if it is both conformant and faithful; anything valid-but-wrong is blocked, never queued. A missing UCUM system is not a warning to log and move past — it is a stop.

Regulatory Touchpoints

FHIR publishing sits on the regulated boundary where an internal decision becomes a record another system acts on, and several regimes constrain the implementation above.

  • CLIA §493.1291 report-content requirements do not evaporate because the transport is FHIR: the published Observation must carry the result value with its units and the reference interval, which is why valueQuantity and referenceRange are mandatory in the projection, not optional. A payload that drops the reference interval fails the same clause a paper report would.
  • HL7 FHIR R4 conformance is the interoperability contract itself; the resource must validate against the R4 Observation and DiagnosticReport profiles the receiving EHR expects, which is why structural validation via typed models precedes every publish.
  • HIPAA §164.312(e) transmission security governs the outbound queue: the FHIR payload is PHI, so transport is encrypted, minimum-necessary (no fields the EHR does not need), and every publish is written to the audit sink with the recipient and timestamp — the transport counterpart to the HIPAA-compliant audit trails captured at write time.
  • 21 CFR Part 11 §11.10 attributability applies because the Observation asserts who verified the result and when; performer and effectiveDateTime carry that provenance, and the terminology version recorded with the publish makes the exact codes reproducible during an inspection.

Because the FHIR artifact is a view of a signed record, the audit event for the publish must reference the released record it projected, so an inspector can tie the transmitted payload back to the signed report and the terminology version that produced its codes.

Testing and Validation

Faithful projection is only credible if it is proven, and the proof has three parts: structural conformance, semantic invariants, and round-trip stability.

Structural tests assert that a representative result projects to a resource the FHIR R4 models accept, and that a deliberately broken projection is rejected:

python
from decimal import Decimal
from datetime import datetime, timezone

import pytest


def sample() -> VerifiedResult:
    return VerifiedResult(
        accession="A24-0001", patient_id="p-42", loinc="2823-3",
        loinc_display="Potassium [Moles/volume] in Serum or Plasma",
        value=Decimal("6.9"), unit="mmol/L",
        reference_low=Decimal("3.5"), reference_high=Decimal("5.1"),
        flag=Flag.CRITICAL_HIGH, status=ReportStatus.FINAL,
        collected_at=datetime(2026, 6, 1, 14, 30, tzinfo=timezone.utc),
        verified_by="pr-7", terminology_version="tx-2026.06",
    )


def test_projection_is_structurally_valid() -> None:
    obs = to_observation(sample())
    assert obs.status == "final"
    assert obs.valueQuantity.system == UCUM_SYSTEM
    assert obs.code.coding[0].system == LOINC_SYSTEM


def test_missing_unit_system_is_rejected() -> None:
    payload = project_observation(sample())
    payload["valueQuantity"].pop("system")
    obs = Observation.model_validate(payload)  # structurally still valid
    with pytest.raises(ValueError):
        assert_semantic_invariants(obs, sample())

The second test is the important one: it demonstrates that a payload can pass FHIR structural validation while failing the semantic invariant, which is exactly why the invariant check exists as a separate gate. Property-based tests with hypothesis extend this across the input space — for every generated flag, the projected interpretation code is the expected v3-ObservationInterpretation value; for every status, Observation.status matches the source.

Round-trip tests guarantee the resource is byte-stable through serialization, the property an inspector relies on when comparing a regenerated payload against what was transmitted:

python
def test_observation_round_trips() -> None:
    obs = to_observation(sample())
    again = Observation.model_validate_json(obs.model_dump_json())
    assert again == obs

A conformance test loads the receiving EHR’s expected profile and validates a bundle against it in CI, so a profile change the EHR requires is caught before a nonconformant bundle reaches the outbound queue. The concrete, end-to-end walk-through of building, validating, and posting a single result lives in Publishing Verified Results as FHIR R4 Observations.

Part of: Result Reporting, Release and Interoperability.

Frequently Asked Questions

Why validate semantic invariants when the FHIR profile already validated?

Because structural validation cannot see clinical meaning. A payload can satisfy the FHIR R4 Observation schema while carrying a bare number without a UCUM system, a local code where a LOINC was required, or a final status on a corrected result. The semantic-invariant check runs after structural validation and asserts exactly those facts the profile cannot, blocking any valid-but-wrong payload before it reaches the EHR.

How is the value's unit represented in a FHIR Observation?

As a valueQuantity with the number in value and the unit split across unit (human display), code (the case-sensitive UCUM code), and system fixed to http://unitsofmeasure.org. The number and the unit are never fused into one string; emitting "6.9 mmol/L" as text produces a clinically ambiguous payload even though it may validate.

What is the difference between corrected and amended Observation status?

FHIR reserves corrected for a change that fixes an erroneous result value and amended for a modification that does not alter the value, such as an added comment or a demographic fix. Publishing every change as corrected misleads a clinician into believing a value changed when it did not, so the publisher projects each precisely from the report record’s status.

How is a published FHIR payload reproduced for an inspector?

Projection is a pure function of the verified result and a versioned terminology map, and the terminology version is recorded with every publish. Regenerating the projection from the released record with that version yields a byte-identical resource, which an inspector can compare against what was transmitted and tie back to the signed report via the audit event.