Basic Resource Validation
HAPI FHIR provides comprehensive validation capabilities. Validation ensures resources conform to FHIR specifications, profiles, and business rules before storage or transmission. Early validation catches data quality issues, prevents interoperability problems, and helps maintain compliance with implementation guides. Integrate validation into your data ingestion pipelines and API endpoints.
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ValidationResult;
import ca.uhn.fhir.validation.SingleValidationMessage;
import org.hl7.fhir.r4.model.Patient;
public class BasicValidationExample {
private static final FhirContext ctx = FhirContext.forR4();
public static void validatePatient() {
// Create a validator
FhirValidator validator = ctx.newValidator();
// Create a patient to validate
Patient patient = new Patient();
patient.addName().setFamily("Test");
// Validate
ValidationResult result = validator.validateWithResult(patient);
// Check results
if (result.isSuccessful()) {
System.out.println("Validation passed!");
} else {
System.out.println("Validation failed:");
for (SingleValidationMessage message : result.getMessages()) {
System.out.println(message.getSeverity() + ": " +
message.getMessage() +
" at " + message.getLocationString());
}
}
}
public static ValidationResult validateResource(IBaseResource resource) {
FhirValidator validator = ctx.newValidator();
return validator.validateWithResult(resource);
}
}
Profile Validation with FhirInstanceValidator
Validate resources against StructureDefinitions and profiles. FhirInstanceValidator provides deep validation against FHIR profiles, checking cardinality constraints, type restrictions, binding strength, and invariants. Build a validation support chain that includes default FHIR definitions, terminology services, and snapshot generation for complete validation coverage.
import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.IValidatorModule;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.common.hapi.validation.support.*;
public class ProfileValidationExample {
private static final FhirContext ctx = FhirContext.forR4();
public static FhirValidator createProfileValidator() {
// Create validation support chain
ValidationSupportChain validationSupportChain = new ValidationSupportChain(
// Default validation support
new DefaultProfileValidationSupport(ctx),
// Common code systems (SNOMED, LOINC, etc.)
new CommonCodeSystemsTerminologyService(ctx),
// In-memory terminology service
new InMemoryTerminologyServerValidationSupport(ctx),
// Snapshot generator
new SnapshotGeneratingValidationSupport(ctx)
);
// Create instance validator
FhirInstanceValidator instanceValidator =
new FhirInstanceValidator(validationSupportChain);
// Configure validator options
instanceValidator.setNoTerminologyChecks(false);
instanceValidator.setErrorForUnknownProfiles(true);
instanceValidator.setNoExtensibleWarnings(false);
// Create and configure FhirValidator
FhirValidator validator = ctx.newValidator();
validator.registerValidatorModule(instanceValidator);
return validator;
}
public static void validateAgainstProfile(Patient patient, String profileUrl) {
// Add profile to resource
patient.getMeta().addProfile(profileUrl);
FhirValidator validator = createProfileValidator();
ValidationResult result = validator.validateWithResult(patient);
System.out.println("Validation against profile: " + profileUrl);
System.out.println("Success: " + result.isSuccessful());
for (SingleValidationMessage message : result.getMessages()) {
System.out.println(message.getSeverity() + ": " + message.getMessage());
}
}
}
Loading Custom Profiles
Load and use custom StructureDefinitions for validation. Custom profiles define organization-specific constraints beyond base FHIR, such as required elements, restricted value sets, or additional extensions. Load profiles from files, packages, or registries into PrePopulatedValidationSupport for use during validation.
import org.hl7.fhir.r4.model.StructureDefinition;
import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport;
public class CustomProfileValidation {
private static final FhirContext ctx = FhirContext.forR4();
public static FhirValidator createValidatorWithCustomProfiles() {
// Create pre-populated validation support for custom profiles
PrePopulatedValidationSupport prePopulatedSupport =
new PrePopulatedValidationSupport(ctx);
// Load custom profile from file
IParser parser = ctx.newJsonParser();
String profileJson = loadResourceFromFile("profiles/my-patient-profile.json");
StructureDefinition profile = parser.parseResource(
StructureDefinition.class, profileJson);
prePopulatedSupport.addStructureDefinition(profile);
// Load custom value sets
String valueSetJson = loadResourceFromFile("valuesets/my-valueset.json");
ValueSet valueSet = parser.parseResource(ValueSet.class, valueSetJson);
prePopulatedSupport.addValueSet(valueSet);
// Create validation chain with custom profiles
ValidationSupportChain validationSupport = new ValidationSupportChain(
new DefaultProfileValidationSupport(ctx),
new CommonCodeSystemsTerminologyService(ctx),
new InMemoryTerminologyServerValidationSupport(ctx),
new SnapshotGeneratingValidationSupport(ctx),
prePopulatedSupport // Add custom profiles
);
FhirInstanceValidator instanceValidator =
new FhirInstanceValidator(validationSupport);
FhirValidator validator = ctx.newValidator();
validator.registerValidatorModule(instanceValidator);
return validator;
}
private static String loadResourceFromFile(String path) {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
throw new RuntimeException("Failed to load: " + path, e);
}
}
}
Custom Validation Rules
Implement business-specific validation logic. While profile validation handles structural constraints, custom validators implement domain-specific rules like “active patients must have contact information” or “final observations must have values”. Create validator modules for rules that cannot be expressed in StructureDefinitions.
import ca.uhn.fhir.validation.IValidatorModule;
import ca.uhn.fhir.validation.IValidationContext;
import ca.uhn.fhir.validation.ValidationResult;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhir.validation.SingleValidationMessage;
public class CustomValidatorModule implements IValidatorModule {
private final FhirContext ctx;
public CustomValidatorModule(FhirContext ctx) {
this.ctx = ctx;
}
@Override
public void validateResource(IValidationContext<?> theContext) {
IBaseResource resource = theContext.getResource();
ValidationResult result = new ValidationResult(ctx, resource);
if (resource instanceof Patient) {
validatePatientRules((Patient) resource, result);
} else if (resource instanceof Observation) {
validateObservationRules((Observation) resource, result);
}
// Add messages to context
for (SingleValidationMessage message : result.getMessages()) {
theContext.addValidationMessage(message);
}
}
private void validatePatientRules(Patient patient, ValidationResult result) {
// Rule: Patient must have at least one identifier
if (!patient.hasIdentifier()) {
result.addValidationMessage(
new SingleValidationMessage()
.setSeverity(ResultSeverityEnum.ERROR)
.setMessage("Patient must have at least one identifier")
.setLocationString("Patient"));
}
// Rule: Active patients must have contact information
if (patient.getActive() &&
!patient.hasTelecom() &&
!patient.hasAddress()) {
result.addValidationMessage(
new SingleValidationMessage()
.setSeverity(ResultSeverityEnum.WARNING)
.setMessage("Active patient should have contact information")
.setLocationString("Patient"));
}
// Rule: Birth date cannot be in the future
if (patient.hasBirthDate() &&
patient.getBirthDate().after(new Date())) {
result.addValidationMessage(
new SingleValidationMessage()
.setSeverity(ResultSeverityEnum.ERROR)
.setMessage("Birth date cannot be in the future")
.setLocationString("Patient.birthDate"));
}
}
private void validateObservationRules(Observation obs, ValidationResult result) {
// Rule: Observation must have a subject
if (!obs.hasSubject()) {
result.addValidationMessage(
new SingleValidationMessage()
.setSeverity(ResultSeverityEnum.ERROR)
.setMessage("Observation must have a subject")
.setLocationString("Observation.subject"));
}
// Rule: Final observations must have a value or reason
if (obs.getStatus() == Observation.ObservationStatus.FINAL) {
if (!obs.hasValue() && !obs.hasDataAbsentReason()) {
result.addValidationMessage(
new SingleValidationMessage()
.setSeverity(ResultSeverityEnum.ERROR)
.setMessage("Final observation must have value or data absent reason")
.setLocationString("Observation"));
}
}
// Rule: If has components, shouldn't have a value
if (obs.hasComponent() && obs.hasValue()) {
result.addValidationMessage(
new SingleValidationMessage()
.setSeverity(ResultSeverityEnum.WARNING)
.setMessage("Observation with components should not have a value element")
.setLocationString("Observation.value"));
}
}
}
Terminology Validation
Validate codes against terminology services. Terminology validation ensures coded values come from the correct code systems and conform to value set bindings. Use the $validate-code operation to check individual codes or configure RemoteTerminologyServiceValidationSupport for automatic validation during resource validation.
import org.hl7.fhir.r4.model.ValueSet;
import org.hl7.fhir.r4.model.CodeSystem;
public class TerminologyValidationExample {
private static final FhirContext ctx = FhirContext.forR4();
public static boolean validateCodeAgainstValueSet(
String system,
String code,
String valueSetUrl) {
IGenericClient client = ctx.newRestfulGenericClient("http://hapi.fhir.org/baseR4");
// Use $validate-code operation
Parameters input = new Parameters();
input.addParameter().setName("url").setValue(new StringType(valueSetUrl));
input.addParameter().setName("system").setValue(new StringType(system));
input.addParameter().setName("code").setValue(new StringType(code));
try {
Parameters result = client.operation()
.onType(ValueSet.class)
.named("$validate-code")
.withParameters(input)
.execute();
BooleanType resultParam = (BooleanType) result.getParameter("result");
return resultParam.getValue();
} catch (Exception e) {
System.err.println("Validation failed: " + e.getMessage());
return false;
}
}
public static void validateObservationCode() {
// Validate LOINC code
boolean isValid = validateCodeAgainstValueSet(
"http://loinc.org",
"8480-6", // Systolic blood pressure
"http://hl7.org/fhir/ValueSet/observation-codes"
);
System.out.println("Code is valid: " + isValid);
}
public static ValueSet expandValueSet(String valueSetUrl) {
IGenericClient client = ctx.newRestfulGenericClient("http://hapi.fhir.org/baseR4");
Parameters input = new Parameters();
input.addParameter().setName("url").setValue(new StringType(valueSetUrl));
ValueSet expanded = client.operation()
.onType(ValueSet.class)
.named("$expand")
.withParameters(input)
.returnResourceType(ValueSet.class)
.execute();
return expanded;
}
}
Remote Terminology Service
Connect to external terminology servers for validation. Remote terminology services provide access to large code systems like SNOMED CT and LOINC that are impractical to load locally. Configure the validation chain to call external services for code validation while using local services for structure validation.
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
public class RemoteTerminologyValidation {
private static final FhirContext ctx = FhirContext.forR4();
public static FhirValidator createValidatorWithRemoteTerminology(
String terminologyServerUrl) {
// Create remote terminology support
RemoteTerminologyServiceValidationSupport remoteTermSupport =
new RemoteTerminologyServiceValidationSupport(ctx);
remoteTermSupport.setBaseUrl(terminologyServerUrl);
// Create validation chain
ValidationSupportChain validationSupport = new ValidationSupportChain(
new DefaultProfileValidationSupport(ctx),
remoteTermSupport, // Use remote terminology server
new SnapshotGeneratingValidationSupport(ctx)
);
FhirInstanceValidator instanceValidator =
new FhirInstanceValidator(validationSupport);
FhirValidator validator = ctx.newValidator();
validator.registerValidatorModule(instanceValidator);
return validator;
}
}
Validation Result Handling
Process validation results effectively. Validation produces messages at different severity levels: errors indicate non-conformance, warnings suggest potential issues, and information provides context. Convert results to OperationOutcome for returning validation failures through FHIR APIs. Categorize and prioritize messages to present meaningful feedback to users.
public class ValidationResultHandler {
public static void processValidationResult(ValidationResult result) {
// Categorize messages by severity
List<SingleValidationMessage> errors = new ArrayList<>();
List<SingleValidationMessage> warnings = new ArrayList<>();
List<SingleValidationMessage> info = new ArrayList<>();
for (SingleValidationMessage message : result.getMessages()) {
switch (message.getSeverity()) {
case ERROR:
case FATAL:
errors.add(message);
break;
case WARNING:
warnings.add(message);
break;
default:
info.add(message);
}
}
// Report results
System.out.println("Validation Results:");
System.out.println(" Errors: " + errors.size());
System.out.println(" Warnings: " + warnings.size());
System.out.println(" Info: " + info.size());
if (!errors.isEmpty()) {
System.out.println("\nErrors:");
for (SingleValidationMessage error : errors) {
System.out.println(" - " + error.getLocationString() +
": " + error.getMessage());
}
}
if (!warnings.isEmpty()) {
System.out.println("\nWarnings:");
for (SingleValidationMessage warning : warnings) {
System.out.println(" - " + warning.getLocationString() +
": " + warning.getMessage());
}
}
}
public static OperationOutcome toOperationOutcome(ValidationResult result) {
OperationOutcome outcome = new OperationOutcome();
for (SingleValidationMessage message : result.getMessages()) {
OperationOutcome.OperationOutcomeIssueComponent issue =
outcome.addIssue();
// Map severity
switch (message.getSeverity()) {
case FATAL:
issue.setSeverity(OperationOutcome.IssueSeverity.FATAL);
break;
case ERROR:
issue.setSeverity(OperationOutcome.IssueSeverity.ERROR);
break;
case WARNING:
issue.setSeverity(OperationOutcome.IssueSeverity.WARNING);
break;
default:
issue.setSeverity(OperationOutcome.IssueSeverity.INFORMATION);
}
issue.setCode(OperationOutcome.IssueType.PROCESSING);
issue.setDiagnostics(message.getMessage());
issue.addLocation(message.getLocationString());
}
return outcome;
}
}
Validation Summary Table
| Validator Type | Use Case |
|---|---|
| FhirValidator | Basic structural validation |
| FhirInstanceValidator | Profile and StructureDefinition validation |
| SchematronValidator | Schematron-based rules |
| Custom IValidatorModule | Business-specific rules |
| RemoteTerminologyService | Code/terminology validation |
Related Articles
Deep dive into FHIR validation with these tutorials:
- FHIR Programming using Java HAPI - Validating FHIR Resources - Validation implementation guide
- FHIR Programming using .NET - Validating FHIR Resources - .NET validation examples
- FHIR Programming using Java HAPI - Canadian Profiles - Working with regional profiles
- FHIR Programming using .NET - Canadian Profiles - .NET Canadian profile support
- Coded Vocabularies in Health Informatics - Understanding terminology bindings