Validation and Profiles

Section 13 of 18
72% complete

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 TypeUse Case
FhirValidatorBasic structural validation
FhirInstanceValidatorProfile and StructureDefinition validation
SchematronValidatorSchematron-based rules
Custom IValidatorModuleBusiness-specific rules
RemoteTerminologyServiceCode/terminology validation

Deep dive into FHIR validation with these tutorials:

Quiz: Validation and Profiles

Question 1 of 5

What is a FHIR Profile?