DEV Community

Harikrushna V
Harikrushna V

Posted on

FHIR DiagnosticReport Resource: A Developer's Guide to Lab Reports and Radiology in an Indian HMIS

FHIR DiagnosticReport Resource: A Developer's Guide to Lab Reports and Radiology in an Indian HMIS

If you've ever tried to build a lab module or a radiology viewer inside a Hospital Management Information System, you know the pain. Pathology labs send PDFs. Radiology departments push DICOM. Hospital staff re-types values into Excel. Patients carry printouts between visits, and when they switch hospitals, the new doctor's first question is always the same — "can you bring your old reports?"

The FHIR DiagnosticReport resource is the FHIR answer to that chaos. It is the canonical way to model a lab report, a histopathology report, or a radiology report as a structured, queryable, machine-readable document. When your HMIS stores DiagnosticReports properly, the patient's history follows them across facilities — through ABDM's ABHA-linked records, across the PHR (Personal Health Record) app on their phone, and into the next hospital that sees them.

In this post, we'll walk through every important field of DiagnosticReport, build it with HAPI FHIR in Java + Spring Boot, and tie it together with the Indian healthcare context: NABL-accredited labs, ABDM's diagnostic record sharing, and SNOMED CT coding.

This is the eighth post in our FHIR Resource Deep-Dive series for Indian healthcare IT. The previous posts covered Patient, Practitioner, Organization, Encounter, Condition, Observation, and MedicationRequest. If you're building or scaling an HMIS in India and want the FHIR fundamentals tight, start with Patient and Observation — DiagnosticReport hangs off both.

What Is a FHIR DiagnosticReport?

The official FHIR R4 spec describes DiagnosticReport as: "The findings and interpretation of diagnostic tests performed on patients, groups of patients, devices, and locations, and/or specimens derived from these. The report includes clinical context such as requesting and performing provider information, and some mix of atomic results, images, textual and coded interpretations, and formatted representations of these reports."

In plain English: a DiagnosticReport is the wrapper around a lab or radiology report. It is not the individual test results themselves — those are Observation resources (e.g., a haemoglobin value, a blood glucose reading, a tumour measurement on a CT scan). The DiagnosticReport is the report document that groups all those observations together, attaches them to a patient and an encounter, identifies who performed the test and who interpreted it, and gives the report a status and a coded type.

Think of it like this:

  • Observation = a single lab value (HbA1c = 7.2%)
  • DiagnosticReport = the complete "HbA1c report" document containing that value plus the methodology, the reference lab, the interpreting pathologist, and the PDF attachment the lab sends back

In a well-modelled FHIR system, a single DiagnosticReport for a Complete Blood Count (CBC) might reference a dozen Observation resources — RBC count, WBC count, haemoglobin, haematocrit, platelets, MCV, MCH, MCHC, and so on. The report tells you what was ordered, when, by whom, and where to find the underlying numbers.

Why This Matters for an Indian HMIS

Three things make DiagnosticReport especially important if you're building healthcare software for India:

1. NABL accreditation requires structured outputs. The National Accreditation Board for Testing and Calibration Laboratories (NABL) accredits pathology labs in India against ISO 15189. NABL-accredited labs are required to issue reports that identify the test, the methodology, the reference range, and the accreditation number of the lab. A free-text PDF is no longer enough for many empanelled labs. FHIR DiagnosticReport gives you the structured backbone those reports need.

2. ABDM's diagnostic record sharing is built around DiagnosticReport. India's Ayushman Bharat Digital Mission (ABDM) uses FHIR profiles under the hood. When a patient fetches their records through an ABHA-linked PHR app like the ABDM-compliant health locker, lab reports show up as DiagnosticReport resources bundled with their referenced Observations. If your HMIS doesn't produce FHIR-compliant DiagnosticReports, your patients' lab history simply won't show up in their ABDM PHR — which means the patient still has to carry printouts.

3. SNOMED CT is the international coding system that NABL and AIIMS are pushing. India adopted SNOMED CT as a national terminology in 2018 through the National Resource Centre for EHR Standards (NRCeS). For diagnostic reports, the report code and the conclusionCode fields should be SNOMED CT-coded so that reports can be exchanged across hospitals and integrated with international research datasets.

The DiagnosticReport Fields That Actually Matter

