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.