FHIR Programming using Java and HAPI FHIR Server - Validating FHIR Resources

Introduction

Welcome back to my series on FHIR Programming using Java and HAPI FHIR Server. In this article, we will dive into the essential task of validating FHIR resources. Validation ensures that the data you are working with adheres to the FHIR standard, which is crucial for maintaining data integrity and interoperability in healthcare applications.

This tutorial builds upon our previous discussions on searching FHIR resources. If you're unfamiliar with searching or manipulating resources, I recommend reviewing those articles before proceeding. By the end of this article, you'll be able to validate FHIR resources effectively, ensuring the consistency and correctness of your healthcare data.

Understanding FHIR Validation

Before diving into validation code, it's essential to understand the theoretical foundation of FHIR validation and the different layers involved.

StructureDefinitions: The Foundation of Validation

A StructureDefinition is a FHIR resource that defines the structure and constraints for other resources. It's the foundation of all FHIR validation:

  • Base Definitions - FHIR defines a StructureDefinition for each resource type (Patient, Observation, etc.). These define the "base" rules all instances must follow.
  • Profiles - StructureDefinitions that constrain or extend base resources for specific use cases. For example, US Core Patient adds requirements specific to US healthcare.
  • Extensions - StructureDefinitions that define new elements that can be added to resources to capture data not in the base specification.

StructureDefinitions specify:

  • Which elements are required, optional, or prohibited
  • Cardinality constraints (min/max occurrences)
  • Data type restrictions
  • Fixed values or patterns
  • Terminology bindings (which code systems/value sets to use)
  • Invariants (business rules expressed as FHIRPath expressions)

Validation Levels

FHIR validation occurs at multiple levels, each catching different types of issues:

1. Schema/Structural Validation

  • Verifies the resource is well-formed JSON or XML
  • Checks that element names are valid for the resource type
  • Validates data types (strings, booleans, dates are properly formatted)
  • Ensures required elements are present

2. Cardinality Validation

  • Checks minimum and maximum occurrences of elements
  • Example: Patient.name has cardinality 0..* (optional, unlimited), while Observation.status has 1..1 (required, exactly one)

3. Invariant/Business Rule Validation

  • Evaluates FHIRPath expressions that define constraints
  • Example: "If Observation.dataAbsentReason is present, Observation.value should not be present"
  • Cross-element validation that can't be expressed by simple cardinality

4. Profile Conformance Validation

  • Validates against implementation guide profiles
  • Checks additional constraints beyond the base specification
  • Verifies required extensions are present

5. Terminology Binding Validation

  • Verifies coded values come from the correct code systems
  • Checks that codes are valid members of required value sets
  • Validates binding strength (required, extensible, preferred, example)

Binding Strengths

Terminology bindings have different strengths that affect validation:

  • Required - Must use a code from the specified value set. Validation fails if not.
  • Extensible - Must use a code from the value set if an appropriate code exists; otherwise, can use other codes.
  • Preferred - Recommended to use codes from the value set, but not enforced.
  • Example - Just examples; any code from the code system is acceptable.

OperationOutcome Resource

Validation results are returned as an OperationOutcome resource, which contains detailed information about issues found:

// OperationOutcome structure
OperationOutcome outcome = (OperationOutcome) result.toOperationOutcome();

for (OperationOutcome.OperationOutcomeIssueComponent issue : outcome.getIssue()) {
    // Severity: fatal, error, warning, information
    System.out.println("Severity: " + issue.getSeverity());

    // Code: categorizes the type of issue
    System.out.println("Code: " + issue.getCode());

    // Location: FHIRPath to the element with the issue
    System.out.println("Location: " + issue.getLocation());

    // Diagnostics: human-readable description
    System.out.println("Details: " + issue.getDiagnostics());
}

Severity Levels:

  • Fatal - The resource is unusable; processing cannot continue
  • Error - A violation of the specification; the resource is not conformant
  • Warning - A potential issue that doesn't make the resource invalid
  • Information - Informational messages; no action required

Slicing and Discriminators

Slicing is a powerful profiling technique that allows you to define different constraints for different occurrences of a repeating element:

  • Slicing - Divides a repeating element into "slices" with different rules
  • Discriminator - Tells validators how to identify which slice an instance belongs to

Common discriminator types:

  • value - Match based on the value of a child element
  • pattern - Match based on a pattern in a child element
  • type - Match based on the data type used
  • profile - Match based on which profile a resource conforms to

Example: A profile might slice Patient.identifier to require:

  • One identifier with system = "http://hospital.org/mrn" (MRN)
  • One identifier with system = "http://hl7.org/fhir/sid/us-ssn" (SSN)