Let's go field by field. I'll focus on the ones you'll touch in real Indian HMIS implementations. For the full list, the FHIR R4 DiagnosticReport spec is the reference.

status — the lifecycle of the report

The status field is a required code that tells you where the report is in its lifecycle. The allowed values in R4 are:

  • registered — the report has been ordered but not yet performed
  • partial — some results are available, but the report is not final
  • preliminary — the report is available in a draft form, not yet verified by the signing pathologist or radiologist
  • final — the report is complete and verified
  • amended — the report was modified after being finalized
  • corrected — the report was corrected after being finalized (used when a wrong value was issued)
  • appended — the report was amended by appending additional content
  • cancelled — the report has been withdrawn
  • entered-in-error — the report should never have been made (e.g., filed against the wrong patient)

In a real lab workflow in India, you'll most commonly see registered (ordered, sample collected), preliminary (instrument finished, awaiting pathologist sign-off), final (pathologist signed), and corrected (typo in original final report fixed after the fact). The state transitions matter because:

  • A preliminary report should not trigger downstream billing finalization.
  • A corrected report should generate an alert on the patient's record.
  • A cancelled report should be excluded from cumulative summaries.

category — what kind of report is this?

The category field uses a CodeableConcept to classify the report. The most common values you'll see:

  • LAB — laboratory (pathology, biochemistry, microbiology)
  • RAD — radiology (X-ray, CT, MRI, ultrasound)
  • PATH — anatomical pathology / histopathology
  • HISTO — histology specifically
  • CARD — cardiology (ECG, echo, stress test reports)

NABL accreditation matters most for LAB, PATH, and HISTO. Indian HMISes often show a different colour or icon for each category so the doctor can scan a patient's record quickly.

code — what was the report for?

The code field is a CodeableConcept that identifies the type of report. In Indian practice:

  • For lab reports, the code is typically a LOINC code. For example, the LOINC code for a Complete Blood Count is 58410-2 (Complete blood count (hemogram) panel - Blood).
  • For radiology reports, you can also use LOINC, but many Indian PACS systems use their own internal codes. Mapping them to LOINC at the HMIS boundary is a worthwhile engineering investment.
  • For histopathology, LOINC is the standard but the SNOMED CT morphology code may also be useful.

LOINC adoption in India is growing but still patchy. Many Indian labs use locally-defined codes that map to a smaller set of common tests. If you're building an HMIS, decide on a policy: either store both the local code and the LOINC code, or commit to LOINC at the FHIR boundary.

subject — which patient

A reference to the Patient resource. In a multi-hospital ABDM setup, this would be the patient's ABHA-linked Patient resource.

effective[x] — when was the test performed

This is a choice type. It's either effectiveDateTime (a single timestamp for tests that happen at one moment, like a glucose reading) or effectivePeriod (a range for tests that span time, like a 24-hour urine collection or a Holter monitor). For radiology, effectivePeriod is often the right choice because the scan acquisition takes time.

issued — when was the report released

This is when the lab or radiology system released the report to the requesting clinician. In a finalised pathology report, this is the timestamp the pathologist signed off. Different from effective[x] (when the sample was collected or scan done) and different from date (an FHIR metadata timestamp).

performer — who performed the test

An array of references to Practitioner and Organization resources. For a lab report, you'd typically reference both the lab organization (e.g., "SRL Diagnostics, Andheri branch") and the performing pathologist. For radiology, the performing radiologist is the key performer.

results — the underlying Observations

An array of references to Observation resources. Each Observation referenced here is a single test result (haemoglobin value, blood glucose, lesion measurement on CT, etc.) that belongs to this report. In our ABDM context, the ABDM Health Locker's "Lab Reports" view shows the DiagnosticReport with a button to view all referenced Observations.

imagingStudy — for radiology reports

A reference to an ImagingStudy resource. ImagingStudy is the FHIR resource for the actual imaging metadata (DICOM Study UID, number of images, modality). For an X-ray or CT, you'd create both an ImagingStudy and a DiagnosticReport. The DiagnosticReport.presentedForm carries the actual PDF or image.

conclusion — the free-text interpretation

The radiologist's or pathologist's narrative interpretation. For example: "Impression: 1.2 x 1.4 cm hypoechoic lesion in the right lobe of the liver, likely simple cyst. Clinical correlation advised."

conclusionCode — coded diagnoses from the report

An array of CodeableConcept. For histopathology, you'd put SNOMED CT morphology codes here. For radiology, you might use SNOMED CT clinical findings or ICD-10.

presentedForm — attachments

An array of Attachment resources. This is where the actual PDF or image goes. In practice, the FHIR server stores the binary content and presents a URL the client can fetch. For Indian labs, the PDF is typically the signed-and-stamped report the lab sends to the patient.

Building It with HAPI FHIR in Java

Let's see the actual Java code. I'll use HAPI FHIR R4, which is the most widely used FHIR implementation in Java.

Maven dependencies

<dependencies>
    <dependency>
        <groupId>ca.uhn.hapi.fhir</groupId>
        <artifactId>hapi-fhir-structures-r4</artifactId>
        <version>6.8.0</version>
    </dependency>
    <dependency>
        <groupId>ca.uhn.hapi.fhir</groupId>
        <artifactId>hapi-fhir-client</artifactId>
        <version>6.8.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

Creating a CBC lab report

Here's how to model a Complete Blood Count (CBC) DiagnosticReport with three referenced Observations: haemoglobin, total WBC count, and platelet count.

package com.orglance.hmis.fhir;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import org.hl7.fhir.r4.model.*;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

@Service
public class DiagnosticReportBuilder {

    private final FhirContext fhirContext = FhirContext.forR4();

    /**
     * Build a CBC (Complete Blood Count) DiagnosticReport for an Indian patient.
     *
     * @param patientMrn   The patient's hospital MRN (will be the Patient.id)
     * @param patientName  The patient's display name (for the report header)
     * @param encounterId  The encounter this report belongs to
     * @param labOrgId     The NABL-accredited lab's Organization.id
     * @param pathologistId The performing pathologist's Practitioner.id
     * @return A complete DiagnosticReport resource as a JSON string
     */
    public String buildCbcReport(String patientMrn,
                                  String patientName,
                                  String encounterId,
                                  String labOrgId,
                                  String pathologistId) {

        // The DiagnosticReport resource itself
        DiagnosticReport report = new DiagnosticReport();

        // Logical id
        report.setId("cbc-" + patientMrn + "-" + System.currentTimeMillis());

        // status: final (pathologist has signed off)
        report.setStatus(DiagnosticReport.DiagnosticReportStatus.FINAL);

        // category: LAB
        report.addCategory(new CodeableConcept()
                .addCoding(new Coding()
                        .setSystem("http://terminology.hl7.org/CodeSystem/v2-0074")
                        .setCode("LAB")
                        .setDisplay("Laboratory")));

        // code: LOINC 58410-2 - Complete blood count (hemogram) panel
        report.setCode(new CodeableConcept()
                .addCoding(new Coding()
                        .setSystem("http://loinc.org")
                        .setCode("58410-2")
                        .setDisplay("Complete blood count (hemogram) panel - Blood"))
                .setText("Complete Blood Count (CBC)"));

        // subject: reference to the Patient
        report.setSubject(new Reference("Patient/" + patientMrn)
                .setDisplay(patientName));

        // effectiveDateTime: when the sample was collected
        report.setEffective(new DateTimeType("2026-06-25T09:30:00+05:30"));

        // issued: when the report was released
        report.setIssued(new Date());

        // performer: the lab org and the pathologist
        report.addPerformer(new Reference("Organization/" + labOrgId)
                .setDisplay("NABL Accredited Lab"));
        report.addPerformer(new Reference("Practitioner/" + pathologistId)
                .setDisplay("Dr. Priya Sharma, Pathologist"));

        // encounter
        report.setEncounter(new Reference("Encounter/" + encounterId));

        // results: references to the three Observations
        // In a real system, you'd persist these Observations first and use their real IDs.
        // Here we create them inline for clarity.
        Observation hb = buildObservation(patientMrn, encounterId,
                "718-7", "Hemoglobin [Mass/volume] in Blood",
                "g/dL", 14.2, "13.0-17.0");
        Observation wbc = buildObservation(patientMrn, encounterId,
                "6690-2", "Leukocytes [#/volume] in Blood by Automated count",
                "10*3/uL", 7.8, "4.0-11.0");
        Observation plt = buildObservation(patientMrn, encounterId,
                "777-3", "Platelets [#/volume] in Blood by Automated count",
                "10*3/uL", 250.0, "150-400");

        // Note: in a real FHIR server, you'd POST each Observation first,
        // get the assigned IDs back, then reference them here.
        // For demonstration we set logical references.
        report.addResult(new Reference()
                .setReference("Observation/cbc-hb-" + patientMrn)
                .setDisplay("Hemoglobin"));
        report.addResult(new Reference()
                .setReference("Observation/cbc-wbc-" + patientMrn)
                .setDisplay("Total WBC count"));
        report.addResult(new Reference()
                .setReference("Observation/cbc-plt-" + patientMrn)
                .setDisplay("Platelet count"));

        // conclusion: free-text interpretation
        report.setConclusion(
            "All values within reference range. " +
            "No evidence of anemia, leukocytosis, or thrombocytopenia. " +
            "Recommend repeat CBC in 6 months as part of routine health check-up.");

        // conclusionCode: SNOMED CT code for "normal findings"
        report.addConclusionCode(new CodeableConcept()
                .addCoding(new Coding()
                        .setSystem("http://snomed.info/sct")
                        .setCode("17621005")
                        .setDisplay("Normal (qualifier)")));

        // presentedForm: the PDF attachment (in real use, attach the actual binary)
        Attachment pdfAttachment = new Attachment()
                .setContentType("application/pdf")
                .setTitle("CBC Report - " + patientName + " - " + report.getIssued())
                .setCreation(report.getIssued())
                .setHash(org.hl7.fhir.r4.model.Base64BinaryType
                        .fromStringValue("placeholder-base64-pdf-content"));
        report.addPresentedForm(pdfAttachment);

        // Serialize to JSON
        IParser parser = fhirContext.newJsonParser()
                .setPrettyPrint(true);
        return parser.encodeResourceToString(report);
    }