Terminology Services for Validation

FHIR defines terminology operations that support validation:

  • $validate-code - Check if a code is valid in a code system or value set
  • $lookup - Get details about a code (display name, properties)
  • $expand - Get all codes in a value set
// Validate a code against a value set
Parameters params = new Parameters();
params.addParameter("system", new UriType("http://loinc.org"));
params.addParameter("code", new CodeType("8867-4"));
params.addParameter("url", new UriType("http://hl7.org/fhir/ValueSet/observation-vitalsignresult"));

Parameters result = client.operation()
    .onType(ValueSet.class)
    .named("$validate-code")
    .withParameters(params)
    .execute();

boolean isValid = ((BooleanType) result.getParameter("result")).getValue();

Prerequisites

Before you start, ensure your environment is ready:

  • Java Development Kit (JDK) is installed and configured.
  • Apache Maven is installed and set up for your project.
  • HAPI FHIR Server is running and accessible.
  • You can find all the code demonstrated in this tutorial on GitHub here

“To live is the rarest thing in the world. Most people exist, that is all.” ~ Oscar Wilde

Step 1 of 4: Import Required Classes

To validate FHIR resources, we need to import specific classes from the HAPI FHIR library. These imports will allow us to interact with the FHIR context, validate resources, and handle validation results. Open your `App.java` file and include the following imports:

package com.saravanansubramanian.fhir;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ValidationResult;
import ca.uhn.fhir.validation.ValidatorModule;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import ca.uhn.fhir.validation.SingleValidationMessage;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.instance.model.api.IBaseResource;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

These imports provide access to the FHIR context, validator, and classes required for validating FHIR resources. The `ValidationResult` class is used to capture the results of the validation process, and `SingleValidationMessage` allows inspection of individual validation issues.

Step 2 of 4: Basic Resource Validation

Validation is a critical process in FHIR programming, ensuring that the resources you work with are compliant with the FHIR standard. The following example demonstrates how to validate a `Patient` resource using the HAPI FHIR validator:

public class BasicValidationExample {
    public static void main(String[] args) {
        // Initialize FHIR context
        FhirContext ctx = FhirContext.forR4();

        // Create a FHIR validator
        FhirValidator validator = ctx.newValidator();

        // Configure the validator with default validation modules
        validator.registerValidatorModule(new FhirInstanceValidator(ctx));

        try {
            // Create a sample patient resource to validate
            Patient patient = new Patient();
            patient.addName().setFamily("Johnson").addGiven("Robert");
            patient.setGender(Enumerations.AdministrativeGender.MALE);
            patient.setBirthDate(new Date());

            // Validate the resource
            ValidationResult result = validator.validateWithResult(patient);

            // Check if the resource is valid according to FHIR specification
            if (result.isSuccessful()) {
                System.out.println("The resource is valid!");
            } else {
                System.out.println("Validation failed with the following issues:");
                for (SingleValidationMessage message : result.getMessages()) {
                    System.out.println("Severity: " + message.getSeverity());
                    System.out.println("Location: " + message.getLocationString());
                    System.out.println("Message: " + message.getMessage());
                    System.out.println("-----------------------------");
                }
            }

            // You can also convert validation result to an OperationOutcome
            OperationOutcome outcome = (OperationOutcome) result.toOperationOutcome();
            String outcomeJson = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
            System.out.println("\nValidation Outcome as OperationOutcome resource:");
            System.out.println(outcomeJson);

        } catch (Exception e) {
            System.err.println("Error during validation: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

In this example, we create a simple `Patient` resource and validate it using the HAPI FHIR validator. The validator checks if the resource conforms to the FHIR standard's requirements. If any issues are found, they are reported in the `ValidationResult`. This feedback is essential for debugging and ensuring your resources are correctly structured.

Step 3 of 4: Validating Resources with Custom Constraints

Beyond basic validation against the FHIR schema, you might want to validate resources against custom profiles or apply specific validation rules. The following example demonstrates more advanced validation scenarios:

public class CustomValidationExample {
    public static void main(String[] args) {
        // Initialize FHIR context
        FhirContext ctx = FhirContext.forR4();

        // Create a FHIR validator
        FhirValidator validator = ctx.newValidator();

        // Configure the validator
        validator.registerValidatorModule(new FhirInstanceValidator(ctx));

        try {
            // Create a FHIR client
            IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");

            // Example 1: Create a patient with missing required fields (according to custom rules)
            Patient incompletePatient = new Patient();
            // Intentionally leave out name and other fields to trigger validation errors

            System.out.println("Example 1: Validating an incomplete patient resource");
            ValidationResult incompleteResult = validator.validateWithResult(incompletePatient);
            printValidationResult(incompleteResult);

            // Example 2: Create a patient with invalid data
            Patient invalidPatient = new Patient();
            invalidPatient.addName().setFamily("Smith").addGiven("John");
            
            // Set an invalid birth date (future date)
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            Date futureDate = sdf.parse("2050-01-01");
            invalidPatient.setBirthDate(futureDate);
            
            System.out.println("\nExample 2: Validating a patient with a future birth date");
            ValidationResult invalidResult = validator.validateWithResult(invalidPatient);
            printValidationResult(invalidResult);

            // Example 3: Create a valid patient with all required fields
            Patient validPatient = new Patient();
            validPatient.addIdentifier()
                .setSystem("http://hospital.org/mrns")
                .setValue("12345");
            validPatient.addName()
                .setFamily("Doe")
                .addGiven("Jane");
            validPatient.setGender(Enumerations.AdministrativeGender.FEMALE);
            validPatient.setBirthDate(sdf.parse("1980-08-15"));
            validPatient.addTelecom()
                .setSystem(ContactPoint.ContactPointSystem.PHONE)
                .setValue("555-123-4567")
                .setUse(ContactPoint.ContactPointUse.HOME);
            validPatient.addAddress()
                .setCity("Anytown")
                .setState("CA")
                .setPostalCode("12345")
                .setCountry("USA");

            System.out.println("\nExample 3: Validating a complete, valid patient resource");
            ValidationResult validResult = validator.validateWithResult(validPatient);
            printValidationResult(validResult);

            // Example 4: Validate resource against a profile (if available on server)
            System.out.println("\nExample 4: Validating a resource against a profile");
            try {
                OperationOutcome outcome = client.validate()
                    .resource(validPatient)
                    .profileUri("http://hl7.org/fhir/StructureDefinition/Patient")
                    .execute();
                
                String outcomeJson = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
                System.out.println("Profile Validation Result:");
                System.out.println(outcomeJson);
                
            } catch (Exception e) {
                System.err.println("Error validating against profile: " + e.getMessage());
            }

        } catch (Exception e) {
            System.err.println("Error during custom validation: " + e.getMessage());
            e.printStackTrace();
        }
    }
    
    private static void printValidationResult(ValidationResult result) {
        if (result.isSuccessful()) {
            System.out.println("Validation PASSED (No issues found)");
        } else {
            System.out.println("Validation FAILED with the following issues:");
            
            // Group messages by severity for better readability
            int errorCount = 0;
            int warningCount = 0;
            int infoCount = 0;
            
            for (SingleValidationMessage message : result.getMessages()) {
                if (message.getSeverity() == IssueSeverity.ERROR) errorCount++;
                else if (message.getSeverity() == IssueSeverity.WARNING) warningCount++;
                else if (message.getSeverity() == IssueSeverity.INFORMATION) infoCount++;
            }
            
            System.out.println("Summary: " + errorCount + " errors, " + 
                warningCount + " warnings, " + infoCount + " info messages");
            
            // Print detailed messages
            for (SingleValidationMessage message : result.getMessages()) {
                System.out.println("- [" + message.getSeverity() + "] " + 
                    message.getLocationString() + ": " + message.getMessage());
            }
        }
    }
}

This example demonstrates several validation scenarios: an incomplete patient resource missing required fields, a patient with invalid data (future birth date), a valid patient resource with all required fields, and validation against a specific profile URI. The `printValidationResult` helper method formats the validation results for better readability, grouping messages by severity.

Step 4 of 4: Server-Side Validation and Profile Validation

In addition to client-side validation, FHIR also supports server-side validation and validation against specific profiles. The following example demonstrates these capabilities:

public class ServerValidationExample {
    public static void main(String[] args) {
        // Initialize FHIR context
        FhirContext ctx = FhirContext.forR4();

        try {
            // Create a FHIR client
            IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");

            // Create a patient resource for validation
            Patient patient = new Patient();
            patient.addIdentifier()
                .setSystem("http://hospital.org/mrns")
                .setValue("MRN12345");
            patient.addName()
                .setFamily("Wilson")
                .addGiven("James");
            patient.setGender(Enumerations.AdministrativeGender.MALE);
            
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            patient.setBirthDate(sdf.parse("1975-02-20"));

            // Example 1: Validate on server without a profile
            System.out.println("Example 1: Server-side validation without a profile");
            OperationOutcome outcome1 = client.validate()
                .resource(patient)
                .execute();
                
            printOperationOutcome(ctx, outcome1);

            // Example 2: Validate on server against a profile
            System.out.println("\nExample 2: Server-side validation against a profile");
            try {
                OperationOutcome outcome2 = client.validate()
                    .resource(patient)
                    .profileUri("http://hl7.org/fhir/StructureDefinition/Patient")
                    .execute();
                    
                printOperationOutcome(ctx, outcome2);
            } catch (Exception e) {
                System.err.println("Error validating against profile: " + e.getMessage());
            }

            // Example 3: Validate an invalid resource on server
            System.out.println("\nExample 3: Server-side validation of an invalid resource");
            
            // Create an invalid patient (missing required fields according to profile)
            Patient invalidPatient = new Patient();
            // Intentionally leaving out required fields
            
            try {
                OperationOutcome outcome3 = client.validate()
                    .resource(invalidPatient)
                    .profileUri("http://hl7.org/fhir/StructureDefinition/Patient")
                    .execute();
                    
                printOperationOutcome(ctx, outcome3);
            } catch (Exception e) {
                System.err.println("Error validating invalid resource: " + e.getMessage());
            }

            // Example 4: Validate a resource that's already on the server
            System.out.println("\nExample 4: Validating an existing resource on the server");
            
            try {
                // First, save a resource to the server
                Patient savedPatient = new Patient();
                savedPatient.addIdentifier()
                    .setSystem("http://hospital.org/mrns")
                    .setValue("MRN67890");
                savedPatient.addName()
                    .setFamily("Taylor")
                    .addGiven("Elizabeth");
                savedPatient.setGender(Enumerations.AdministrativeGender.FEMALE);
                savedPatient.setBirthDate(sdf.parse("1982-11-05"));
                
                // Save to server
                MethodOutcome saveOutcome = client.create()
                    .resource(savedPatient)
                    .execute();
                    
                if (saveOutcome.getId() != null) {
                    System.out.println("Resource saved with ID: " + saveOutcome.getId().getValue());
                    
                    // Now validate the saved resource
                    OperationOutcome outcome4 = client.validate()
                        .resourceId(saveOutcome.getId())
                        .execute();
                        
                    printOperationOutcome(ctx, outcome4);
                }
            } catch (Exception e) {
                System.err.println("Error validating existing resource: " + e.getMessage());
            }

        } catch (Exception e) {
            System.err.println("Error during server validation: " + e.getMessage());
            e.printStackTrace();
        }
    }
    
    private static void printOperationOutcome(FhirContext ctx, OperationOutcome outcome) {
        // Print a summary of the operation outcome
        int errorCount = 0;
        int warningCount = 0;
        int infoCount = 0;
        
        for (OperationOutcome.OperationOutcomeIssueComponent issue : outcome.getIssue()) {
            if (issue.getSeverity() == IssueSeverity.ERROR) errorCount++;
            else if (issue.getSeverity() == IssueSeverity.WARNING) warningCount++;
            else if (issue.getSeverity() == IssueSeverity.INFORMATION) infoCount++;
        }
        
        System.out.println("Validation Result Summary: " + errorCount + " errors, " + 
            warningCount + " warnings, " + infoCount + " info messages");
            
        // Print detailed messages if any issues were found
        if (outcome.getIssue().size() > 0) {
            System.out.println("Detailed Issues:");
            
            for (OperationOutcome.OperationOutcomeIssueComponent issue : outcome.getIssue()) {
                System.out.println("- [" + issue.getSeverity() + "] " + 
                    (issue.hasLocation() ? issue.getLocation().get(0).getValue() + ": " : "") + 
                    issue.getDiagnostics());
            }
        } else {
            System.out.println("No issues found. Resource is valid.");
        }
        
        // Optionally, print the full JSON representation
        // String outcomeJson = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
        // System.out.println("Full OperationOutcome JSON:");
        // System.out.println(outcomeJson);
    }
}

This example demonstrates server-side validation scenarios, including validating resources without a profile, validating against a specific profile, validating an invalid resource, and validating a resource that already exists on the server. The `printOperationOutcome` helper method formats the operation outcome for better readability.

Conclusion

In this article, we've explored the process of validating FHIR resources using Java and the HAPI FHIR library. We covered how to perform basic validations, validate resources against custom profiles, and utilize server-side validation. These validation techniques are essential for ensuring that the FHIR resources in your healthcare applications are accurate, complete, and compliant with the FHIR standard.

With the knowledge gained from this tutorial, you can now validate your FHIR resources confidently. This ensures data quality and reliability in your healthcare solutions. Stay tuned for the next article in this series, where we will continue to explore advanced FHIR programming techniques, focusing on chaining FHIR operations.