    private Observation buildObservation(String patientMrn, String encounterId,
                                          String loincCode, String loincDisplay,
                                          String unit, double value, String refRange) {
        Observation obs = new Observation();
        obs.setStatus(Observation.ObservationStatus.FINAL);
        obs.setCode(new CodeableConcept()
                .addCoding(new Coding()
                        .setSystem("http://loinc.org")
                        .setCode(loincCode)
                        .setDisplay(loincDisplay)));
        obs.setSubject(new Reference("Patient/" + patientMrn));
        obs.setEncounter(new Reference("Encounter/" + encounterId));
        obs.setValue(new Quantity().setValue(value).setUnit(unit).setSystem("http://unitsofmeasure.org").setCode(unit));
        obs.setReferenceRange(List.of(
                new Observation.ObservationReferenceRangeComponent()
                        .setText(refRange)));
        return obs;
    }
}
Enter fullscreen mode Exit fullscreen mode

Storing and exposing the report

Once you build the DiagnosticReport JSON, you POST it to your FHIR server. With HAPI FHIR's JPA server starter:

@PostMapping("/api/fhir/DiagnosticReport")
public ResponseEntity<String> createReport(@RequestBody DiagnosticReport report) {
    // The DaoConfig and FhirContext are autowired by Spring Boot
    MethodOutcome outcome = myFhirClient.create()
            .resource(report)
            .execute();

    return ResponseEntity
            .status(HttpStatus.CREATED)
            .header("Location", outcome.getId().toUnqualifiedVersionless().getValue())
            .body("Report created: " + outcome.getId().getIdPart());
}
Enter fullscreen mode Exit fullscreen mode

To fetch all CBC reports for a patient:

@GetMapping("/api/patients/{mrn}/lab-reports")
public List<DiagnosticReport> getLabReports(@PathVariable String mrn) {
    Bundle results = myFhirClient.search()
            .forResource(DiagnosticReport.class)
            .where(DiagnosticReport.SUBJECT.hasId("Patient/" + mrn))
            .and(DiagnosticReport.CATEGORY.exactly().code("LAB"))
            .and(DiagnosticReport.STATUS.exactly().code("final"))
            .returnBundle(Bundle.class)
            .execute();

    return results.getEntry().stream()
            .map(entry -> (DiagnosticReport) entry.getResource())
            .toList();
}
Enter fullscreen mode Exit fullscreen mode

This is the query that powers a patient's "My Lab Reports" view in your HMIS UI — and that same query is what ABDM's PHR apps use (with appropriate authorization) when fetching a patient's records through ABHA consent.

An Indian Radiology Workflow, End to End

Let me walk through a realistic scenario for a CT abdomen report at an Indian hospital.

Step 1 — Order placement. The requesting doctor orders a "CT Abdomen with Contrast" using a ServiceRequest resource (LOINC 30619-5 or a local code). This creates a DiagnosticReport with status = registered and a placeholder code. The Order Entry UI shows it as "Ordered, awaiting scheduling."

Step 2 — Sample/Study performed. The patient undergoes the CT scan. The PACS system creates an ImagingStudy resource with the DICOM Study Instance UID. The HMIS updates the DiagnosticReport.effectivePeriod to reflect the scan acquisition window and the imagingStudy field to reference the new ImagingStudy.

Step 3 — Preliminary report. The radiologist opens the study in their reporting workstation and saves a preliminary draft. The HMIS updates status = preliminary and starts populating conclusion. The requesting doctor's UI shows "Preliminary report available" with a yellow indicator.

Step 4 — Final report. After the radiologist signs off, status = final and issued is set to the sign-off timestamp. A conclusionCode of the relevant SNOMED CT clinical finding is added (e.g., 414916001 for "Obstruction"). The signed PDF is attached via presentedForm.

Step 5 — ABDM sharing. If the patient has linked their ABHA, your HMIS's ABDM integration publishes the final DiagnosticReport to the ABDM Health Information Exchange. The patient can now see this CT report in their ABHA app on their phone, and any other ABDM-compliant hospital they visit can fetch it with the patient's consent.

This flow is the difference between a hospital that "has a computer system" and a hospital that participates in India's digital health ecosystem.

Common Pitfalls When Modelling DiagnosticReport

After working with multiple Indian HMIS implementations, here are the mistakes I see most often:

1. Treating DiagnosticReport as a single value, not a wrapper. A DiagnosticReport without referenced Observations is just a labelled PDF. Always model the underlying Observations and reference them. The PDF in presentedForm is for human readability, but the structured Observations are what enable analytics, alerts, and cross-hospital exchange.

2. Using free-text only for code. If you only set code.text = "CBC" without a LOINC or SNOMED CT coding, your reports can't be discovered by code-based search across institutions. Always set a coding entry.

3. Not handling the corrected and amended statuses. Indian labs do issue corrected reports — a wrong patient ID is caught, a typo is fixed. Your HMIS should treat corrected reports as a new revision, not as a duplicate, and surface the correction clearly in the UI.

4. Storing the PDF in the FHIR server instead of a binary store. presentedForm.content can be inline base64, but it's not efficient for large PDFs. The recommended pattern is to POST the binary to a FHIR Binary endpoint, then reference it from presentedForm.url. Most production FHIR servers, including HAPI's JPA server, support this.

5. Ignoring category. Without category, you can't filter "show me only lab reports" or "show me only radiology reports" efficiently at the FHIR layer. Always populate it.

How ArogyaPlus HMIS Handles DiagnosticReport

At ArogyaPlus HMIS, we've built DiagnosticReport support directly into the lab and radiology modules. When a NABL-accredited lab partner pushes results through our API, we automatically:

  1. Create referenced Observation resources for each test value, SNOMED CT-coding where the lab provides the code
  2. Bundle them into a DiagnosticReport with the correct status lifecycle
  3. Attach the signed PDF via presentedForm
  4. Push the bundle to ABDM's HIU if the patient has linked their ABHA

This is the same FHIR R4 pattern you'll find in any ABDM-compliant HMIS, and it's the architectural baseline we'd recommend if you're building or modernizing a hospital system in India.

If you're scaling an HMIS or a digital health platform and want to talk FHIR, ABDM integration, or lab/radiology workflows, reach out — we work with hospital chains, diagnostic lab networks, and health-tech startups across India and South-East Asia. Drop us a line at hello@orglance.com or book a slot on our Calendly.


About the author: Harikrushna V is a software architect with 13 years of experience in Java, Spring Boot, and healthcare IT — ex-PayPal, Salesforce, NCR, and ST Microelectronics. He's the founder of Orglance Technologies and SnowCare Health Tech, building FHIR-native hospital systems and digital health platforms. Connect on LinkedIn.

Tags: fhir, healthcareit, java, india

Top comments (0)