The Complete Guide to FHIR Resource Validation: From Errors to Solutions
FHIR (Fast Healthcare Interoperability Resources) has become a cornerstone of modern healthcare data exchange, but validation errors can make or break an integration. In real-world deployments, even minor formatting mistakes or missing fields can cause data to be rejected, leading to data loss or delays in care. For example, HL7 notes that overly strict validation in production can result in the loss of critical health care data – e.g. if one field has an unexpected value, an entire patient record might be dropped. On the other hand, insufficient validation can allow bad data to propagate unnoticed. A balance is needed.
How common are validation issues? Studies and industry reports suggest they are very common. One analysis of FHIR implementations found that validation is one of the most frequent challenges teams face. At HL7 connectathons and in ONC certification testing, it’s typical for implementers to encounter dozens of errors on first attempt – missing required fields, invalid codes, etc. As an example, Microsoft’s Azure API for FHIR team has shown how failing to include mandatory elements like patient gender or identifier will trigger errors during validation. Similarly, in a national health data project in India, missing required fields were identified as a common issue that “can result in improper processing”.
The impacts of these errors are tangible. Data integration projects have reported initial failure rates of 5–10% of incoming FHIR resources due to validation errors – meaning those records had to be corrected or were not imported at all. This affects provider trust and can even have patient safety implications if critical data is omitted. In one case study, a hospital interface dropped all lab results for a day because an upstream system started sending an Observation without the status
field, causing the FHIR server to reject each message. As HL7’s guidance points out, deciding how much validation to do in production is a careful decision: too little and you risk bad data, too much and you risk missing data.
In this comprehensive guide, we’ll explore FHIR resource validation from end to end. We’ll start with the fundamentals of what validation entails and how it fits into the data lifecycle. Then we’ll dive into the 20 most common FHIR validation errors, explaining each error, why it happens, and how to solve it – with a focus on practical examples in JavaScript (and notes on other languages like Java and C# where relevant). We’ll also provide decision trees to help diagnose errors, discuss performance considerations when validating at scale, and outline strategies for building a robust validation pipeline in production. By the end, you’ll have a full toolkit for handling FHIR validation like a pro – ensuring high data quality without sacrificing performance or interoperability.
*(Fun fact: According to one industry analysis, over 70% of early FHIR projects encountered issues with missing required elements or invalid code values on their first trial – so if you’re seeing those errors, you’re not alone!) *
FHIR Validation Fundamentals: Layers and Lifecycle
Before we jump into specific errors, let’s establish what FHIR validation means and how it fits into the overall lifecycle of health data. At its core, validating a FHIR resource means checking that the resource is conformant to expectations. The FHIR specification defines multiple layers of rules that a resource might be checked against:
- Structure – Are all elements in the resource allowed by the FHIR specification (and no unexpected elements present)? Are the JSON or XML syntax and field names correct? This is a basic structural check.
- Cardinality – Does each element appear the correct number of times, respecting minimum and maximum occurrences? (e.g. a required field with min=1 must be present at least once; an element with max=1 should not appear twice).
- Value Domains – Do the values of each element conform to the data type and constraints? For example, are dates in the proper
YYYY-MM-DD
format, integers actually numeric, strings not exceeding length limits, etc.. This also includes checking coded element values: if an element is of typecode
orboolean
, is the value one of the allowed options. - Terminology Bindings – For coded fields bound to a specific ValueSet or code system, is the code provided valid? For example, if a Diagnosis resource must use ICD-10 codes, a code of “XYZ” that isn’t in the allowed set should be flagged. (This often requires integration with a terminology server or pre-loaded code lists.)
- Invariants (Constraints) – These are business rules or co-occurrence rules defined in the spec or profiles. Invariants are usually expressed in FHIRPath expressions. For instance, an Observation has an invariant that if there is no value, it must have a
dataAbsentReason
. If a resource violates an invariant, that’s a validation error. - Profiles – If a resource claims conformance to a specific profile (or an implementation guide requires certain profiles), all additional constraints in that profile must be validated too. Profiles can introduce stricter rules: new required fields, fixed values, specific extensions, and slicing of elements.
- Cross-resource rules & Workflow – Some validations may involve multiple resources or context. For example, a Bundle of type
document
must have a Composition as its first entry, or references between resources should be resolvable. Also, a QuestionnaireResponse should be valid against its defining Questionnaire. - Business Rules – These go beyond the FHIR spec: things like checking that there are no duplicate records, that certain references point to active patients, or that the user is authorized for the data. These are often implemented as custom validations by specific applications or jurisdictions.
Not every validation scenario will include all these layers. Basic validation (often done by most servers) covers structure and cardinality, and maybe obvious value errors. More advanced validation (e.g. in a compliance testing tool) will dive into terminology and profile rules.
Let’s also clarify when and where validation occurs in the data lifecycle:
- During Data Entry or Construction: If you’re building FHIR resources in an app or converting from another format (like HL7 v2 or CSV), doing some validation at source can catch errors early. For example, a UI form might enforce required fields (e.g. don’t let a user submit a Patient without a name) and basic formatting (dates, etc.).
- Client-Side Validation: Before sending data to a FHIR server, a client application might use a validation library or schemas to ensure the resource is valid. This can prevent needless round trips. For instance, a JavaScript client could use a JSON Schema or a FHIR JS library to check an Observation object locally.
- Server-Side Validation (On Ingestion): Many FHIR servers perform validation when data is received (via
$validate
operation or automatically on create/update). In fact, every FHIR server in production performs some validation – though how much varies. Some servers only do minimal checks (just enough to store data), others enforce full profile compliance. This is often configurable. For example, the Azure FHIR Service allows turning on profile validation on create/update via a header, but it’s off by default to optimize performance. Google’s Cloud Healthcare API similarly doesn’t fully validate every import unless asked, to achieve higher throughput for bulk data. - Post-Storage or Batch Validation: In some workflows, validation might be done asynchronously. A system might accept data, store it, but then run a background job to validate and flag any issues. This approach, while not blocking ingestion, requires carefully handling any errors found (possibly by alerting or later correction).
- Testing and QA: During development of interfaces or Implementation Guides, teams often run large sets of sample data through validators (like HL7’s validator CLI or test servers) to ensure conformance. For example, before deploying an update, you might validate 100 sample patient records against the new profile to see if any fail.
- Ongoing Data Quality Checks: Even in production, periodic validation checks can be part of a data quality pipeline. E.g., a nightly job might run
$validate
on random samples of data or new records and report issues. This can catch when upstream systems start drifting from the spec.
It’s important to note that validation can be strict or lenient. HL7 encourages following Postel’s Law: be conservative in what you send, liberal in what you accept. This means your systems should try to produce perfectly valid FHIR, but when receiving, you might choose to accept slight deviations (perhaps logging warnings) rather than rejecting outright, especially if data would be lost. Each organization has to set its policy: some regulatory contexts (like submission to a national database) might require 100% valid data, while internal systems might accept and correct minor errors.
Validation layers summary: The table below (adapted from the FHIR spec) shows various validation methods and which aspects they cover:
- XML/JSON Schema: Checks structure & basic cardinality, and simple data types. Doesn’t enforce advanced invariants or terminology.
- Schematron (XML) or JSON Schema with custom rules: Can implement some invariants and additional rules (e.g., schematron rules for invariants).
- ShEx (Shape Expressions): A grammar-based approach that can cover structure, cardinality, and some invariants (including slicing), though it’s less commonly used.
- FHIR Validator (reference implementation): The official validator (Java-based) covers everything: structure, cardinality, values, bindings (with a terminology server or pre-loaded expansions), invariants (via FHIRPath), profile rules, etc. It’s the most comprehensive tool.
- Custom/Business Rule Validators: Your application can add on checks for things the spec doesn’t cover (like “if Observation.code = X then Patient must have condition Y”, or “no two active encounters for same patient overlap in time”). These aren’t part of standard validation but are important in workflow context.
Understanding these fundamentals will help you interpret the errors we’re about to explore. Often, a validation error message will hint at which layer it comes from (e.g., “Required element missing” is cardinality, “Unknown element” is structural, “code invalid” is terminology, “invariant failed” is a constraint). In the next section, we’ll break down 20 common errors and map them to these categories with concrete examples.
Top 20 FHIR Validation Errors (and How to Fix Them)
In this section, we’ll go through twenty of the most common validation errors that developers encounter, especially when first integrating with FHIR. For each error, we’ll provide: the error message or type, an explanation of its cause, a real-world scenario where it might occur, and a solution – often illustrated with a JavaScript snippet or approach. We’ll also cross-reference how similar issues appear in other environments (Java/.NET) when relevant.
Note: In FHIR’s terminology, validation issues are often classified with an issue type code (such as required
, structure
, value
, invariant
, etc.). We’ll mention these where applicable, as they help identify the category of error.
For clarity, we will format each error as follows:
- Error Message: Actual message text you might see (or a summary)
- Cause: Why this error occurs
- Scenario: An example of when you might run into this error
- Solution: How to resolve or fix the error (with JS examples where possible)
Let’s dive in:
1. Missing Required Element (required
issue)
-
Error Message: “Missing required field: status” or “Required element missing:
Observation.code
” -
Cause: A mandatory element (min cardinality 1) is not present in the resource. FHIR defines many required elements – for example,
Observation.status
andObservation.code
are 1..1 (required) in the base spec, so omitting them triggers an error. The validator sees that the element’s minimum required occurrences is not met (0 present when at least 1 is needed). -
Scenario: This happens frequently when constructing resources manually or mapping from another system that doesn’t provide that field. For instance, an interface mapping lab results to FHIR might forget to set the Observation’s
status
. Another example: a Patient resource without anyidentifier
orname
when the profile or business rules expect one. In an Azure FHIR $validate example, a patient missing a required identifier and gender produced errors: “Instance count for ‘Patient.identifier.value’ is 0, which is not within the specified cardinality of 1..1” and similarly for gender. -
Solution: Add the missing element with an appropriate value. Determine what value is appropriate in context – some fields have coded enums. For
Observation.status
, for example, you might set"status": "final"
(or"preliminary"
, etc., depending on context). In JavaScript, if you have a resource object, you’d simply add the property:// Example: Fixing a missing Observation.status in a JS object let observation = { resourceType: "Observation", code: { text: "Blood Pressure" } // status is missing here }; // Validation would complain "Missing required field: status" observation.status = "preliminary"; // Provide a valid status code, e.g., 'preliminary'
Always use a value allowed by the spec. For statuses, FHIR defines an allowed set (e.g.
registered | preliminary | final | …
). If you’re not sure, check the FHIR spec or your Implementation Guide for the permitted values. Many validators will also accept"unknown"
if you truly have no idea, but use that sparingly and only if allowed (some profiles disallow unknown statuses).Cross-reference: In Java (HAPI FHIR), this error would appear as an
IssueType.REQUIRED
in an OperationOutcome, often with a message like “Element ‘Observation.code’ is required but missing”. In .NET (Firely SDK), you might see a similar message in the validation results indicating a cardinality failure. The fix in any language is the same: populate the missing element.
2. Missing resourceType
or Other Metadata
-
Error Message: “Invalid JSON content detected, missing required element: ‘resourceType’”
-
Cause: The fundamental
resourceType
field (which every FHIR resource must have) is absent. This is a special case of a required element missing, but it’s worth calling out because withoutresourceType
, the parser often cannot even identify what type of resource it’s dealing with. This usually results in a parsing error or exception (often aDataFormatException
in HAPI or a generic error “Cannot determine resource type”). -
Scenario: Very common when someone is new to FHIR and manually creates JSON. For example, you might construct a JSON object with patient data but forget
"resourceType": "Patient"
. The server or validator cannot process it because it doesn’t know it’s supposed to be a Patient resource. This can also happen if the JSON is slightly malformed and theresourceType
tag got dropped or misspelled ("resourceType": "Patietn"
– typo – would be seen as unknown element and also effectively missing the correct identifier). -
Solution: Include the correct
resourceType
property at the root. For each resource JSON, ensureresourceType
is present and exactly matches the name of the resource (case-sensitive). For example:// Constructing a Patient resource in JS let patient = { // "resourceType": "Patient", // This was missing, causing error active: true, name: [{ "family": "Doe", "given": ["John"] }] }; // Fix: patient.resourceType = "Patient";
If you already have the resource as JSON text, ensure the JSON starts like:
{"resourceType": "Observation", "id": "...", ... }
. Many tools (like FHIR serializers) add this automatically, but when building JSON by hand or mapping, it’s a common oversight.After adding it, try validation again. This should resolve the immediate parse error. Keep in mind
resourceType
must match the actual type’s definition – if you put"resourceType": "Patient"
but the content has fields that only an Observation would have, you’ll get other errors (unknown elements).
3. Unknown Element Found (structure
issue)
-
Error Message: “Encountered unknown element ‘birthPlace’ at location ‘Patient.birthPlace’”, or “Unknown element ‘foo’ found while parsing”
-
Cause: The resource contains a field that is not defined in the FHIR specification (or profile) for that resource. In other words, the JSON/XML has an unexpected property. This could be a simple typo (e.g., using
birthPlace
instead of the correctaddress
in Patient) or using an element from the wrong version/profile. It is a structural error – the validator flags that it doesn’t know what this element is. -
Scenario: Typos and case sensitivity mistakes are a common scenario. FHIR element names are case-sensitive (e.g.,
"BirthDate"
with a capital B would be invalid; it must be"birthDate"
). Another scenario is using an outdated or different version field. For instance, in DSTU2 (FHIR R2) there was aCarePlan.activity.simple
, which doesn’t exist in R4 – sending that to an R4 server yields unknown element errors. Or perhaps you read an example somewhere and added aPatient.birthPlace
element not realizing it isn’t part of core FHIR (it might have been an extension in that example). In .NET Firely validation, a common error is “Encountered unknown element ‘X’ at location ‘Resource.X’ while parsing”, which explicitly indicates the element is not allowed. -
Solution: Remove or correct the unrecognized element. First, double-check the spelling and casing of the element against the FHIR spec. For example, if you meant the patient’s place of birth, the core spec doesn’t have
birthPlace
– you would need to use an extension (like patient-birthPlace extension). If it’s a typo, fix the key name. If it’s a genuine extra data that doesn’t belong, you have options:- If an equivalent standard element exists, map your data to that. (E.g., maybe someone put
"nickname": "Jack"
in Patient – the correct representation is to usePatient.name
with a given name or a specific extension for nickname.) - If it’s a custom element that your use case needs and there’s no standard field, use a defined Extension with a proper URL instead of an ad-hoc field. Custom fields are not allowed unless expressed as extensions.
- If it was included by mistake and isn’t needed, simply remove it.
For instance, to correct a typo in JavaScript:
let patientData = { resourceType: "Patient", BirthDate: "1980-01-01" // Wrong casing, should be "birthDate" }; // Validator complains unknown element 'BirthDate'. patientData.birthDate = patientData.BirthDate; delete patientData.BirthDate;
After this fix,
patientData
will havebirthDate
which is the correct field. If the element was something unsupported likebirthPlace
, decide if you map it to an extension:// Using an extension for birthPlace (example) patientData.extension = patientData.extension || []; patientData.extension.push({ "url": "http://hl7.org/fhir/StructureDefinition/patient-birthPlace", "valueAddress": { "text": "Amsterdam" } }); delete patientData.birthPlace;
Now the data about birth place is in an extension that a validator will recognize (assuming that StructureDefinition is known or at least the format is correct). If you go this route, ensure you have the relevant extension definition available to your validator; otherwise, it will just see an unknown extension URL (which typically is a warning unless the profile forbids it).
Note: If the validator error mentions **“… is not allowed at this position”*, it might be you put a field in the wrong place hierarchically. E.g., having an
Observation.code
directly under a Bundle instead of inside an entry’s resource. Always check that your JSON hierarchy matches the FHIR structure (the official JSON schemas can help here). Tools like the one from Data4Life use JSON Schema to catch unknown properties – unknown elements clearly violate the schema. Removing or renaming them is the fix. - If an equivalent standard element exists, map your data to that. (E.g., maybe someone put
4. Cardinality Exceeded (Too Many Repetitions)
-
Error Message: “Instance count for ‘Patient.identifier’ is 5, which is not within the specified cardinality of 0..3”, or “Element X must not appear more than Y times”.
-
Cause: An element appears more times than allowed by its defined max. For example, if a profile or base resource says an element’s cardinality is
0..3
(maximum 3 occurrences) and you provide 5 entries, that’s an error. In FHIR, some fields are single-valued (max=1
), others allow multiple. Violation of max occurs either by repeating a field that shouldn’t repeat, or including an element that’s forbidden (max=0
means “must not be present at all”). -
Scenario: One scenario is a mistaken array versus single element. For instance,
Patient.gender
is a single-valued field (0..1
), but if someone represents it as an array (maybe thinking multiple genders could be listed) – e.g.,"gender": ["male", "female"]
– that will trigger an error (“gender must have at most 1 value”). Another scenario: a profile might restrict repeats. For instance, a national profile could say Patient.identifier (0..*) in base is restricted to 0..3 (no more than 3 identifiers). If a patient record had 5 identifiers, validation fails against that profile. Or consider elements likeObservation.value[x]
which is 0..1 – you can’t have both avalueQuantity
and avalueString
in the same Observation (that would be two “value” fields, violating cardinality, even aside from the choice type issue). Validators like the HL7 .NET SDK will report something like “Instance count for ‘X’ is N, which is not within the specified cardinality of …” as we saw in the Azure example for too few occurrences; the same style applies to too many. -
Solution: Ensure you do not exceed the allowed number of repeats. If you accidentally used an array where only one item is allowed, fix the structure. For example:
// Wrong: gender as an array patient.gender = ["male", "female"]; // Correct it to a single value: patient.gender = "male"; // or the appropriate single code
If an element truly has multiple items in your data but the profile forbids that many, you have to reconcile it:
- Perhaps you can drop some if they’re redundant (e.g., pick the primary identifier or split the resource into multiple).
- If
max=0
(element not allowed at all by profile), you must remove it entirely. For example, some profiles setCommunication.language
to 0..0 (disallowing the field) – if you have it, the only fix is to remove it to comply with that profile. - If you must include the data, then you’re not conforming to that profile – you might need a different profile or a conscious decision to not validate against it.
In JavaScript, ensure your JSON serialization doesn’t inadvertently produce an array of objects when only one object is expected. This can happen if your code always outputs arrays for consistency. Use arrays only for fields that truly repeat (check the FHIR spec cardinalities).
For the case of duplicate choice elements (like
valueQuantity
andvalueString
together), this is also a cardinality problem on the choice “value[x]” (which is 0..1 in total). The fix is to only include one of the choice options. If you mistakenly included multiple, decide which is correct and remove the other. If the intention was to convey multiple values, consider if you need multiple Observations or use components, but that’s a design change.Cross-reference: In HAPI FHIR’s OperationOutcome, this might appear as an
issue.severity = error
withissue.code = structure
orinvalid
and a text like “Too many repetitions of element X”. The underlying code in HAPI uses messages like “element: … has too many elements”. The JSON Schema approach would also flag this as a violation of a “maxItems” constraint. Regardless, trimming the list to the allowed maximum or splitting data is the resolution.
5. Incorrect Data Type (Type Mismatch)
-
Error Message: “Type checking the data: Since type XYZ is not a primitive, it cannot have a value”, or “Expected type Integer but found String”, “Invalid JSON type for element ‘valueQuantity’, object expected”.
-
Cause: The value provided for an element doesn’t match the expected data type. This can happen in a few ways:
- Providing a primitive where a complex type is expected, or vice versa.
- Using the wrong JSON structure for a data type. E.g., FHIR defines
valueQuantity
as an object with subfields (value, unit, system, etc.), but if someone just put"valueQuantity": 5.4
(a number) instead of an object, that’s a type error. - Using a string when an integer is expected (e.g.,
"value": "123"
as a string instead of number 123). JSON is type-sensitive and the schemas/validators check that. - Or a more subtle case: placing a primitive value in an element that should be a backbone element or complex structure. The Firely SDK error “Since type [type] is not a primitive, it cannot have a value” is referencing an XML scenario, but essentially means an element that should have children was given a direct value attribute incorrectly.
-
Scenario: A classic scenario is misunderstanding how choice elements work. For example, Observation has
value[x]
. If you want to send a numeric value, you should usevalueQuantity: {value: 5.4, unit: "mg/L", ...}
. If instead you dovalueQuantity: "5.4 mg/L"
(a single string), it’s wrong – the validator will complain that a valueQuantity must be an object (Quantity type). Similarly, if a field expects a boolean and you put"true"
(string) instead oftrue
(boolean literal), that’s a type mismatch. Another scenario: extension values. If an Extension is defined to havevalueCodeableConcept
, but you accidentally usedvalueString
with a complex object – type mismatch. These errors often come out when using JSON schema or the Instance Validator – e.g., “invalid type: expected object, got string at …”. -
Solution: Provide the value in the correct format/type as defined by FHIR. This usually means adjusting how you construct the JSON. Some tips:
- Check the FHIR spec or profile for the element’s type. For example, if it says
Quantity
, make sure your JSON is an object with appropriate subfields. If it saysstring
, ensure you give a JSON string. - For numeric types, do not quote them.
"value": 123
is a number, whereas"value": "123"
is a string (wrong if the field is integer). Likewise for booleans (true
/false
not"true"
). - If you inadvertently put a primitive in a complex, restructure. The Firely error example likely came from an XML context: in XML, primitive types have a
value="..."
attribute, whereas complex types have child elements. In JSON, this translates to: primitives are simple JSON values, complex are objects. So do not try to collapse a complex type into a single value.
Example fix in JS for an Observation value:
// Wrong: valueQuantity given as a simple string (incorrect type) observation.valueQuantity = "5.4 mg/L"; // Correct: valueQuantity as an object observation.valueQuantity = { value: 5.4, unit: "mg/L", system: "http://unitsofmeasure.org", code: "mg/L" };
Another example, fixing a boolean:
// Wrong: expecting a boolean, got string patient.active = "true"; // Should be boolean true, not string // Fix: patient.active = true;
If you use a library like AJV with FHIR JSON schemas, it will catch these type mismatches easily (with messages about expected type X). If you’re manually constructing, always follow the schema. The FHIR spec’s JSON schema and documentation pages for each resource list the type of each field.
Cross-reference: In .NET or Java, these issues might even surface at parse time (e.g., the parser might throw an error if a field is wrong type). For instance, HAPI’s parser might say “Failed to parse integer value” if a string is in an integer field. The official validator will list it as an OperationOutcome issue of type
structure
orvalue
. By correcting the JSON types as shown, the error will be resolved. - Check the FHIR spec or profile for the element’s type. For example, if it says
6. Invalid Format or Pattern (value
issue)
-
Error Message: “Element value invalid: Value ‘20211301’ does not match format yyyy-mm-dd”, “Invalid format for identifier (must match regex …)”, or “Validation failed for ‘$.birthDate:pattern’ constraint(s)”.
-
Cause: The content of a field doesn’t conform to the expected format or pattern. FHIR data types often have specific formats: dates, datetimes, URIs, IDs, codes, etc. If you provide a value that doesn’t meet those criteria, it’s considered invalid. Examples: a date string that isn’t ISO 8601, an
id
with illegal characters, a code containing a space when spaces aren’t allowed, a UUID that’s not properly formatted, etc. Many of these are validated via regular expressions in the spec. -
Scenario: Here are a few common ones:
- Date/Time Formats: FHIR dates must be in
YYYY-MM-DD
(for date),YYYY-MM-DDThh:mm:ss+zz:zz
(for dateTime with timezone) etc. If someone provides20211301
for 2021-13-01 (an invalid month “13”), or01/31/2021
(US format) instead of2021-01-31
, the validator will throw an error. Ballerina validation example given in a Medium post shows an error like “Validation failed for ‘$.birthDate:pattern’ constraint(s).” meaning the birthDate didn’t match the required pattern. - Identifier and ID patterns:
Resource.id
and identifier values have restrictions.id
(the resource id) must match the regex[A-Za-z0-9\-\.]{1,64}
– if you include a space or special character, error. For example, an id like “Patient/123” is invalid (should just be “123” without “Patient/”). Or an id “abc_def” with underscore is invalid because underscore is not allowed. - Code and Coding: If a field is
code
(a coded primitive), it cannot contain spaces and usually must match a specific set of allowed chars (basically printable chars but no whitespace). For instance, acode
like"not applicable"
(with space) is invalid. Also, if a code is supposed to come from a known set and you give something outside that set’s pattern (like a LOINC code must be like digits separated by hyphen and a check digit, e.g.,1234-5
– if you gave12345
without hyphen, the validator might say it’s not a valid code format for that system). - URI/URLs: If an element expects a URI and you give something not URI-like, might error. E.g., a
namespace
field expecting a URI but you gave “Not a URL”. - FHIRPath constraints: Some patterns are enforced via invariants. For example, Identifier.value has an invariant that if system is present, value must be present (min 1 if system exists). But format-wise, we mostly consider regex patterns here.
- Date/Time Formats: FHIR dates must be in
-
Solution: Conform to the specified format for the data type or element. The FHIR specification is your friend here – for each data type it lists the pattern or format. Also, many validation messages actually tell you the expected pattern or example. For instance, an error might explicitly say what format was expected (like the birthDate example expected
YYYY-MM-DD
).Some fixes:
-
For dates, times: Use a proper date library to format if possible. In JS, if you have a Date object, use functions to format as ISO string. Or use moment.js / dayjs to ensure
YYYY-MM-DD
format. Do not include timezone if not needed (e.g., Patient.birthDate is just a date with no time – provide exactly 10 characters “YYYY-MM-DD”). -
For IDs and identifiers: Strip or replace illegal characters. If your local IDs have spaces or other punctuation, you might need to encode them or generate a FHIR-compatible ID. For instance:
let patient = { resourceType: "Patient" }; patient.id = "123 456"; // has a space, invalid patient.id = patient.id.replace(/\s+/g, '-'); // e.g., replace spaces with hyphen, now "123-456" // Also ensure length <= 64.
It’s often better to keep IDs simple (alphanumeric and hyphen/dot).
-
For codes: If you have a code with a space and it’s not allowed, either remove the space or, if the concept label had a space, you might be using the wrong field (maybe that should be in
display
not incode
). E.g.,maritalStatus
in Patient expects a code like “M” or “S” from a known set – if someone put “Married” as the code, that’s wrong (the display is “Married”, the code should be something like “M”). So align with the required code system. The validator might also give a terminology error in that case, which we’ll handle in the next item. -
Check known regex patterns: For instance, FHIR Identifiers often have to follow the issuing system’s rules, but FHIR itself doesn’t regex the identifier value broadly (aside from length). FHIR id (resource id) does have a regex. FHIR datetime does (a fairly strict subset of ISO). If you see a pattern error, search the spec for that element’s definition and you will usually find something like “must match the pattern …”.
In JavaScript, using regex or library functions can help format. Example fix for date:
// Given a date in MM/DD/YYYY format, convert to YYYY-MM-DD for FHIR function toFhirDate(mmddyyyy) { const [m,d,y] = mmddyyyy.split('/'); return `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}`; } patient.birthDate = toFhirDate("01/31/2021"); // becomes "2021-01-31"
Always ensure timezone offsets in dateTime if present. E.g., if you have a datetime, include a
Z
or +/- offset. “2021-01-31T14:30:00” by itself is not a valid full dateTime (no timezone specified); make it “2021-01-31T14:30:00Z” (UTC) or e.g. “-05:00” offset.After formatting correctly, the validator’s pattern constraint should pass. A quick sanity check: use a small JSON Schema validator or a regex test for fields like IDs in your pipeline to catch obvious format issues early.
-
7. Code Not in ValueSet (Invalid Code) (code-invalid
issue)
-
Error Message: “Unknown code ‘XYZ’ for ValueSet ‘http://hl7.org/fhir/ValueSet/observation-status’”, “The code provided is not valid in the required valueset”, or “Coding ‘ABC’ is not in the bound ValueSet [id]”.
-
Cause: A coded element’s value isn’t part of the allowed set of codes (ValueSet) as required by the specification or profile. FHIR uses terminology bindings to specify what codes can be used. If the binding is “required” or “extensible” and your code isn’t in the set, validators will complain. Essentially, the code is unrecognized or not acceptable in context. This might also occur if the code system isn’t recognized or if no system is provided and one is expected. The official code for this type of issue is often
code-invalid
. -
Scenario: Common scenarios include:
- Using a local or custom code where a standard code is required. For example, Observation.status in base FHIR is bound to a fixed set (
registered | preliminary | final | ...
). If someone providesstatus: "done"
orstatus: "completed"
(not in that set), it’s invalid. The validator might say “Unknown code ‘completed’ for ValueSet ‘…/observation-status’” (since “completed” isn’t an allowed status in FHIR R4, the allowed code is “final” for that meaning). - Using an outdated code system reference. Perhaps the profile expects SNOMED CT codes for a condition, but an ICD-9 code was used.
- ValueSet expansions not loaded: sometimes the error can be triggered if the validator doesn’t have the expansion of a ValueSet. It might say “Unknown CodeSystem” or “Cannot validate code, unknown system” – but here we focus on the “code is not in valueset” scenario.
- An example from a Google Group: *“Unknown code ‘document’ for in-memory expansion of ValueSet ‘http://hl7.org/fhir/ValueSet/bundle-type’”*:contentReference[oaicite:41]{index=41} – meaning “document” wasn’t recognized in that context (maybe wrong version or not expanded).
- Using a local or custom code where a standard code is required. For example, Observation.status in base FHIR is bound to a fixed set (
-
Solution: Use an appropriate allowed code or adjust the binding. There are a few angles:
- If you intended a standard code: Correct the code to one from the required set. E.g., for Observation.status, use
"final"
instead of"completed"
. Or if a Condition category expects a LOINC code and you gave something else, find the closest matching allowed code. Often the spec or IG will list the codes or reference a well-known set. - If you used a custom code knowingly: If the binding strength is “extensible” or “preferred”, you might be allowed to use others but with a warning. If it’s “required”, you really can’t use an out-of-set code. In some cases, you may consider using an extension or providing your code in
CodeableConcept.text
if it truly isn’t in the set (but then technically it fails required binding – required means must come from the set). - Ensure that you include the
system
for any coding. Sometimes a code is flagged invalid because the system was missing, so it couldn’t be validated properly. For instance, sending{"code": "1234-5"}
without a system – the validator can’t know it’s LOINC, so it might say invalid code (or assume unknown system). Always include"system": "http://loinc.org"
(for example) if a code system is implied. - If dealing with a ValueSet you manage (in an IG), make sure the validator knows about it (e.g., load the IG package). Otherwise, it might not recognize any code as valid because it doesn’t have the list. In Azure’s validator, they mention you must upload the ValueSets (with expansions) for the server to validate those.
Example in JS: Suppose a profile requires administrativeGender from HL7’s set (
male, female, other, unknown
) and you have:patient.gender = "unk"; // a short code not in the allowed set
The validator will likely flag this. The fix is:
patient.gender = "unknown"; // use the proper code from the ValueSet
Another example: a codeable concept for a Condition code required to use SNOMED:
condition.code = { text: "Flu", coding: [{ system: "http://acme.org/codes", code: "FLU123" }] };
If the profile says use ICD-10 or SNOMED,
acme.org
code “FLU123” is invalid. Solution:-
Find the ICD-10 or SNOMED code for Flu (say SNOMED code “6142004” for Influenza).
-
Replace the coding:
condition.code.coding = [{ system: "http://snomed.info/sct", code: "6142004", display: "Influenza" }]; condition.code.text = "Flu";
Now it’s an allowed code.
If you cannot change the code (maybe your system doesn’t have a mapping yet), one strategy in “extensible” binding cases is to send your code and perhaps also send a translation or leave a text. But with “required”, you really have to conform.
Cross-reference: A
code-invalid
issue in OperationOutcome will often haveseverity: error
andcode: "value"
orcode: "code-invalid"
, with diagnostics like “The code provided (XYZ) is not in the value set…”. In HAPI, if using terminology validation, it might integrate with a terminology service to say code not found. The best solution is always to align with the expected terminology or load the appropriate codes. For testing, you can disable terminology check to get other errors first, but eventually you need to address the invalid codes for production compliance. - If you intended a standard code: Correct the code to one from the required set. E.g., for Observation.status, use
8. Invariant Violation (Constraint Failed)
-
Error Message: “Validation rule failed: Observation must have a value or a dataAbsentReason (obs-3)”, “Invariant [id] failed: {description of rule}”, or a generic “Constraint violation: …”.
-
Cause: A logical invariant rule specified in the base spec or a profile was not satisfied by the resource content. Invariants are often denoted by an ID (like
obs-3
,pat-1
, etc.) and have a human description. They cover conditional requirements (if X is present, Y must be present, etc.) or other co-occurrence and content rules beyond simple cardinality. The validator evaluates these (usually via FHIRPath expressions) and flags any that return false. -
Scenario: There are many FHIR invariants; some examples:
- Observation invariants: In base FHIR, one invariant (let’s call it obs-3 for illustration) says: “If Observation.value[x] is not present, then Observation.dataAbsentReason must be present (and vice versa)”. If you create an Observation without a value and also without a dataAbsentReason, you’ll get an error about this rule. Conversely, if you erroneously include a dataAbsentReason and a value, that also breaks the invariant (should be one or the other).
- Patient invariants: An example invariant on Patient (pat-1 in some profiles) might be that a Patient must have at least one identifier or name or telecom – i.e., not completely empty of identifying info. If you made a Patient with just
resourceType: "Patient"
and nothing else, a rule might complain “a patient must have a name or identifier or official id” (depending on jurisdiction). - DomainResource invariants: FHIR says if a resource is contained inside another, it must not have certain elements (no
meta.versionId
, etc.). If you violate that by having a contained resource with a versionId, the invariantdom-2
triggers. - Profile-defined invariants: Implementation Guides can define custom rules. For example, an IG could say “if Observation.category = Laboratory, then Observation.code must be from LOINC” (a constraint linking two fields).
- The error message might not always be super friendly, but often includes the invariant description. For instance, “All FHIR elements must have a @value or children (ele-1)” is an invariant (ele-1) that fires if an element exists but is empty (no value and no children).
-
Solution: Make the resource satisfy the rule’s condition. This requires understanding the rule logic:
-
If it’s a missing field scenario (like Observation missing value and missing dataAbsentReason), fix by adding the required counterpart. For example:
if (!observation.valueQuantity && !observation.valueString && !observation.valueCodeableConcept) { // no value provided, so ensure dataAbsentReason is there observation.dataAbsentReason = { "coding": [{ "system": "http://terminology.hl7.org/CodeSystem/data-absent-reason", "code": "unknown", "display": "Unknown" }] }; }
This ensures that if you have no value, you supply an explanation via
dataAbsentReason
. Alternatively, if you have a meaningful value, then you remove anydataAbsentReason
. Only one of the two should be present. -
If the invariant says “either X or Y must exist”, make sure at least one is there. If none are, add one. If both are present but shouldn’t be, remove one. For example, ext-1 invariant on Extension says either
value[x]
or sub-extension
children, not both. So if you accidentally did an extension with both, decide if your data should be in one form or split into sub-extensions. -
If the rule is a cross-field dependency (if A, then B), ensure that condition holds. E.g., a MedicationRequest might have an invariant “if status = completed, there must be a authoredOn date” (hypothetical example). So add the missing authoredOn if status is completed, or change status if no date is available (depending on correct data).
-
Some invariants enforce calculations or references (rare). For those, ensure the data meets the criteria (e.g., an invariant might say “Reference must resolve to a resource of type X” – which overlaps with reference type issues).
In practice, the OperationOutcome often directly tells you the human-language requirement that was violated. Using that, adjust the resource. Another example: “Bundle.type=‘document’ requires a Composition as the first entry”. If you get that, you know to fix your Bundle accordingly (see Bundles section below).
For an empty element invariant (ele-1: “must have a value or children”), this usually happens if your JSON has an empty object
{}
somewhere. The fix is to remove that empty object or populate it properly. For instance:// Wrong: extension with neither sub-extensions nor value observation.extension = [ { "url": "http://example.org/fhir/StructureDefinition/foo" } ]; // This triggers ele-1 (empty element) and likely ext-1 too. // Fix: either give it a value or remove if not needed: observation.extension[0].valueString = "Bar"; // or if it was placeholder, remove the extension entirely if not used.
After making the resource meet the invariant’s conditions, re-run validation and that error should disappear.
Cross-reference: The HL7 validator and others will use issue
invariant
orstructure
with details including the invariant ID and description. This is immensely helpful. Also, you can often find the invariant by ID in the FHIR spec. If unsure, search the spec for that ID (like “obs-3 FHIR”) to see the exact rule. That can guide your fix. -
9. Extension Constraint Violations
-
Error Message: “ext-1: Must have either extensions or value[x], not both”, “Extension URL missing or invalid”, or an empty element error related to extensions.
-
Cause: Building on the previous invariant discussion, the Extension rules in FHIR have specific constraints:
- ext-1: An Extension element must have either a
value[x]
or nestedextension
children, but not both. If you populate an extension incorrectly with both, or with neither, you violate this. - ext-2: Every extension must have a
url
. If you forgot to provide theurl
or it’s blank, that’s an error. - Context constraints: Some profiles restrict which extensions can appear (or disallow unknown ones). If you include an extension that isn’t allowed by the profile (and the profile says no additional extensions or requires specific ones), you’ll get an error.
- ext-1: An Extension element must have either a
-
Scenario: A developer might create an extension like:
{ "url": "http://example.com/fhir/StructureDefinition/myExt", "valueString": "test", "extension": [ { "url": "...", "valueCode": "ABC" } ] }
Here they included both a valueString and a nested extension – violating ext-1. Another scenario: forgetting the URL:
{ "valueInteger": 5 }
as an extension entry is invalid because
url
is missing (the validator might treat it as unknown element or extension with no url). Also, if a profile says “no other extensions except X”, and you include Y, that’s a profile validation error (though the error might be phrased as “Element not allowed” or “Profile constraint …”). -
Solution: Correct the structure of the extension according to FHIR rules.
-
For ext-1 (not both value and children): Decide which one your extension needs. If your extension is simple (single value), then remove any nested
extension
array from it. If it’s meant to be complex (with sub-extensions), then remove the top-levelvalue[x]
. In other words, one or the other. To correct the example above:// If the extension is meant to carry a string value only, drop the sub-extension: myExtension = { "url": "http://example.com/fhir/StructureDefinition/myExt", "valueString": "test" }; // The nested extension with code "ABC" should perhaps be a separate extension or not used.
Alternatively, if it was meant to convey two pieces of info, define the extension to use sub-extensions:
myExtension = { "url": "http://example.com/fhir/StructureDefinition/myComplexExt", "extension": [ { "url": "subpart1", "valueString": "test" }, { "url": "subpart2", "valueCode": "ABC" } ] }; // (No valueString at the top-level in this case)
This satisfies ext-1.
-
For missing url: Always include a
url
for each extension entry. Theurl
should be the canonical URL that defines that extension’s meaning (often an official or custom StructureDefinition URL). If you don’t have one and you’re just testing, you can use something like"http://example.com/fhir/StructureDefinition/temp"
as a placeholder, but in real scenarios define your extensions properly. Without a URL, receivers won’t know what the extension represents. -
For profile extension rules: If a profile disallows an extension you used, you either remove that extension or adjust the profile (if you control it). Some systems choose to ignore unknown extensions (treat them as errors or warnings based on policy). Check the profile documentation; sometimes they explicitly list allowed extensions. Stick to those. If you have a use case for a new extension, you might need to get it approved or at least expect that strict validators will flag it. In a robust pipeline, you might choose to log a warning for an unexpected extension rather than fail, but that depends on requirements.
Code example: ensuring ext-1 compliance in JS:
// Given an extension object ext: if(ext.value !== undefined && ext.extension !== undefined) { console.error("Extension has both value and sub-extensions. Removing sub-extensions."); delete ext.extension; } if(ext.value === undefined && ext.extension === undefined) { console.error("Extension has neither value nor sub-extensions. This is invalid."); // decide what to do: remove extension or supply a value. }
(Note:
ext.value
here stands for anyvalueX
property.)Ensuring
url
presence:if(!ext.url) { throw new Error("Extension missing URL"); }
After these fixes, extension-related errors should resolve. The Data4Life JS validator noted that extensions and profiles beyond structure aren’t fully supported by JSON schema alone, so such rules are often enforced by the FHIRPath invariants (like ext-1) and profile validators.
-
10. Profile Constraint Violation
-
Error Message: “Profile requirement violation: Element Patient.name is mandatory in profile X”, “Element does not match fixed value in profile Y”, or “Profile X constraint: … failed”.
-
Cause: The resource fails to meet some constraint imposed by a StructureDefinition/Profile it’s supposed to conform to. This is a broad category – it could be missing a required element that the base spec didn’t require but the profile does, an element value not matching a fixed/pattern value specified by the profile, an unsupported element present (max 0 in profile), or not meeting a specified ValueSet in the profile (beyond base). Essentially, any rule in the profile’s differential that isn’t satisfied.
-
Scenario: For example:
- The US Core Patient profile requires at least one patient identifier of a specific type and a
name
with certain parts. If you validate a Patient against US Core and it has no identifier or name, you’ll get errors referencing US Core profile rules (even though base Patient allows no identifier). The Azure validation example showed a profile that requiredPatient.gender
and an identifier, giving errors when those were missing. - A profile might fix a value for an element. E.g., an Immunization profile might fix
status
= “completed” (meaning only completed immunizations). If you send one with status “entered-in-error”, the validator will report it doesn’t match the fixed value. Or a profile fixesObservation.code
to a specific LOINC – any other code will error. - Slicing issues (which we’ll treat more in the next item) are also profile-specific: e.g., an identifier slice requiring one identifier of type “MRN”. If that slice condition isn’t met, it’s a profile violation.
- Profile not declared: Sometimes, if you told the validator to validate against a certain profile by URL, it expects
Resource.meta.profile
to include that URL. If not, it might warn or error. Or vice versa: your resource hasmeta.profile = [url]
but you didn’t supply that profile definition to the validator – then it can’t validate fully and may throw an error or warning that profile is unknown (some validators will just skip unknown profiles with a warning).
- The US Core Patient profile requires at least one patient identifier of a specific type and a
-
Solution: Adjust the resource to meet the profile’s requirements (or choose the correct profile to validate against). Steps:
- Read the profile documentation or snapshot. Identify what the constraints are. For required elements, add them. For disallowed elements, remove them.
- For fixed values: Set the element exactly as required. For example, if profile fixes
Observation.category
to a certain CodeableConcept, use those exact coding(s). If profile fixes an extension URL must have a certain value, ensure it’s so. - For pattern: If the profile uses
pattern[x]
, your value must at least have those components. E.g., pattern CodeableConcept with a certain coding means your CodeableConcept must include that coding (could have extras if not prohibited). - If your data inherently doesn’t satisfy the profile (e.g., missing something you cannot supply), you have a choice: obtain the missing data from source, or you cannot claim conformance to that profile. In the latter case, you might remove the
meta.profile
claim or use a different profile that matches reality. However, in many systems (like US Core compliance), you have to make the data fit. - Example: Profile says Patient.name is 1..* (at least one). Your data has none. Solution: populate at least a placeholder name (if allowed). Maybe use
{"text": "Unknown"}
or similar if policy allows, or better, get the real name from source. Another: profile says Patient.gender is mandatory – if unknown, you might put"unknown"
(with appropriate code system) rather than leaving it blank. - For max 0 elements in profile: e.g., some profiles might set Communication.language to 0 (meaning don’t include it). If your resource has it, remove it when using that profile.
- Ensure you include the profile’s canonical URL in
meta.profile
if you want to declare conformance – some validators use that to automatically apply profile rules. But note the HL7 spec says you don’t have to validate a resource againstmeta.profile
unless explicitly requested – still, many do check it. It’s a good practice to include it if data is supposed to conform, so it’s self-identifying.
JavaScript example: adjusting to a profile:
// Suppose profile requires at least one identifier with system X. if(!patient.identifier || patient.identifier.length === 0) { patient.identifier = []; } // Add a required identifier if not present patient.identifier.push({ system: "http://hospital.example.org/MRNs", value: "12345" }); // Ensure meta.profile includes the profile URL patient.meta = patient.meta || {}; patient.meta.profile = patient.meta.profile || []; const profileUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"; if(!patient.meta.profile.includes(profileUrl)) { patient.meta.profile.push(profileUrl); }
If the error was about a fixed code:
// Profile fixes Observation.code to LOINC 1234-5 observation.code = { coding: [{ system: "http://loinc.org", code: "1234-5", display: "Example Code" }], text: "Example Code" };
Even if your original code was different, you must use the fixed one if conforming to that profile.
In summary, align every deviating element with the profile. After that, run validation against that profile again. It should pass those constraints. If there are multiple profiles (e.g., meta.profile has 2 entries), you have to satisfy all of them – which can be tricky if they conflict, but typically one wouldn’t declare conflicting profiles on the same resource.
11. Slicing Errors
-
Error Message: “No slice for element X matches the instance”, “Multiple slices match for element Y”, or “Slice validation failed: slice ‘abc’ required but not found”.
-
Cause: Slicing is a profiling feature where an array element (like multiple identifiers, multiple codings, etc.) is divided into slices based on some criteria (discriminator). A slicing error means the instance’s data in that array doesn’t conform to the slicing rules:
- No slice matches: The data provided doesn’t fit any of the defined slices’ patterns.
- Multiple slices match: The data is ambiguous and fits criteria for more than one slice when it should only belong to one (slices are usually defined to be mutually exclusive by discriminator, but if the data isn’t distinct enough, this can happen).
- Missing required slice: A particular slice was marked
min=1
(required at least one entry of that kind), but none in the instance fulfill that criteria.
-
Scenario: A classic example is Patient.identifier slicing. Imagine a profile slices
Patient.identifier
into slices: one for MRN, one for SSN, etc., using the system as a discriminator (system = hospital MRN system vs system = SSN system). If the patient you send has an identifier that doesn’t have a system matching any known slice (say an identifier with system “driver’s license”), the validator might say “no slice matches this identifier”. Or if the profile requires at least one MRN (min 1 for MRN slice) and you provided none, you’ll get an error about missing that slice. Another example: Observation.category in some IGs – they might slice category to expect one entry with system X for a specific category. If you provide categories that don’t include that, error. Or Bundle.entry slicing (like expecting certain types of resources in certain entries positions – less common, but e.g., first entry must be Composition in a document bundle, effectively a slicing by position or type). If not met, error. Multiple matching slices could occur if slices aren’t defined with exclusive discriminators and your element accidentally has properties that satisfy two slice conditions – this is more an IG design issue, but the validator will flag it. -
Solution: Make sure your data fits the profile’s slicing expectations. Steps:
-
Review the profile’s slice definitions for the element in question. It will tell you how slices are differentiated (by
value
of some sub-element, or by type, or pattern, etc.) and what is required. -
To fix “no slice matches” for an item: Possibly you’ve included an item that the profile didn’t anticipate. You might need to remove that item or change its properties so it matches one of the defined slices. For example, if an identifier’s system is not recognized in any slice, and that identifier isn’t required, maybe drop it or change the system to one of the expected ones if it was supposed to be that. If the data is important but the profile didn’t cover it, that might be a case for profile extension or just acknowledging you can’t fully conform.
-
To fix “missing slice”: Add an element that fulfills that slice. E.g., if “Medical Record Number” slice is required, ensure one of the identifiers has the system (or type coding) that the profile expects for MRN. Concretely:
// The profile expects an identifier with system "http://hospital.org/MRN" const hasMRN = patient.identifier?.some(id => id.system === "http://hospital.org/MRN"); if(!hasMRN) { patient.identifier = patient.identifier || []; patient.identifier.push({ system: "http://hospital.org/MRN", value: "ABC123", type: { coding: [{ system: "http://terminology.hl7.org/CodeSystem/v2-0203", code: "MR" }], text: "Medical Record Number" } }); }
Now the MRN slice is satisfied.
-
For “multiple slices match”: You might need to adjust the data to more clearly fall into one slice. This is rarer; it could mean your element has overlapping properties. If, say, two slices were distinguished by code in different ValueSets and your code accidentally is in both sets, you might have to refine it. Often, IG authors try to avoid such situations, but it can happen.
-
Always ensure each array element that should go into a slice has whatever discriminator fields the profile expects. For instance, if slicing by Observation.component code, you must have a component with that exact code for each slice that’s required.
As a simple real example: US Core Vital Signs profile slices Observation.component for blood pressure: one slice for systolic (LOINC 8480-6) and one for diastolic (LOINC 8462-4). If you provide a blood pressure Observation without one of those (say missing diastolic), you’ll get a missing slice error. The fix is to include the diastolic component. If you include an extra component with a different code, it may say “no slice for that component” – the fix might be to remove it or accept a warning depending on binding (US Core likely forbids extras in that case).
After adjusting to fulfill slices (and only those slices), your resource should pass slicing validation. This can be one of the trickiest parts of profile conformance, so don’t be discouraged – often it requires iterative tweaks.
-
12. Reference Type Mismatch
-
Error Message: “Reference type Patient is not allowed for this element (expected Reference(Group|Device|Location))”, “Resource type mismatch for reference: expected X, got Y”.
-
Cause: The resource referenced by a reference field is of a type that is not permitted by the definition. In FHIR, many reference fields limit which resource types are valid targets. For example,
Observation.subject
can reference a Patient, Group, Location, Device (depending on context) but not a Practitioner. If you reference a Practitioner as the subject, validation fails. This is a structure/type error: the Reference doesn’t resolve to an allowed type. Note this is about the type of the reference, not whether the actual resource exists (that’s a different error). -
Scenario: A common scenario is misunderstanding the allowed references. For instance,
Encounter.subject
should be a Patient in most cases. If someone tried to reference a Practitioner as the subject of an Encounter, that’s invalid (a Practitioner can be an encounter participant, but not the subject – the subject is the patient or group of patients). Another scenario:DocumentReference.content.attachment
might allow only certain content types or references, etc. OrCondition.subject
must be Patient or Group; referencing a RelatedPerson would break it. In a profile, these constraints can be even tighter (maybe an Observation is only supposed to refer to Patient, not Group). If you build references as plain strings (like{"reference": "Practitioner/123"}
) without considering allowed types, you might hit this. Validators will catch it by comparing the reference string prefix (or resourceType if resource provided inline) to the allowed list. -
Solution: Provide a reference to an allowed resource type, or correct the field used for that reference.
-
If you mistakenly used the wrong field, consider if there’s another field for that relationship. For example, if you set Encounter.subject = Practitioner/123, the fix is to remove that (since subject must be patient) and instead maybe add Practitioner/123 as an
Encounter.participant.individual
. That is the correct place for practitioner involvement. -
If the reference truly should point to a resource of disallowed type, you have a design problem: either the data model is wrong or you need an extension to allow that relationship. Usually, it’s a mistake.
-
To fix, change the reference to an allowed type. If for test you don’t have a Patient, you might reference a Group (if logically a group of patients) or if it was simply wrong, remove it.
-
Example: Observation.subject allowed [Patient|Group|Device|Location]. If you had a scenario where you thought subject could be a Practitioner (maybe you mis-used subject to track who reported something), correct by using Observation.performer for Practitioner or some other field. And ensure subject is a Patient (or Group etc.) as intended.
-
In JavaScript, if you have the reference as a string, you might validate it like:
function isAllowedReference(ref, allowedTypes) { // ref like "ResourceType/id" or "ResourceType/id/_history/x" const refType = ref.split('/')[0]; return allowedTypes.includes(refType); } let allowed = ["Patient","Group","Device","Location"]; if(!isAllowedReference(observation.subject.reference, allowed)) { console.error("Subject reference type not allowed:", observation.subject.reference); // Fix: assign a proper reference observation.subject.reference = "Patient/" + patientId; }
Of course, you need a patientId available. If not, this observation might be invalid to send.
-
If the reference is contained (like
reference: "#someContainedRes"
), ensure the contained resource is of an allowed type. If not, same issue.
After correcting the reference to an appropriate resource type, that error will be resolved. The HL7 validator tends to produce messages like “Resource reference is of wrong type”. In the Firely .NET validator, it might come through as a structure error with the expression pointing to the reference element, and saying expected types vs found. Align those and you’re good.
-
13. Unresolvable Reference (Not Found)
-
Error Message: “Reference could not be resolved: Resource Patient/123 not found”, or OperationOutcome with
issue.code = not-found
and message about a reference. -
Cause: The validator or server attempted to resolve a reference to ensure it exists or is valid, but could not find the referenced resource. This is slightly outside pure structural validation – it’s more of a consistency check often done by servers or validator with access to a resource context. For example, if you call
$validate
on a server with mode=update, it might check that references to other resources (likesubject: Patient/123
) actually exist on that server. A pure off-line validator might not do this unless you provide a bundle or context. But many real-world validations include this to prevent dangling references. -
Scenario: You post a Composition referencing Practitioner/999, but Practitioner/999 doesn’t exist in the server’s database – the server validation returns an error. Or using a
$validate
with a bundle that has references to external resources the server can’t find. Another scenario is an application-level check: e.g., you have a Bundle with entries and internal references (#refs or between entries) and one reference doesn’t match any entry or contained resource – validator can flag that (“Entry reference Encounter/ABC not found in bundle”).- In a contained resource scenario, if you reference
#abc
but no contained resource hasid="abc"
, that’s an error. - If references are supposed to be resolved externally and you’re offline, some validators might skip or warn. But servers often enforce that you’re not referencing things that don’t exist (especially on create if referential integrity is enforced).
- In a contained resource scenario, if you reference
-
Solution: Ensure all references are valid and resolvable in context.
- If validating on a server, either load or create the resources being referenced or remove/fix the references. For instance, if Observation.subject references Patient/123, make sure Patient/123 exists on that server (perhaps create that Patient first). For validation without actual creation, you might need to provide those resources in the input (like in a bundle or via the validator’s fetch mechanism).
- Some validators allow you to provide a custom resolver or supply a bundle of supporting resources. For example, the HL7 CLI validator can take multiple files – if your Observation references a Patient, give both to the validator so it can resolve. In a pipeline, if you know you’ll be validating references, consider retrieving necessary resources or mocking them.
- If a reference is simply incorrect (typo in ID, or outdated ID), correct it to the correct identifier.
- For contained references (local #), fix any broken links. E.g., if
Observation.subject.reference = "#gen1"
but the contained resource id was “gen01”, either change one to match the other. - If you cannot immediately supply the target resource in a test environment, you might ignore or accept such an error as a warning if your validation focus is structural. But in production, you likely want to ensure references resolve to real records.
Example: validating a bundle with internal references in JavaScript context:
// Pseudo-code to verify contained references: function verifyContainedReferences(resource) { let containedMap = {}; (resource.contained||[]).forEach(cont => { if(cont.id) containedMap["#"+cont.id] = true; }); // Also map bundle entries by fullUrl if needed. // For simplicity, check contained only: traverse(resource, (node) => { if(node.reference && node.reference.startsWith("#")) { if(!containedMap[node.reference]) { console.error("Unresolved contained reference:", node.reference); } } }); }
(Where
traverse
iterates through all reference fields.)On a server, typically the solution is to ensure order of operations: create or validate referenced resources first. If using a transaction bundle, include the referenced resources in the bundle.
Once references are resolvable (or at least the validator is given the context to resolve them), those errors will disappear. If using HL7 $validate, note that by spec it’s allowed to do reference resolution if mode is specified (e.g., update mode might require it).
14. Bundle Structure Errors
-
Error Message: “Bundle.type is ‘document’ but first entry is not a Composition”, “Bundle.entry[0].resource must be Composition for document bundles”, “Transaction Bundle entry missing request data”.
-
Cause: The content of a Bundle doesn’t meet special rules associated with its type. FHIR Bundles have specific rules for certain bundle types:
- If
Bundle.type = document
, the first entry must be a Composition (that serves as the document’s root). If not, error. - If
Bundle.type = message
, the first entry must be a MessageHeader. - If
Bundle.type = transaction
orbatch
, each entry must have arequest
element with method, URL, etc. If any entry lacks those or they’re improperly formatted, validation fails. - If
Bundle.type = transaction
and you have duplicate fullUrls or inconsistent references, might be flagged (though that might be runtime). - Also, some IGs slice Bundle entries by resource type for expectations.
- If
-
Scenario: A common one is trying to construct a FHIR Document. Say you create a Bundle of type “document” and put a list of resources, but you either forgot the Composition or it’s not first. The validator will complain. Another scenario: using the
$validate
operation on a transaction bundle that’s intended for posting – if any entry is missing therequest
part, the validator will likely treat it as invalid (because a transaction entry requires method, URL, etc.). OrBundle.type = transaction
but an entry has aresponse
(which should only appear in response bundles). These structural rules are often caught by Schema/Schematron and invariants. For example, invariantbundle-1
might check that if bundle type is document, a Composition with Bundle.entry[0].resource exists. -
Solution: Conform to the Bundle-specific rules:
-
For documents: Ensure the first entry is a Composition resource that ties the document together. The Composition should reference other entries as appropriate (section contents, etc.), and Bundle.entry[0].resource = that Composition. If you had a different resource first, reorder the entries. Also ensure Composition.subject and other required fields are present, but specifically this rule is about ordering and presence.
-
For messages: First entry must be MessageHeader. So do similarly, put the MessageHeader as entry[0]. It should reference other entries if needed (MessageHeader.focus, etc.).
-
For transaction or batch: Each Bundle.entry should contain:
request
element with at leastmethod
andurl
. Check that all entries have it.- No
response
elements (those would come back from a server, not to be sent). - Possibly ensure
Bundle.entry.fullUrl
is unique for transaction creates (not strictly a validation error if not, but good practice). If the validator complains about missing request, add them. For example:
bundle.type = "transaction"; bundle.entry.forEach(e => { if(!e.request) { e.request = { method: "POST", url: e.resource.resourceType }; } });
Of course, set the correct HTTP method and URL (resourceType or specific). If your bundle was supposed to be a transaction but you left those out, add them.
-
If a Bundle invariant fails: e.g., “Bundle.entry.resource is mandatory for each entry” (meaning you have an entry without a resource, which shouldn’t happen), remove empty entries or include the resource.
-
Another check: For document bundles, ensure Bundle.entry.content (if any references in Composition sections by reference) are correctly pointing to entries.
By aligning the Bundle structure, you satisfy both base spec invariants and potential profile requirements (like “The bundle must contain X”). The NHS guide snippet we saw emphasizes these points: Document Bundles: first entry Composition; Collection Bundles and others have expectations etc. Make sure to follow those guidelines.
After fixing, a re-validation of the bundle should yield no structural complaints.
-
15. Narrative / XHTML Issues
-
Error Message: “Narrative content is not valid XHTML”, “Narrative contains disallowed script/content”, or warnings about narrative vs content mismatch.
-
Cause: The
Resource.text
element, which holds a human-readable narrative (XHTML), has problems. It might not be well-formed XHTML, or it includes illegal elements (like<script>
or external references which are a security no-no). Another aspect is narrative consistency: sometimes profiles require that the narrative adequately represents certain data (though that’s usually a warning-level check). -
Scenario: If you generate narrative manually or via a templating and something goes wrong, you might produce invalid XHTML. For example, an unclosed
<div>
tag, or using an HTML tag that’s not allowed (FHIR limits narrative to a subset of XHTML for safety). If the validator parses the narrative and it’s not well-formed, it will complain. Another scenario: You include an<img>
in narrative that fetches from an external URL – some validators may flag that as potentially unsafe (information could leak or be dynamically loaded). Or a<script>
tag would be definitely flagged (security issue). HL7 guidance suggests validation of narrative may be needed for security reasons (to avoid active content). So some validators enforce that narrative contains no active content and is well-formed. -
Solution: Ensure narratives are well-formed, minimal XHTML that passes XML validation and security guidelines.
-
If you’re not comfortable crafting XHTML, use libraries to generate narrative. Many FHIR libraries (HAPI, Firely) can generate a simple narrative from data or at least ensure wrapping in
<div xmlns="http://www.w3.org/1999/xhtml">...</div>
. -
Validate the narrative as XML. If the validator pointed out an error like “element X not closed” or “unknown element span” (if span is not allowed), fix those. Only a limited set of tags are allowed (e.g.,
<p>, <div>, <br>, <table>, <ul>, <b>, <i>
etc., basically text formatting). Remove any disallowed ones. -
Remove any scripts or external references. If you need an image, consider inline data URI or just avoid it unless absolutely necessary, as it might be considered external content.
-
Example: If you had:
<text><div>Patient Data <b>John Doe</b></div></text>
The above is missing the XHTML namespace and wrapping properly. It should be:
<text> <div xmlns="http://www.w3.org/1999/xhtml"> Patient Data <b>John Doe</b> </div> </text>
Ensuring the
xmlns
is critical; without it, it’s not considered XHTML by validators, just an unknown element. -
If the error was narrative vs content mismatch (like a profile might want the narrative to contain certain key info), the fix is to update the narrative text to include that info or mark the resource with
text.status="generated"
if the narrative is auto-generated and trusted to represent the content. Some jurisdictions require a human narrative for certain resources to ensure interpretability. -
Many validators treat narrative issues as warnings unless there’s a serious problem. But security-related ones (script tags, etc.) could be errors. So treat them accordingly.
After cleaning up the narrative, these issues should clear. As a tip: if you’re not sure, you can strip narrative for testing (remove the
text
element and validate the rest) to see if other errors exist, then add back narrative. Obviously, final data should have narrative if required, but that helps isolate issues. -
16. Content Too Long
-
Error Message: “Provided content is too long (denial of service protection)”, “Resource content exceeds maximum allowed length”.
-
Cause: This is a less common validation issue, but some validators or servers impose limits on size for performance reasons. The FHIR spec even has an issue type
too-long
for when content is deemed excessively long. It’s not about a specific field’s length (unless specified by profile) but overall size or particular fields like a huge Base64Binary.data
. Validators might throw this if a resource is gigantic or a string element far exceeds typical lengths, as a safeguard. For example, a server might refuse to validate a 50MB JSON resource citing it’s too large. -
Scenario: You try to validate a Binary resource that contains a large PDF encoded, and the validator or server times out or errors with content-too-long. Or a field like
DocumentReference.description
with millions of characters (which is unusual) might trigger it. Typically, this is not a spec violation per se (FHIR doesn’t define global max size), but practical limits. Some national implementations might set specific limits (e.g., an API might say “Observation.valueQuantity cannot be larger than 10^8 in magnitude” or “Narrative limited to 100KB”). If you exceed those, you get errors or rejections. -
Solution: Reduce the size of the content or adjust configuration.
- If this occurred in a test environment, see if the validator has an option to raise the limit or skip size check. HAPI, for instance, might have a configurable parser limit (like maximum string length).
- If it’s in production or by design, consider whether sending such large content is necessary or can be split. E.g., if you have a huge bundle of thousands of resources, maybe break it into chunks. If a Binary is too large, perhaps use FHIR’s $chunk or some out-of-band transfer (or store the content externally and reference it).
- For example, if a diagnostic report’s PDF is 10MB and the server’s limit is 5MB, you could compress the PDF or store it elsewhere and just include a link. Or use the Attachment.url instead of inline data (so the server doesn’t have to handle the big base64).
- There’s no direct “fix code” for this, because it’s situational. But as a developer, be mindful of any documented payload limits. Some servers list these in their documentation (e.g., “Max resource size 2MB”).
- On the flip side, if this is an artificial limit in a dev tool, maybe disable it if it hampers testing.
Once the content size is within acceptable bounds, validation will proceed to more meaningful checks. If you absolutely must exceed a limit, you might need special arrangements or a different approach because validators and servers protect themselves for good reason.
17. Business Rule Violations (Custom Validation)
-
Error Message: “Business rule violated: duplicate record”, “Patient consent missing for restricted data (access denied)”, etc.
-
Cause: These are errors not directly about FHIR format, but about rules external to the FHIR spec that an implementation chooses to enforce during validation or processing. FHIR’s OperationOutcome can carry codes like
business-rule
,security
,forbidden
. While not “spec validation” issues, they often surface when you try to POST data that fails some institution-specific validation logic. For example, an immunization registry might reject a FHIR Immunization resource if a patient opt-out flag is set (consent issue), or if it’s a duplicate submission. The error might come back as an OperationOutcome with codebusiness-rule
or similar. -
Scenario: An API says “each patient must have a consent on file before data can be submitted” – if you send data without prior consent resource, you get a security/consent error. Or a national directory might validate that an organization’s identifier is unique; if you try to submit another with same identifier, you get a duplicate error (business rule). Another scenario: The UK’s Spine might enforce NHS number validation (specific algorithm check) – if failing, it returns an OperationOutcome error (identity validation error). These are not standard FHIR validation errors, but real-world ones encountered when integrating.
-
Solution: Satisfy the external rule or policy. This might involve steps outside of just fixing the FHIR JSON:
- If it’s a duplicate record issue, you may need to do a GET to see if the data is already there, and either update instead of create, or change the identifier to avoid conflict. This is more about workflow (ensuring idempotency or unique constraints).
- If it’s a consent or authorization problem, ensure the necessary preliminary steps are done (e.g., create a Consent resource granting permission, or include an authorization token with appropriate scope).
- If it’s a business logic like “no future-dated observations allowed”, adjust your data (don’t send dates in the future unless legitimate).
- For algorithmic checks (like checksum in an ID), make sure the data meets those. E.g., NHS number has a check-digit formula – the validator might apply it and if the number is invalid, you get an error. The fix is to correct the ID or leave it blank if unknown (depending on allowed).
- Since these vary widely, the key is to carefully read the error message and any documentation of the API. Often, these errors will come with an OperationOutcome.issue.details or diagnostics explaining what rule was violated.
- For instance, if an error says “Duplicate record – a Patient with this identifier already exists”, you might choose to update that patient instead of creating a new one, or ensure you send a unique identifier for new ones.
- Or if error: “No patient consent to share” (security error), maybe you need to obtain and submit a Consent resource (and possibly link it to the data or ensure server knows of it).
In a robust pipeline, you might incorporate checks for known business rules before sending to server. For example, perform a search for existing record to avoid duplicates, or validate patient ID formats locally. This turns these runtime errors into preventable conditions.
After addressing the specific business condition, your FHIR submission should succeed (or at least move past that error). These are often beyond what a generic validator (like the HL7 one) would catch, since they require context or database knowledge. They highlight the need for integration testing beyond just schema validation.
18. Testing and Diagnostic Errors
(This is not a specific FHIR error type, but a quick mention:) Sometimes when using validation tools, you might get errors due to environment issues – e.g., “Unable to fetch terminology” (if the validator tried to look up codes online and failed), or “Unknown profile …” (if a profile isn’t loaded). These are not problems with the resource itself but with the validation setup. If you see errors like that:
- Ensure you have internet connectivity if the validator needs to fetch profiles or ValueSets, or better, pre-load the necessary packages (HL7 validator can load an IG package, so it doesn’t need to go online).
- If a profile is unknown, provide the StructureDefinition to the validator. For example, use the
-ig
parameter in HL7 CLI or appropriate API in HAPI (ValidationSupport
) to load your profile. - If terminology server is unreachable, you might either configure a local terminology cache or disable terminology checks temporarily (some validators have a flag to not check codes if service not available, treating them as warnings).
Addressing these will give you a cleaner run focusing on actual content errors.
Now that we’ve covered a wide range of errors and their solutions, let’s move on to a more procedural view: how to systematically diagnose validation errors when you encounter them.
Decision Trees for Diagnosing Validation Errors
Dealing with a barrage of validation errors can be daunting. It’s helpful to approach debugging in a stepwise fashion. Below is a textual decision tree (a logical workflow) you can follow when you get validation errors, to pinpoint the cause and apply the right fix. This process helps ensure you don’t miss something obvious while chasing a complex issue.
Step 1: Check for Syntax/Parse Errors First
› Can the resource be parsed at all? If you see errors about “invalid JSON”, missing resourceType
, or unrecognized format, address those first. For example, if the JSON is malformed (maybe a trailing comma or unquoted key), fix the JSON syntax. If resourceType
is missing (error “missing required element: ‘resourceType’”), add it. Essentially, make sure the input is well-formed FHIR JSON/XML at the basic level.
(Reasoning: You can’t trust other validation messages until the resource parses correctly – many follow-on errors disappear once the structure is readable.)
Step 2: Identify Structural vs. Content Errors After syntax is OK, look at the error codes or messages:
-
Errors like “unknown element …”, “element not allowed”, “Unexpected child” indicate structural issues. Use this branch:
- Unknown elements: Possibly typos or wrong FHIR version. Cross-check element names with the FHIR spec for that resource. Remove or correct any illegitimate elements (as discussed in Error #3).
- Not allowed (max=0): You included something the profile forbids. Remove that element.
- Too many repeats: Cardinality issue – trim the repeats to acceptable count (Error #4).
- Choice conflicts: Only one of a
[x]
choice element should be present. Remove extras. - Containment rules: If error says contained resource has disallowed elements (meta, etc.), adjust the contained resource (drop meta.versionId, etc., per invariant dom-2, dom-4).
- Fix these, then re-run validation to see if structural errors are resolved. It’s often useful to get to a point where structural errors are gone, because then the remaining issues will be about content/values.
-
Errors like “required element missing”, “value invalid”, “constraint failed” suggest content or data quality issues. Use next steps for those.
Step 3: Resolve Cardinality and Missing Data Address any messages about required elements or cardinality:
- For missing required fields (issue type
required
): Immediately add those fields (with placeholder values if needed, better than nothing, but ideally real values). E.g., Observation.status must be added if missing. - For excess elements: Remove or split as needed (e.g., if you jammed multiple observations into one, perhaps you need multiple Observation resources).
- For “must have value or children” errors (invariant ele-1): ensure no element is empty. Either remove the empty element or populate it properly.
- Re-run validation; these kinds of errors should disappear once cardinalities are satisfied. This will also often clear many cascade errors (one missing field can cause multiple invariants to complain).
Step 4: Handle Data Type and Format Errors Next, fix any type mismatches or format issues:
- Scan errors for keywords like *“expected” vs “found”, “invalid format/pattern”. If e.g., an integer is provided as string, convert it to number. If a date format is wrong (error referencing a pattern or regex), reformat the date as ISO string.
- Check all values against FHIR’s spec: Are your booleans true/false (not “true”/“false”)? Are your IDs and codes free of spaces and special chars if not allowed? Adjust accordingly.
- If you find one instance, search your resource for others. For example, if one date was in wrong format, ensure all dates follow the pattern.
- This step often involves using tools: you could feed suspect values into a small regex test or a date parser to be sure. But generally, conform to spec examples.
- After making these changes, run validation again. By now, the errors should hopefully be down to more semantic issues (terminology, invariants, profiles).
Step 5: Investigate Terminology and Code Errors Now focus on errors about codes or valuesets:
- Unknown Code / not in valueset: Determine which element it is. If it’s a required binding, find the correct code from that valueset. This might involve looking up the valueset definition or documentation. Implement the fix (mapping your code to an allowed one, or adding system if missing).
- If the code system is unrecognized by the validator (maybe you forgot to load a local CodeSystem), either load it or decide to accept a warning. But for required bindings, you should either switch to a known standard or include a custom CodeSystem resource in your validation context.
- ValueSet expansion issues: If you see errors about “unknown code system” or inability to validate some code, ensure the validator has access to the terminology. If not, it might treat those as warnings or skip, but better to supply an IG package or terminology files.
- Fixing terminology often requires coordination (e.g., asking the source system to start sending SNOMED instead of local codes). As a short-term, you might use a translation map in your ingestion pipeline to convert local codes to standard ones before validation.
- Once addressed, run validation. Ideally, no
code-invalid
errors remain.
Step 6: Satisfy Invariants and Profiles What remains are usually the more complex rules:
- Read any invariant failure messages. For each, ensure your resource meets the condition. This might mean adding a complementary field, or adjusting values. This step can be tricky – if needed, refer to spec for the invariant or ask on community forums if an invariant’s meaning isn’t clear.
- Check profile-specific messages: They often explicitly mention the profile name or id. For example, “… in profile X”. For each, adjust the resource to comply (as per Error #10 and #11 discussion on profiles and slices).
- If an invariant seems to be from a profile (custom constraint), handle it in context of that profile’s rules.
Step 7: Re-run Validation & Iterate After the above, run the validator again on the fixed resource. Ideally, you see a clean result or only minor warnings. If there are new errors or something you missed:
- Sometimes fixing one thing can reveal another that was masked or create a new minor issue (e.g., you added an extension but forgot the url, now you get an extension error – easy to fix).
- Address any stragglers. At this stage it’s usually small details.
Step 8: Optional – Use Differential Diagnosis If an error is puzzling, a good technique is to simplify the resource or isolate the problem:
- Remove sections of the resource and validate again to see if error goes away – helps pinpoint the problematic element. For example, if you suspect an extension causing trouble, try removing it and see if the error disappears, confirming it was the extension.
- Validate using multiple validators (if possible). Sometimes one gives a clearer message than another. The HL7 Java validator, HAPI, and Firely .NET might word things differently. If you have access, cross-checking can help.
- Break the problem into smaller pieces: e.g., validate a contained resource standalone if possible to see if error is in the contained part or in how it’s referenced.
By following this decision tree, you systematically narrow down issues and fix them in a logical order. Figure 1 (textual summary): Ensure parseable → fix structure → fill required fields → correct types/formats → align codes → satisfy invariants/profiles → verify references. This sequence addresses errors from most fundamental to more advanced, which is efficient.
Remember that some errors might hide behind others. For instance, if the JSON is unparsable, you won’t yet see the invariant errors that would come later. So always start at the base.
Lastly, make use of the OperationOutcome details:
- The
severity
tells you if it’s fatal (error
vswarning
). - The
location
(FHIRPath expression) often points to the exact element in the resource causing trouble. Use that as a guide to jump to that part of the JSON. - The
code
(likestructure
,required
,value
,invariant
) hints at the nature of the issue.
With practice, you’ll get faster at reading these clues and applying the right fix. Next, we’ll discuss how performance considerations might alter how and when you validate (for instance, maybe you don’t run full validation on every single transaction in real-time).
Performance Benchmarking and Optimization of FHIR Validators
Validating FHIR thoroughly can be computationally expensive, especially when terminology and profiles are involved. In large-scale environments (e.g., national health exchanges or EHRs with millions of records), naive validation of every resource can introduce latency. It’s important to understand the performance characteristics of major validators and strategies to optimize validation throughput.
Major FHIR Validation Engines:
-
HL7 Reference Validator (Java) – This is the gold-standard in terms of completeness. It checks everything (if configured to do so) – schemas, schematrons, FHIRPath invariants, terminology via HL7 Terminology Service, etc. However, it is not the fastest. It loads StructureDefinitions, does a lot of reflection and FHIRPath evaluation. In benchmarks, it’s not unusual for a single resource validation to take tens of milliseconds up to hundreds if heavy (especially with terminology lookups). Validating large bundles or many resources serially can quickly add up. The upside is accuracy and strictness.
-
HAPI FHIR Validator (Java) – HAPI actually wraps the HL7 validator internally (as of HAPI 5+ using the official implementation for instance validation). Performance is similar to HL7’s, with some overhead. HAPI has some caching mechanisms (like reusing
ValidationSupport
caches for StructureDefinitions, ValueSet expansions) which you should take advantage of. For example, you can preload the validator with all needed profiles so it doesn’t fetch or parse them repeatedly. HAPI’s team acknowledges that “validation can impose unacceptable time delays in production” if used blindly, echoing HL7’s caution. -
Firely .NET SDK Validator – This is the primary .NET engine. It’s quite optimized in C# and integrates with .NET’s handling of XML/JSON. Anecdotally, it is fairly efficient, but if using terminology, it will need either a connected TerminologyService or provided expansions. Firely’s docs sometimes list big-O for certain operations; overall it’s comparable to HL7’s validator. One advantage is if you’re on a Microsoft stack (like Azure FHIR), it’s built in and possibly tuned for that environment.
-
Node.js / JavaScript Validators – There are a couple of community validators like the d4l-data4life JS validator which rely on JSON Schema. These are extremely fast for what they cover (structure, cardinality, primitive formats) because JSON Schema validation via something like AJV or another engine is typically microseconds to a few milliseconds per resource. However, as noted, they do not cover invariants, slicing, or terminology fully. If performance is critical and you only need basic validation, this is an approach (fast but not comprehensive).
-
Server-side integrated – e.g., Smile CDR (HAPI) or IBM FHIR or Blaze or Aidbox. Each may have their own optimizations. For example, Smile (which uses HAPI) can run validators in parallel in a cluster; IBM’s FHIR server might precompile some validation logic; Aidbox uses its FHIR Schema (inspired by JSON Schema) for faster validation.
-
Cloud FHIR Services (Azure, Google Cloud, etc.): These often use the above engines under the hood but might run in scalable infrastructure. For instance, Google Cloud’s Healthcare API likely has custom code (performance tuned in C++ or Go, possibly). Microsoft Azure’s service uses .NET under the hood (it’s basically the Firely validator integrated, as their docs show typical OperationOutcome results like Firely’s format). In terms of benchmarking:
- A Google Cloud whitepaper (2020) showed ingestion of millions of resources; they likely did not validate each one fully to achieve that scale, or they parallelized heavily. They highlight that performance varied widely across offerings.
- Smile CDR’s benchmarking pointed out that for bulk imports, they often skip validation to improve speed – bulk import typically trusts the data as already validated, hence the note "(unlike a bulk import which is typically un-validated)”. In their test of loading 2+ TB of FHIR data, they likely disabled validation or did minimal checks to hit those numbers.
-
Ballerina FHIR validator (mentioned in a Medium post) – a niche example, but it indicates folks implementing validators in other languages focusing on performance in their ecosystems.
Performance Considerations:
-
Terminology is the Slowest Part: Checking codes against large ValueSets (like all of SNOMED or ICD) can be very slow if not cached. The HL7 validator will attempt to download expansions or query a terminology server if configured. This can turn a 50ms validation into a multi-second one if it’s the first time a ValueSet is seen. Optimization: Pre-expansion. If you know you’ll validate against certain ValueSets, obtain the expansion (a list of codes) offline and feed it to the validator’s ValidationSupport. Or use a terminology service that’s local and fast. Or configure the validator to skip code validation in tight loops and do it asynchronously later.
-
Reuse Validator Instances: Spinning up a fresh validator for each resource, loading all necessary context each time, is costly. If using HAPI or HL7’s, create a
Validator
object once, configure it with FHIRContext (in HAPI) or whatever structures, and reuse it for a batch of validations. This avoids repeated parsing of StructureDefinitions. For example, HAPI FHIR allows setting a cachedPrePopulatedValidationSupport
with all the profiles and ValueSets loaded once. -
Parallel Processing: If you have a multi-core system, you can validate multiple resources in parallel. HL7’s validator CLI even has a
-threads
option to validate batches with concurrency. If you integrate in code, you can spin threads or use async in Node to process multiple items. But be cautious – if all threads hit the terminology service at once, that can be a bottleneck. Throttle or ensure caching is in place. -
Scope of Validation: Decide what needs full validation. Perhaps in production you only validate profiles/invariants on certain critical resources or at certain points (like at ingestion, but not at every read). Or you might run a lighter validation (schema only) in real-time and queue a deeper validation for offline analysis. HL7 specifically notes that doing full validation including terminology at runtime can impose unacceptable delays, so many adopt a balanced approach.
-
Streaming and Large Bundles: If validating a large bundle, some validators handle entry-by-entry, but it could still be memory heavy. Consider splitting very large bundles for validation purposes.
-
Benchmark figures: While we don’t have exact numbers in this text due to the dynamic nature, here’s an approximate idea:
- Basic JSON Schema validation of a resource: ~0.1-2 ms (very fast).
- Full HL7 validation of a simple resource (Patient with no fancy codes): ~5-20 ms.
- Full validation of a complex Observation with terminology (like multiple LOINC, SNOMED codes not cached): could be 100-300 ms if it has to fetch terms.
- Validating a 50-resource bundle with profiles: could be a few seconds if done sequentially.
- The Medium article by Vivian Neilley noted dismal performance numbers for some servers – presumably meaning some servers couldn’t handle heavy loads with validation on. After a call to action, Smile CDR published numbers (though they downplayed raw numbers). The key is, performance varied widely between implementations, meaning you should test your specific stack.
Strategies to Optimize in Your Pipeline:
- Turn Off Unneeded Checks: If, for instance, you trust that codes are valid or will be validated elsewhere, you might disable terminology checking in the validator (
validator.setValidateTerminology(false)
in HAPI, for example). This can speed things up drastically. You could then do terminology validation in a nightly job or via a Terminology Service as needed. - Validate Once, Use Many: If data doesn’t change, you don’t need to validate it every time it’s read. Validate on create/update (perhaps asynchronously) and mark it as validated. E.g., store an OperationOutcome or a flag indicating it passed. Then skip validation on retrieval.
- Profile-Driven Validation on Demand: Instead of always validating against all profiles, do it when required. For example, if a client specifically wants to ensure a resource conforms to an IG, call $validate with that profile; otherwise, maybe just base validate. This reduces overhead for general operations.
- Leverage Database Constraints for Some Validation: Some validations (like uniqueness of certain identifiers, or basic format checks) can be done at the database level or via simpler code. If those are enforced upstream, you can skip those aspects in the FHIR validator. E.g., if your DB schema ensures no two active allergies have the same code for a patient, you won’t get a duplicate business rule, so you might not need a custom validator for that.
- Profile Packaging: Use HL7’s packaged Implementation Guides (NPM packages) with the validator – it’s faster to load a prepackaged set than hundreds of individual definitions.
- Monitor and Tune: In a production system, monitor how long validation is taking. If you find spikes (like a particular profile or resource slows it down), investigate if a particular invariant or valueset is the culprit. Perhaps you can cache the value set expansion or even pre-evaluate that invariant in a simpler way.
- Staged Validation: As touched above, do quick checks first (schema) then deeper ones. This way, you catch gross errors fast (which might be 90% of issues) with almost no overhead, and only do the heavy lifting if those pass. This is akin to a multi-tier pipeline: JSON schema validation (cheap) → if passes, FHIRPath invariants and terminology (expensive).
Example: If building a Node.js microservice to ingest FHIR:
- Use Ajv + FHIR JSON Schema to validate request body structure & required fields (in ~1ms). If fails, return error immediately – you avoided invoking a slower validator.
- If schema passes, then call a more comprehensive validator (maybe a local HAPI instance via REST or HL7 CLI via a spawn) in a background task. If it finds issues, perhaps log them or send a notification, but you might decide not to block the ingestion if they’re non-critical. Or if critical, you can still roll back the transaction.
- You could also use FHIR $validate on a server as your validation step (e.g., send to a dedicated validation endpoint). However, note that doing that for each resource could double your traffic and add network latency. So it’s fine for occasional use or batch validation, but not for ultra-high throughput.
Benchmark anecdote: The Smile CDR team indicated that with 6 servers and parallel processing, they could ingest 2.18 TB of FHIR data (2.4 billion resources) in a reasonable time, but likely they weren’t validating each resource fully. They noted using transactions (bundles) improves throughput by reducing HTTP overhead. That implies in practice, high-volume systems lean on transaction integrity rather than per-resource validation overhead.
In summary, know your needs: If you’re in a closed system with trusted data, you might not validate everything always. If you’re in an open interface (like an EHR accepting from unknown sources), more validation is needed but you then architect for scale (maybe separate validation service, asynchronous processing, etc.).
Next, we will outline how to build a robust validation pipeline combining many of these ideas – ensuring data quality without crippling performance or user experience.
Building a Robust Validation Pipeline
Implementing validation in production is not just about calling a validator – it’s about workflow. A robust pipeline will catch errors early, handle them gracefully, and integrate with your business processes. Let’s break down a best-practice validation pipeline in stages:
Pre-Validation Checks (Input Scrubbing)
Before running a full FHIR validation, it’s beneficial to do some pre-flight checks:
- Basic Format Validation: Ensure the incoming payload is valid JSON (or XML). This might seem obvious, but if you’re getting data from external sources, always guard against malformed input. Many frameworks do this automatically by parsing JSON into objects, but have try/catch around it and return a 400 Bad Request if parse fails.
- Schema or Structural Validation: Use a lightweight method to verify that the resource at least matches general FHIR structure (fields of correct type, no unknown fields if you have a JSON schema). This can be done via JSON Schema validation. For instance, the Kodjin suite’s profiler tool uses a built-in JSON Schema updated from HL7’s GitHub to check structure. This catches obvious mistakes quickly.
- Authentication/Authorization: This is outside FHIR content but in pipeline: ensure the request is authenticated and allowed to send this resource type. No point validating content if the user can’t even post an Observation due to auth.
- Routing & Preliminary Business Rules: Possibly check things like patient identifiers format (if you have a custom quick check) or whether the patient exists if required for context. Some systems do a preliminary “does patient exist?” query. However, be careful not to do heavy DB lookups here if you plan to do them in validation later to avoid duplication (maybe the validator or subsequent step will do reference resolution).
- Logging the Receipt: Good practice is to log that data was received (and maybe the raw payload) before you modify it. This helps debugging later (especially if validation fails and you need to see original input).
By the time you pass pre-validation, you have a well-formed FHIR object to feed into deeper validation logic.
Layered Validation Workflow
Consider splitting validation into multiple layers:
-
Structural & Syntactic Layer: (We largely did this in pre-checks with JSON Schema). If not done earlier, the first thing your pipeline’s validation stage should do is structural validation (which catches unknown elements, cardinality, type issues). Many choose to rely on the FHIR Instance Validator for this as well. HAPI’s
InstanceValidator
will cover structure, cardinality, etc., with good messages. If you did JSON Schema pre-check, you might skip straight to next layer for efficiency. -
Terminology & Invariants Layer: Next, incorporate rules and terminology. If performance is a concern and this is a live transaction, you might here decide to skip some of these or offload them. But let’s assume we want robust validation: configure the validator to check valuesets and invariants. If using HAPI, you might attach a
ValidationSupport
that connects to a terminology service (or has expanded valuesets). Or if that’s not available in process, you might mark code validation to be done later (e.g., log a warning now, actual verification offline). -
Profile/Custom Layer: Ensure that the validator knows which profiles to apply. If the incoming data has
meta.profile
, the validator will automatically try to validate those (given it has the StructureDefinitions). If not, and you have context (like “all Observations coming through this endpoint must conform to US Core”), you explicitly call validate with that profile. This can be done via$validate?profile=...
if using a server, or via validator API (validator.validate(resource, profile)
). By applying profiles here, you enforce site or project-specific rules. -
Business Rule Layer: After pure FHIR validation, do any custom checks that FHIR doesn’t cover. This might be in code:
- E.g., if a lab result Observation comes in for a patient that’s marked as deceased before the observation date, maybe flag that as suspicious (business rule, not in core).
- Or enforce that a certain identifier must be unique (which requires checking database).
- Or check that if Encounter.type = “ER” then an Encounter.location must be an ER wing (just hypothetical). These are rules from requirements or policies. These checks can be implemented with FHIRPath or just regular code logic after validation passes basic checks. Some teams embed these as FHIRPath invariants in a custom profile so that the normal validator catches them. That’s elegant if you have the means – essentially encode business rules as custom invariants in StructureDefinitions. If not, just do them in code after. For example, to check duplicate:
let existing = findResourceByUniqueKey(resource); if(existing) { issues.push({ severity: 'error', code: 'business-rule', diagnostics: 'Duplicate record exists with identifier X' }); }
Aggregate such issues as an OperationOutcome or similar object.
At each layer, if errors are found, you have to decide the pipeline’s behavior:
-
Reject immediately vs. accumulate errors: Often, structural errors cause immediate rejection (HTTP 400), since the payload is bad. Content errors (like invalid code) might also result in 400 (because the request is semantically invalid). Some systems choose to collect all errors and return one comprehensive OperationOutcome listing them (the approach recommended by HL7 for $validate). This is user-friendly as they can fix all in one go. Others might stop at the first fatal error – simpler but sometimes frustrating to clients who then get one error at a time. Best practice: try to return all validation issues in one response if feasible.
-
Logging and Monitoring: Regardless of what you return to user, log the errors internally. That way you can monitor trends (“50 records failed validation today due to code issues – maybe a mapping table is outdated”).
-
Error Categorization: Perhaps differentiate between errors and warnings:
- Errors (fatal issues) cause a failure response.
- Warnings might be returned in OperationOutcome with severity warning or information, but the transaction still succeeds. For example, if an extension is unrecognized but you allow it, you might just warn “unrecognized extension ignored” rather than error.
- Decide on these policies. Many validators output both; you then filter what to treat as blocking. E.g., HL7’s validator might label a missing
text.div
(narrative) as a warning only – you could let it through but note it.
Integration into Workflows
After validation step, define how it integrates:
-
Synchronous API call ($validate): If you provide a $validate operation (like many FHIR servers do), the pipeline essentially is: take resource, run validation pipeline, return OperationOutcome. This is straightforward.
-
Data Ingestion (create/update): Here you must decide – do you validate within the same request handling (and reject bad ones with OperationOutcome), or accept everything and fix later? Most servers do some validation and reject obviously bad data (according to the spec’s MUSTs). For optional constraints or profiles, some might accept and log warnings if in “liberal” mode.
- If you have an intake queue (say data flows in via messages), you could accept, put in queue, then a validator process picks up and validates. If it finds errors, it may quarantine that data or send alerts. This is common in scenarios where immediate response to sender is not needed (e.g., internal data loads).
- For an interactive API, usually you validate in real-time and give feedback instantly (fail the request).
-
Post-Validation Steps: After successful validation, proceed to whatever persistence or business logic is next (e.g., save to database, trigger downstream processes). If validation failed, return error (and possibly do not proceed to saving anything).
-
Storing Validation Results: A robust pipeline might actually store the OperationOutcome or validation report somewhere (especially if you do asynchronous). For example, you could attach an OperationOutcome reference to the resource (in a data quality log) or send it back to data senders in a report. This can help track over time if certain systems are constantly sending bad data – you have a record to show them.
Example Workflow Putting it Together:
Imagine we run a health information exchange and we get a Bundle of lab Observations from a lab system:
-
Receive request (Bundle of Observations).
-
Pre-validate: Check JSON syntax; confirm
bundle.type
is “transaction” or “collection” as expected; ensure the bundle has at least one entry. If something is off, respond with error immediately. -
Schema check each entry: Use JSON schema to ensure each Observation has
resourceType
, no unexpected properties. If any fails, log it and mark that entry as invalid. -
Iterate entries:
- For each Observation, run the HL7 validator (or HAPI) against base spec + our custom IG (say we have a profile requiring LOINC codes, etc.).
- Collect OperationOutcome issues for each. Separate them by severity.
-
If any entry has fatal errors:
- Depending on policy, either reject the whole bundle (transaction style) or just those entries. If transaction, we might fail everything with an OperationOutcome describing which entries had issues (perhaps using
OperationOutcome.issue.location
to point toBundle.entry[n]
). - Or if our system allows partial acceptance, we might save the good Observations and return errors for the bad ones (maybe in a Batch response, with OperationOutcome for failed entries).
- Depending on policy, either reject the whole bundle (transaction style) or just those entries. If transaction, we might fail everything with an OperationOutcome describing which entries had issues (perhaps using
-
If warnings only:
- We might accept all, but return an OperationOutcome in response with severity=warning issues so the sender is aware (and possibly also send them via email or dashboard).
-
Save valid data to DB.
-
Post-save: If needed, trigger any business logic (e.g., if a critical lab comes in, maybe alert a provider – which should only happen if data is valid).
-
Analytics: Log validation stats (# of errors, types of errors) to a monitoring system. Over time, this can highlight problem areas in data feeds.
Workflow for CI/CD and Testing:
Another angle: incorporate validation into your development pipeline:
- When developers create or modify mapping code or profiles, run a suite of example resources through validation as part of CI tests. This catches regressions early.
- Have unit tests for each common error scenario, if you’ve written custom validators. For instance, if you added a rule “flag duplicate”, write a test that feeds duplicate data and expects a certain OperationOutcome.
- In a staging environment, perhaps replay real data with stricter validation turned on to see what would fail, without impacting production.
Utilizing Validation in Production Safely:
HL7’s caution is worth repeating: be careful not to throw away data just because it’s slightly invalid if that data is clinically important. A robust pipeline can have fallbacks:
- If a resource fails validation in a non-critical way (maybe just a warning or minor issue), you might still store it but mark it as “needs attention”. For example, store it but also store an OperationOutcome with it, and perhaps display in an admin dashboard for data stewards to review/correct later.
- If it fails in a critical way (missing key info like patient identity), maybe route it to a manual reconciliation queue rather than outright discarding. Someone could then fill in missing pieces and reinsert it.
- Use Postel’s law as guidance: be liberal in what you accept – maybe accept slightly off data (with warnings) but ensure when you output data you clean it up to be strictly valid. This might mean your system internally stores something incomplete, but when another system asks for it, you augment or fix it before sending (or at least tag it with dataAbsentReason, etc., on the fly). This is advanced, but some implementations do that to not lose data.
In summary, a robust pipeline is one that:
- catches issues early (before they cause bigger problems downstream),
- gives useful feedback to senders or developers,
- enforces crucial rules without unnecessarily preventing data flow,
- and continuously monitors and improves based on error patterns.
Next, we’ll cover testing strategies to ensure your validation logic itself is solid (so your pipeline doesn’t become a bottleneck or source of bugs).
Testing Strategies for FHIR Validation Logic
Just as you test application features, you should test your validation rules and setup. This ensures that your validators are catching what they should and not throwing false positives/negatives. Here are strategies:
Unit Testing Individual Rules
For each custom validation rule or each known spec rule, create unit tests:
- Example: If you wrote a custom check that Patient must have a national ID, create a test Patient with no ID and run validation, expecting an OperationOutcome with a specific issue. Also test a Patient with the ID present and ensure no such error.
- Use known official examples for positive cases. The FHIR spec provides lots of example resources (which are usually valid). You can run them through your validator to ensure you don’t get unexpected errors. If an official example fails your validation, that signals you might have configured something too strict or mis-loaded a profile.
- Conversely, prepare some deliberately invalid examples for each common error type to see if your validation catches them. E.g., an Observation missing status – expect a “required element missing” issue.
- If you have multiple validators (like schema + instance validator), test that their combination catches everything. Perhaps a JSON Schema might not catch an invariant – your integration test with the full validator should catch it.
- If using JavaScript, frameworks like Mocha or Jest can automate these tests. For Java, JUnit; for .NET, NUnit or MSTest, etc.
Integration Testing Whole Pipeline
Set up scenarios where data flows through as it would in real usage:
- For instance, simulate an API call with a bundle of resources that have various issues. Verify that your pipeline responds with the correct OperationOutcome and that no partial data got saved if it shouldn’t have.
- Test performance on reasonably sized data sets in a staging environment. Does validating 1000 resources take too long? This might reveal scaling issues.
- Negative Tests: Send intentionally bad data and ensure it’s rejected with appropriate messages. E.g., send a Patient with an unknown element, see that the response OperationOutcome includes an issue about that unknown element (and perhaps a 400 status).
- Positive Tests: Send valid data and verify it passes through and is stored. Also ensure that no warnings are mistakenly raised for valid data.
- If applicable, test multi-language or cross-platform pieces: e.g., if you sometimes use a server’s $validate, compare its output to your local validator for the same input to ensure consistency.
Regression Testing for Validation Changes
When you update FHIR versions or profiles:
- Re-run all your tests to catch any differences. A new FHIR release might add new invariants or change codes, which could cause your previously valid data to be flagged. For example, FHIR R4 -> R5 might introduce new required fields or deprecate some codes. Good tests will catch that, so you can adjust.
- Similarly, if you update the version of your validation library (say HAPI 5 to 6, or new Firely version), do a thorough run of known inputs. There have been instances where a new validator version fixed a bug which means it now catches an error it used to miss – if your tests were not expecting that, they’ll fail and alert you that something that used to pass now fails (which could be a genuine data issue that was slipping through).
Automated Validation in CI/CD
Integrate validation into your continuous integration:
- For example, if you maintain an Implementation Guide or profiles, you can use the HL7 IG Publisher’s validation report. Or have a step in CI that runs the HL7 validator CLI on a set of sample resources (or even all examples in your IG) to ensure they still all validate against the profiles.
- If you maintain example messages for partners, validate them as part of build to ensure they remain correct as spec evolves.
- If possible, incorporate tests using different validators (one might catch something another doesn’t). For instance, you could run your sample resources through both the Java and .NET validators in CI to catch any discrepancies or issues – this is more effort but can increase confidence especially if you know your data will go to systems using different stacks.
Test Data Generation
Use or build diverse test cases:
- Include edge cases: extremely long strings (to test if your pipeline truncates or flags them), missing optional data (should be fine), maximum number of repeats (if spec says 5, test with 5 to ensure allowed, test with 6 to ensure error).
- Leverage tools like Synthea (which generates synthetic patient data) for volume testing. Synthea output is generally valid FHIR – you can run it to produce, say, 1000 Patient resources and then bulk validate them to ensure your pipeline handles volume and sees they’re valid (since they should be).
- Also create some random invalid data (perhaps by mutating some valid ones) to see if validator catches.
- If you use FHIRPath in custom invariants, create tests specifically for those expressions with various inputs (because an error in your FHIRPath could either always pass or always fail incorrectly). Firely’s .NET or HAPI have FHIRPath engines you can use in tests to evaluate your invariant conditions outside the full validator to double-check logic.
User Acceptance Testing & Beta Testing
If possible, run a beta with real users or partner systems:
- Have external systems send data to a test instance of your pipeline. This often reveals unforeseen issues (maybe your validation was too strict for a real-world oddball scenario – e.g., patient with only a single name letter, which might break a regex, etc.).
- Collect feedback: Are the error messages understandable to them? If not, you might improve mapping of OperationOutcome to user-friendly messages or documentation (sometimes mapping technical messages to friendlier text in API documentation is helpful).
Continuous Monitoring
Even after deployment, treat validation errors as something to monitor:
- Set up alerts if validation error rates spike. If overnight a new type of error starts appearing frequently, that could indicate an upstream system change. For example, a lab system might upgrade and start sending a new code system that your validator flags – you’d want to catch that early.
- Periodically review logs to see if there are many warnings that could be addressed (maybe everyone’s sending an extension you haven’t seen; either allow it or get them to stop).
- Also ensure that the absence of errors is noted – if you never see validation errors, is it because data is perfect or because something in the pipeline got turned off inadvertently? (Yes, that can happen – e.g., someone disables validation temporarily and forgets to re-enable; a monitor can catch that by noticing no validation output at all).
By thoroughly testing and then monitoring, you ensure that your validation pipeline itself is reliable and doesn’t become a bottleneck or a source of false issues. This gives confidence when moving on to advanced usage of validation and expansion to new use cases.
Next, we’ll delve into some advanced topics such as creating custom validators or handling multi-profile validation, as well as cross-resource consistency checks.
Advanced Topics in FHIR Validation
As you become more proficient, you may encounter scenarios that go beyond validating single resources against a single profile. Here we cover a few advanced areas: developing custom validation logic, handling multiple profiles simultaneously, and cross-resource validation scenarios.
Custom Validator Development
What if the existing validators don’t meet your needs? Maybe you want to support a new constraint language, or integrate validation tightly with custom data sources. Developing a custom validator can range from writing simple plugins to full from-scratch implementations:
- Extending an Existing Validator: Both HAPI and Firely offer extension points. For example, in HAPI FHIR you can implement a
IValidationSupport
to provide custom logic for things like fetching unknown StructureDefinitions or checking codes in a custom way. You can also create customProfileValidationSupport
modules for new data types or special rules. Firely .NET allows adding custom resolvers and also you can intercept the outcome to add issues. - Custom Rules via FHIRPath: You can attach additional invariants dynamically if needed. E.g., in a particular context, you might want to enforce “All Patient names must have a family and given”. You could create a FHIRPath expression for that and run it on Patient resources as an extra step in your pipeline, even if not in the profile. Some use the FHIR Validator’s ability to take an FHIRPath as a parameter (in the CLI, you can’t directly, but in code, you could evaluate it and add an OperationOutcome if it returns false).
- From Scratch Implementation: This is a big task – essentially re-implementing the spec rules. There are projects in various languages (like some in Rust or Go) where community devs implemented parts of FHIR validation. If you do this, focus on a subset, perhaps for performance or platform reasons. For instance, you might implement just the JSON schema part in a systems language for speed, or implement a tailor-made validator that only checks your profiles (hard-coding some rules for ultimate speed).
- Consider JSG/Shex: The Zulip chat snippet mentions JSG (JSON Schema Grammar) or ShEx as alternatives that could handle some aspects like slicing more elegantly than pure JSON Schema. If you’re adventurous, you could develop validation using those. For example, some have written ShEx schemas for FHIR profiles which can then validate data. Not mainstream, but interesting for custom solutions.
- Performance-oriented custom validator: If you need to validate millions of records quickly but only certain constraints, you might code custom loops in, say, C++ or Go that check exactly those fields. For example, a custom validator that only ensures structure and required fields might be written to be extremely fast in a low-level language, then integrated. This sacrifices completeness for speed.
- Keep up with spec changes: If you roll your own, be prepared to update it for new FHIR versions and test extensively. The official validator goes through lots of testing with the community.
One pragmatic approach is a hybrid: use the official validators for heavy lifting but intercept and add any custom rules:
- e.g., Run HL7 validator, then run your custom checks as a second pass. Merge the results.
If you need to create custom error messages or localize messages, some validators allow overriding messages or providing a different locale. For instance, HAPI supports a localization file to change wording of messages. That can be seen as part of custom development – making errors user-friendly or in local language.
Multi-Profile Validation
A resource can claim conformance to multiple profiles (i.e., multiple URIs in meta.profile
). How to handle this?
- By default, a robust validator will try to validate against all listed profiles. According to spec, if a resource has multiple profile tags, it should meet all of them (logical AND). For example, you might tag an Observation with both a national base profile and a disease-specific profile, meaning it adheres to both.
- If you are manually invoking validation, you may have to explicitly validate against each profile. The HL7 validator CLI takes a
-profile
param for one profile; not sure if it can take multiple at once. In code, you might call the validator twice (once per profile) and aggregate issues. - If two profiles conflict (one says element must be present, another says element must be absent), then no resource could satisfy both. Typically, you wouldn’t have conflicting profiles in
meta.profile
in production. If you do, you have to decide which one to prioritize or remove one tag. - Some servers might only enforce one primary profile (like those listed in CapabilityStatement for that resource) even if more are present.
- Testing multi-profile: If you declare two, test that your resource indeed passes both independently. E.g., validate with profile A, then with profile B.
- A nuance: meta.profile is often used to indicate to receivers “this resource conforms to X profile(s)”. Validators do use it, but make sure you’ve loaded all those profiles. If one profile is unknown, you’ll get an error like “profile not found” or it will skip it. So multi-profile requires making all relevant definitions available.
- If you are authoring IGs, sometimes they are layered (like core profile -> derived profile). In such case, a resource might only list the derived, but by definition conformance to derived implies conformance to base. You might not need to list both. But if you do, the validator might double-check both which is redundant but should pass if one inherits the other’s constraints anyway.
Cross-Resource Validation
This refers to validating relationships across multiple resources:
- Referential Integrity: We covered ensuring references resolve (Error #13). But beyond existence:
- Consistency of data across resources: For example, if you have an Encounter and a Procedure that references that Encounter, you might want to ensure the dates align (Procedure date is during Encounter period). FHIR doesn’t enforce that, but your system might as a business rule. Implementing this is non-trivial in a generic validator because it needs access to all related resources and some rule definitions.
- Document-level checks: A FHIR Document (Composition + entries) might have constraints like “Every section code must correspond to the type of resource referenced” or “Composition.subject should be the same as subject of entries” (depending on design). You can validate a whole bundle for these conditions. The official validator does some document checks (like composition presence, etc.). You can add more if needed.
- Transaction logic checks: In a Bundle of type transaction, maybe you want to enforce that if there’s an Observation on a new patient (Patient resource also in bundle), the Observation.subject points to that Patient’s temporary ID. The validator might not catch mismatches if you used wrong reference (like pointing to a patient not in bundle nor existing). There is an invariant on Bundle for fullUrl uniqueness and that references should resolve within bundle or to existing – but resolution to existing is only known if on server.
- Cohort or Aggregate checks: e.g., If you have two AllergyIntolerances for the same substance, maybe your policy is to have only one active at a time. That’s not a single resource validation, but a cross-resource check. Usually out-of-scope for a validator, but some might incorporate as a business rule (like “duplicate allergy” rule). This drifts into application logic territory, but you could incorporate it by doing a search in validation pipeline for other similar resource (like check DB if any other active allergy with same code exists).
- Workflow state machine rules: E.g., if you have a DiagnosticReport referencing an Observation, ensure that if DiagnosticReport.status = final, the Observation.status is also final (not still preliminary). This is a consistency rule across resources. Implement by fetching or having the Observation available and comparing fields. The FHIR validator won’t know to do that by itself (no such invariant in base spec), so it’s on you if needed.
To perform cross-resource validation, you need context:
- If validating a Bundle, you have all entries, so you can check relationships within it. You could write a custom routine that, after validating each entry individually, then checks pairs or groups of entries for consistency.
- If validating a single resource, you might need to query a FHIR server or have references resolved to actual objects and then run some logic. HL7 validator has a
Fetcher
interface – if configured, it can fetch referenced resources (like to ensure a code is valid or a reference exists). You could piggyback on that to fetch referenced resource and then do some extra checks. For instance, if Observation.subject is Patient/123, fetch Patient/123 and ensure Patient is active or something (if that’s a rule). - This gets into the realm of workflow or business validations more than pure spec conformance. Often implemented in the application/service layer rather than the generic validator. But it’s worth highlighting because in real deployments, these consistency checks improve data quality.
One practical example: If an Encounter is in progress and someone tries to create a second overlapping Encounter for the same patient (without discharge or proper linking), an HIE might treat that as an error or warning. To catch that, during validation of a new Encounter, you’d query for existing open encounters for that patient. If found, you produce a business-rule error. That’s not standard FHIR validation, but it’s an advanced validation your pipeline might do.
Another advanced area:
- Validating QuestionnaireResponse vs Questionnaire – The spec allows using the Questionnaire to validate the structure and content of a QuestionnaireResponse. HL7 validator supports that if you provide the Questionnaire (it checks enableWhen, required questions answered, types match). This is a cross-resource validation of a sort (between a definition and an instance).
- Similarly MeasureReport vs Measure, etc., but those are less common.
Validation of Custom Resources or Extended FHIR Versions
If you are using an IG that defines new resource types or profiles that essentially transform structure heavily, you may need to extend the validator:
- HL7’s validator is updated with each version to know new resource types. If you invent a new Resource (as some realms do via extensions or local models), the generic validator might not know it. You’d have to supply a StructureDefinition for it and hope it can handle it as an “extension” resource. It can validate it as an instance of a custom profile if well-defined, but if you have new primitive types or radically different structures, more customization is needed.
Security Considerations in Validation
We touched on narrative, but broadly:
- If you are building a validator, ensure it is hardened against malicious input (since it might be exposed via $validate). For example, avoid path traversal if validator loads files, avoid XML External Entity (XXE) attacks if validating XML (disable DOCTYPE processing).
- DoS: Validate performance to avoid scenarios where a specially crafted resource with thousands of elements or deeply nested structures could cause the validator to hang or use excessive memory. Some validators have depth limits or size limits to mitigate that.
- The
too-long
error type exists to allow validators to bail on huge content. In your custom pipeline, you might implement similar cutoffs – e.g., if a resource has more than N elements or a string is extremely long, stop processing further and reject it (to protect your system).
These advanced topics show that validation is not one-size-fits-all; it can be tailored to specific domain needs and integrated deeply with business logic. It also demonstrates why HL7’s own guidance sometimes says to be careful about validation in production – because beyond base rules, each use case might have additional rules, and overly strict enforcement without human override can cause data flow issues. A balanced, well-tested approach is key.
Finally, let’s look at some real-world case studies to see how organizations have tackled FHIR validation and what lessons can be learned, then compare tooling options available and wrap up with actionable insights.
Real-World Case Studies in FHIR Validation
To ground our understanding, here are a few anonymized (or generalized) case studies demonstrating FHIR validation challenges and solutions in practice:
Case Study 1: National Health Exchange – Balancing Strictness and Data Completeness
Context: A national health information exchange (HIE) implemented a FHIR-based portal where hospitals submit clinical documents (FHIR Document Bundles) for patient referrals. Initially, they configured the server to strictly validate every incoming Bundle against national profiles (based on HL7 Profiles) and required each reference to resolve.
Problem: In early testing, they found that ~30% of submissions were rejected. Common errors:
- Missing required elements (particularly, smaller clinics didn’t include patient middle names which the profile made mandatory).
- Some hospitals used local codes for lab tests instead of LOINC, causing “code invalid” errors.
- References to older Patient records that weren’t yet present on the exchange (so “not-found” errors).
This high rejection rate meant real patient info wasn’t flowing. Clinicians were frustrated as they had to re-submit or fill in extra data often.
Solution: The HIE adopted a Postel’s Law approach:
- They softened certain profile requirements: e.g., while the national spec said Patient.name 2..* (requiring given and family name), they decided to accept 1..* (at least one name) and just warn if middle name missing. They accomplished this by customizing the profile (creating a local variant) and adjusting the validator configuration to treat that specific rule as warning.
- They built a code mapping service: When “invalid code” errors occurred for known local codes, instead of outright rejecting, they translated the code to the expected standard (logging the event). For instance, if a lab result came with a local code, the system looked it up in a map to find a LOINC equivalent and inserted that before validation. This reduced terminology errors significantly.
- For references, they changed the pipeline to pre-create stub resources if necessary: If a Bundle referenced Patient/123 and that patient wasn’t in the system, they would create a basic Patient record (with the provided demographics) on the fly, so the reference could resolve (essentially following the “create on demand” pattern). They still logged this and expected the source to eventually provide full patient details, but it prevented immediate failure.
- They moved some validation to an asynchronous audit: The exchange stored incoming data even if not perfectly valid, then overnight ran a job to flag any records that still had validation issues. Those were reviewed by data stewards who could manually correct obvious issues (like adding a missing gender if they could find it, or contacting the source).
Outcome: The rejection rate dropped from 30% to under 5%. Critical errors (like completely unparseable data or major inconsistencies) still caused rejection – which is good, as those often indicated mis-identified patients or corrupt data. But non-critical issues were handled gracefully, maintaining data flow. The HIE also provided feedback reports to each hospital monthly, summarizing validation warnings (e.g., “500 of your submissions in March were missing patient middle name; our system filled them as ‘UNK’. Consider updating your process to include full name.”). Over time, these reports led some sources to improve their data at the source, reducing even warnings.
Lesson: Rigid adherence to all validation rules can hinder interoperability. A pragmatic approach that categorizes issues into must-fix vs can-fix-later kept the system operational. Also, investing in tooling (code mapping, automated fixes) upfront saved a lot of manual resubmission effort.
Case Study 2: EHR Vendor – Validation for Certification
Context: An EHR vendor had to pass the ONC’s certification which included demonstrating support for US Core profiles. This meant their FHIR API had to both produce and consume resources that conform to US Core (a set of profiles on Patient, Observation, etc.). They used HAPI FHIR server as the base.
Problem: Early in development, they found that some resources they output did not actually pass validation against US Core profiles:
- They missed some required extensions (like
race
andethnicity
extensions on Patient in US Core). - Some valuesets (like
Patient.telecom.system
had required binding) were not enforced (developers let any value through). - Additionally, when they ingested resources from external apps, if those had slight deviations, the EHR’s HAPI server (with validation interceptor on) would reject them, causing interoperability issues.
Solution: The vendor undertook a comprehensive validation improvement project:
-
They integrated the HL7 FHIR Validator CLI into their build process to validate all example resources for each supported profile. They took sample patient, allergy, etc., and made sure the output of their API (the JSON) when run through the validator came out clean. Initially, it did not – the validator pointed out missing
ombCategory
in race extension, wrong codes for telecom, etc.. Developers fixed those in code, iterating until all sample outputs passed. -
They added unit tests for their FHIR model serialization: for example, create a Patient with all required US Core fields in code, serialize to JSON, and then programmatically call the validator (via HAPI’s
FhirValidator
) on that JSON string, expecting no errors. This was automated in CI. -
For the input side (consuming data):
- They implemented a custom parser error handler that was a bit lenient: if an unknown element is encountered and it’s something harmless (like an extension they don’t recognize), they log it but don’t fail parsing (HAPI by default can be configured to ignore unknown elements vs. error).
- They decided not to strictly validate every incoming resource in real-time, but rather after storing it. They attached a background validation process that would validate newly created records from external sources. If an issue was found, they’d mark that record with an “issue” flag and include the OperationOutcome in an admin dashboard. This allowed them to pass certification (because their outputs were correct and their inputs didn’t crash on bad data), while still monitoring data quality.
-
One particular focus was performance: during ONC testing, they had to run some large patient data exports. The validation on output was done by test proctors using Inferno (the testing tool), which internally validates the responses. The vendor pre-validated those responses with the HL7 validator to ensure Inferno would find no fault. They also turned off HAPI’s schema/schematron validation to rely solely on the instance validator (which was faster and more informative).
Outcome: The vendor passed certification. Post-certification, they left many of these validation checks in place in their product. This caught some integration bugs early – for instance, after an update, a developer accidentally allowed an Observation.component with no code
, which is not allowed (invariant obs-6 says each component must have a code). Their test caught it immediately because validation failed. They fixed it before any release.
Lesson: Using validation tooling as part of development (not just at runtime) improves quality. By treating validation errors as unit test failures, the EHR ensured compliance continuously. It also highlighted that being too strict on input can be counterproductive; better to accept and flag than reject and break interoperability – especially during integration with third-party apps where control over their output is limited.
Case Study 3: Research Data Warehouse – FHIR Data Quality Auditing
Context: A research institution built a FHIR-based data warehouse, ingesting data from multiple hospital systems. Data was converted to FHIR and stored, primarily to be queried for analytics (not necessarily to be re-exposed as FHIR). The priority was completeness of data for research, but also quality because analysis can be skewed by bad data.
Problem: When initially ingesting data (converted from HL7 v2 messages to FHIR), they found many validation errors:
- Demographics conversions sometimes created invalid entries (e.g., an Address.state had numeric codes not matching any known value set, or sometimes state was given as full name when expecting abbreviation).
- Their v2-to-FHIR mapping occasionally put values in wrong fields (they found some lab results where the unit was in the
Observation.code.text
by mistake). - There were also legacy data quirks, like notes that ended up in Observation.value that were too long or contained special characters not allowed in certain code fields.
They didn’t want to lose data, but they needed to know what was “clean” and what wasn’t.
Solution: They established a validation audit pipeline:
-
Every FHIR resource that was loaded into the warehouse would also produce a validation report (OperationOutcome). They used the HL7 validator library inside their ETL process to generate this. They stored each OperationOutcome alongside the resource (maybe in a separate collection/table).
-
They did not prevent storage of resources with errors (because analysis might still use partial data), but they flagged them. Researchers querying data could choose to filter out records that had validation errors if they wanted more trustworthy subsets.
-
They analyzed the OperationOutcome logs to identify systematic issues in their conversion:
- For example, many errors like “valueQuantity unit is invalid” revealed that one source system was sending units like “mg per dl” which didn’t match UCUM codes. The fix was to map those to proper UCUM (mg/dL).
- Errors about address state not in valueset (for US addresses, a two-letter code is expected): they found one source using numeric codes for states, so they implemented a translation to standard state abbreviations.
- The note about lab results: seeing an error “Observation.code coding code system is not the one expected” led them to find the mapping bug and correct it.
-
Over a year, they saw the validation error rate drop as they patched mappings and cleaned data sources. They still kept validation on to catch new issues.
-
In some cases, they decided certain errors were acceptable for their use: e.g., an “invalid phone number format” in Patient.telecom was not critical for their research, so they chose to ignore telecom errors in their quality scoring. But patient gender missing might be important, so that they flagged.
-
They created a data quality dashboard showing % of records by type that are fully valid, have warnings, or have errors. This was used as a metric in data governance meetings. For instance, an increase in valid allergy data over time indicated success of a data entry training program.
Outcome: The warehouse achieved a high level of data consistency, making researchers more confident in using the data. When outliers or anomalies occurred (like an unexpected format from a new data feed), they caught it early via validation logs and addressed it. This proactive auditing saved them from drawing conclusions on faulty data in studies. It also helped them provide feedback to source systems (“hey, 5% of lab results from Hospital X had invalid LOINC codes last month, please check your mappings”).
Lesson: Validation isn’t only for interoperability compliance; it’s a tool for data quality management. Even if you don’t enforce every rule at intake, using validation results to continuously improve the ETL/mapping process yields better data for end use. Storing validation outcomes allows flexible use – filtering or weighting records by quality.
Each of these case studies illustrates different aspects: one about being flexible in production, another about leveraging validation for compliance and testing, and the third about using it for data quality in analytics. In all cases, a thoughtful application of validation (not just pass/fail blindly) was key.
Now, moving on, let’s compare the tooling and libraries available for validation – as those were all crucial in the above stories – and discuss their pros/cons and best use cases.
FHIR Validation Tools and Libraries: Comparison
There are several tools and libraries available to perform FHIR validation. Choosing the right one (or combination) depends on your tech stack, performance needs, and depth of validation required. Here we will compare the major ones in terms of capabilities, ease of use, and ideal use cases:
1. HL7 FHIR Validator (Java CLI & Library)
-
Description: The official reference validator provided by HL7, implemented in Java. Usable via a command-line JAR (
org.hl7.fhir.validator.jar
) or through Java API (part of theorg.hl7.fhir.validation
package). -
Pros:
- Comprehensive: Supports validation against base spec and profiles, handles XML/JSON, checks all rule types (schema, schematron, invariants, terminology, etc.).
- Up-to-date: Maintained by HL7, updated for new FHIR versions (even pre-release snapshots).
- Rich Output: Produces OperationOutcome with detailed issue list, good for returning to users or analysis.
- Configurable: You can supply custom IG packages, choose which profiles to validate, and toggle certain checks (e.g., can choose not to fetch terminology if offline).
-
Cons:
- Performance: Not the fastest, especially with large profiles or lots of terminology. Can be memory-intensive when validating very large resources or many at once.
- Requires Java: If your environment isn’t Java-friendly, calling the CLI can be cumbersome (though doable).
- Learning Curve: Many options (which is a pro once mastered, but initial use might be confusing without reading docs). For example, loading custom packages via
-ig
or dealing with version-specific jar differences.
-
Best Use:
- Development/Testing: Great for IG developers to validate examples, or for QA to verify conformance.
- Command-line validation in CI (there’s no need to write code; just run the jar with your files).
- Integration in Java servers (like custom validation interceptors in a HAPI server can delegate to this).
- Not ideal for high-throughput at runtime due to speed, unless you have resources to scale it or accept the overhead.
2. HAPI FHIR Validator (Java)
-
Description: Part of the HAPI FHIR library. It historically had its own implementation for DSTU2/DSTU3, but for R4+ it largely delegates to the HL7 validator for instance validation while providing a nice API and integration in the HAPI ecosystem.
-
Pros:
- Easy Integration with HAPI: If you’re using HAPI FHIR server or structures, you can validate HAPI model objects directly (no need to serialize to JSON first). For example,
FhirValidator validator = ctx.newValidator(); ValidationResult result = validator.validate(resource);
. - Pluggable Modules: HAPI offers modules like Schema/Schematron, InstanceValidator, and custom
ValidationSupport
to plug in additional logic or cached valuesets. - Extensibility: Can create custom validators (implementing
IValidatorModule
) to add on checks. Also can override messages via localization. - Documentation & Community: HAPI has decent docs and a large user community. You can find FAQs or ask questions (on their Google group or StackOverflow).
- Easy Integration with HAPI: If you’re using HAPI FHIR server or structures, you can validate HAPI model objects directly (no need to serialize to JSON first). For example,
-
Cons:
- Setup: Need to manage FhirContext (for correct version) and ensure to include the validator module dependencies (especially if using the InstanceValidator, you need the org.hl7.fhir.validation and profile definitions).
- Same performance caveats as HL7’s core, since it uses it. HAPI adds a tiny overhead for model traversal.
- If not using HAPI models (say you have just JSON strings), you either parse into HAPI or call the HL7 validator directly.
-
Best Use:
- In Java applications that already use HAPI for FHIR interactions. E.g., embedding validation in a FHIR server (HAPI’s RestfulServer can call validator in interceptors).
- Custom validation logic where you want to mix standard validation with your own checks easily – you can add a custom IValidatorModule that, for instance, checks your business rules after the standard one runs.
- If you want to validate resources in memory (HAPI objects) without round-tripping to text, this is great.
3. Firely .NET SDK (Validator component)
-
Description: The official HL7 FHIR SDK for .NET (a.k.a. Firely SDK, previously called FHIR .NET API) includes a validator (FHIRValidator class).
-
Pros:
- Native .NET: If you work in C# or .NET environment (e.g., building on Microsoft stack, or using Azure API for FHIR which is built on it), this is the go-to. It integrates with POCO models from the SDK.
- Feature Parity: It supports similar features to the HL7 validator – profiles, slicing, invariants, etc. Firely’s Simplifier docs provide details on validator codes and behavior.
- Performance: Generally, pretty good in .NET – Microsoft’s own FHIR server uses it under the hood, handling large loads. The code is optimized in C#.
- Tooling integration: If you use Firely’s Forge or Simplifier, the validation is aligned with those (e.g., you can validate directly in Forge when designing profiles).
-
Cons:
- Windows-centric sometimes: While .NET Core is cross-platform, historically many .NET health systems are Windows-based. Less of an issue now with .NET 5/6 though.
- Documentation is a bit scattered (Simplifier has docs, and the GitHub has some).
- If you need to modify or extend, you might need to dive into their code or use hooks they provide (like custom resolvers for references or custom profile sources).
-
Best Use:
- Any .NET application needing FHIR validation. For example, a plugin in a C# EHR that takes FHIR data from an API and needs to validate it before processing.
- Azure Functions or other cloud components in .NET that validate data (like an Azure Function triggered by FHIR export maybe verifying stuff).
- If your team is .NET-centric and not keen on running Java tools, this is the obvious choice.
4. Inferno/Touchstone (Online Validators)
-
Inferno is an open-source test tool used for ONC certification. It has an online interface and uses the reference validator behind the scenes. You can feed it an endpoint or resource and it will validate mainly for US Core and other program requirements.
-
Touchstone by AEGIS is a cloud-based FHIR testing platform. It also has validation services and will give detailed reports.
-
Pros:
- You don’t have to maintain anything; just use the web service.
- They often update to latest profiles and tests.
- Useful for one-off checks or certification testing.
-
Cons:
- Not suitable for automating within your system beyond maybe calling an API (Touchstone has APIs but it’s a paid service for extensive use).
- Data sensitivity: you might not want to send PHI to a cloud validator.
-
Best Use:
- During development, sanity check your server by hitting these external validators (especially if preparing for an interoperability event).
- As a double-check to compare your local validator’s findings vs an external one (if there’s a discrepancy, one might have a bug or outdated profile).
- Not for real-time production use due to dependency on external service and throughput limits.
5. JavaScript Libraries (AJV + FHIR Schema, Data4Life JS validator, Lantanagroup FHIR.js, etc.)
-
AJV + JSON Schema: HL7 provides a JSON Schema for each FHIR version. You can use AJV (Another JSON Schema Validator) in Node to validate JSON objects against it. Data4Life’s
js-fhir-validator
basically does this with precompiled schemas. -
FHIR.js by Lantana: A library that can parse and do some validation, but not sure how up-to-date it is (last I saw, it was DSTU2/STU3 era).
-
Pros:
- Fast for structure: JSON Schema validation in JS is extremely fast thanks to V8 optimizations. If you want to validate 1000 resources in the browser or in Node quickly for structure, this is great.
- No external dependencies: You can include a JS lib in a web app to validate a resource client-side before sending to server, giving instant feedback to the user if say required fields missing.
- Data4Life’s library is straightforward: just call
validate(resource)
and it returns issues for structure and cardinality.
-
Cons:
- Limited scope: As noted, JSON Schema can’t enforce everything. Terminology, slicing, invariants requiring complex logic are not handled. So you might mark something as valid structurally but it could still fail server validation for deeper reasons.
- Some libraries might be outdated or incomplete for latest FHIR (check support: Data4Life supports R4 (4.0.1) and STU3, but not R4B or R5 at time of writing).
- If you need profile validation, these won’t handle it except if you somehow generate a specific JSON schema for a profile (which is possible for some constraints, but not all).
-
Best Use:
- Client-side form validation or UI apps: e.g., a SMART on FHIR app that lets user enter data – you can validate their input with a JS validator before calling
$submit
to FHIR API, to catch errors early. - Performance-critical bulk checks where you only need basic validation: e.g., checking that a million records have the right structure (maybe before deeper processing).
- Not sufficient alone if you need full compliance checking, but a great first-line filter.
- Client-side form validation or UI apps: e.g., a SMART on FHIR app that lets user enter data – you can validate their input with a JS validator before calling
6. Other Tools:
- IBM FHIR Server’s validator: IBM’s open source FHIR server (in Java) has its own validation component, possibly derived from or similar to HAPI’s. If you use that server, you’d use its built-in.
- Smile CDR: Being based on HAPI, it uses HAPI’s validator. They might have proprietary enhancements for performance, but conceptually similar.
- Custom Scripting: Some have used Python fhir.resources library with pydantic models to validate structure. That covers cardinality and types at the model level using Python classes (the fhir.resources package auto-generates classes from StructureDefinitions). It won’t do invariants or terminology by default, but if you’re in Python and want a quick structure check, that’s an option.
- Aidbox FHIR Schema: Aidbox (a FHIR server by HealthSamurai) uses a custom approach combining JSON Schema and SQL for validation. They even open-sourced a “FHIR Schema” specification. While not commonly used outside Aidbox, it shows alternative patterns.
Comparison Summary Table:
Validator | Language/Platform | Covers | Pros | Cons | Ideal Use |
---|---|---|---|---|---|
HL7 FHIR Validator | Java CLI/Library | Structure, Cardinality, Values, Invariants, Terminology, Profiles | Complete, official; detailed output | Slower; needs Java; memory heavy on big loads | IG development, one-time validation, integration in Java servers, compliance testing |
HAPI FHIR Validator | Java (HAPI lib) | Same as HL7 (uses HL7 under hood) plus modular architecture | Easy if using HAPI models; extensible; community support | Similar performance to HL7; requires adding HL7 structures jars | Java apps using HAPI, need to validate on the fly or with custom rules |
Firely .NET Validator | .NET (C#) | Full spec validation (like HL7) | Native .NET; used in MS ecosystem (Azure) – proven performance | .NET specific; documentation mainly via Simplifier | .NET applications, Azure FHIR services, integration in Microsoft environments |
JSON Schema (AJV) | JavaScript/Node/Web | Structure, Cardinality, basic types | Extremely fast; run in browser; easy to use with AJV | No invariant or valueset checks; needs up-to-date schemas | Client-side validation, quick pre-checks in Node, high-volume structural filtering |
Data4Life JS Validator | JavaScript | Structure (FHIR 3.0.1, 4.0.1) | Simple API, uses JSON schema; lightweight | Limitations (no slicing, invariants, etc.) | As above, plus embedded in JS apps for basic validation |
Inferno/Touchstone | Cloud service (multiple tech) | Full (they use underlying libs) | No setup required; comes with additional scenario tests (esp. Touchstone) | Online only; usage limits; not integrated into prod easily | Certification testing, manual validation double-check, interoperability testing events |
Custom (Various) | Any (Python, Go, etc.) | Varies – you define | Tailored exactly to needs; can be optimized or simplified | Requires dev effort; risk of missing rules; maintenance overhead | Very specific use cases (e.g., internal format conversion where only subset of FHIR used, performance hacks) |
Choosing the Right Tool:
- If you need strict compliance and are in a Java or .NET environment, go with the official validator (via HAPI or Firely accordingly). They ensure you meet HL7’s expectations and will be easier to justify (especially for certification).
- If you need to validate within a Node.js microservice or a client-side app, use the JSON Schema approach for speed, but know its limits. Possibly complement it by occasionally sampling data through a full validator for deeper issues.
- For profile-heavy projects (like national programs with custom IGs), you likely need the full validator so you can load those profiles and check must-supports, etc.
- For interactive client apps (like SMART apps or form interfaces), a combination: use JS validator for instant feedback on required fields, and perhaps have the server do final validation with OperationOutcome returned for anything the client missed (like an invariant).
- If you are integrating with a particular FHIR server product, consider using the same validation engine it does for consistency. E.g., if using HAPI server, use HAPI’s validator so that what you validate in testing will match what the server will accept in production.
Library Support & Maintenance:
- HAPI and Firely are actively maintained with community contributions and update for new FHIR versions promptly (often experimental support for new drafts too).
- JSON Schemas for new FHIR versions are generated by HL7 – e.g., R4, R4B, R5 all have schemas you can get from HL7 GitHub. Community will likely update things like Data4Life’s lib if interest.
- The HL7 validator is updated by core team (Grahame Grieve and others) – keep an eye on release notes, they often fix edge-case validation bugs. Upgrading might cause some resources that passed before to fail or vice versa, due to bug fixes or stricter enforcement, so regression test accordingly.
Next, let’s wrap up with a conclusion focusing on actionable insights and future trends in FHIR validation.
Conclusion: Ensuring Data Quality and Interoperability Through Validation
FHIR resource validation is a critical process that guards the quality and integrity of health data as it moves between systems. We’ve journeyed through the fundamentals of validation, dissected common errors and their resolutions, explored performance and pipeline strategies, and reviewed tools and real-world experiences. Here are the key actionable insights and a look at future trends to keep in mind:
-
1. Validate Early and Often – But with Purpose: Incorporate validation into your development and integration cycles from the start. Use automated tests to catch issues before they reach production. However, be purposeful about what is a show-stopper vs. what can be a warning. Action: Set up a validation step in your CI pipeline (e.g., run
FHIRValidator
on sample data or use unit tests with a validator) so that developers get immediate feedback on conformance. -
2. Prioritize Critical Errors: Not all validation failures are equal. Missing a required patient ID is critical; an unexpected extension that you can ignore is not. Action: Define a validation policy in your organization: which issues result in rejection vs. acceptance with warnings. This policy should be informed by patient safety, data usability, and regulatory requirements. For instance, you might decide that any
required
element missing (IssueTyperequired
) is an error, but an unknown extension yields a warning. Implement this in your validation pipeline so it automatically classifies issues by severity. -
3. Leverage Standard Profiles and Adapt as Needed: If you’re in a jurisdiction with required profiles (US Core, NHS UK Core, etc.), ensure you validate against them to catch deviations. At the same time, be ready to adapt profiles or use extensions to handle real-world data that doesn’t neatly fit. Action: Load the relevant Implementation Guide packages into your validator so that you’re checking those rules. If certain profile constraints cause undue rejections (like in Case Study 1, middle name missing), work with the governing body or use an extension/variance to accommodate – while still tracking those occurrences.
-
4. Build a Feedback Loop with Validation Results: Don’t treat validation errors as the end of the line – use them to improve. Action: Maintain an error log or dashboard (OperationOutcome details) and review it regularly. If a particular error keeps occurring (e.g., code system mismatches or certain missing fields), address it either by educating the data source, updating mappings, or adjusting the acceptance criteria. Over time, aim to reduce the frequency of each error type. This approach turns validation into a continuous quality improvement tool rather than just a gatekeeper.
-
5. Optimize for Performance at Scale: In high-throughput scenarios, naive validation can bottleneck. Action: Apply the optimization strategies discussed: caching structure definitions and valuesets in memory, parallelizing validation tasks, and possibly toggling off expensive checks (like terminology) for non-critical flows. Profile your validation – know if, say, 80% of time is spent on terminology lookups, then invest in a terminology server or cache to alleviate that. Use bulk validation tools or multi-threaded options for batch jobs. If needed, perform asynchronous validation (accept data then validate in background) to not block real-time processing, but ensure to handle any errors found asynchronously (e.g., alert someone or flag records).
-
6. User-Friendly Error Reporting: Remember that the ultimate goal is often for a human (developer or end-user) to correct the data. Action: Strive to make error messages clear. Use the OperationOutcome provided by validators but consider mapping or annotating them with user-friendly explanations (perhaps in your API documentation or UI). For example, instead of just returning “Element value invalid”, add context: “Patient.birthDate format is invalid, expected YYYY-MM-DD”. This helps partner developers fix issues faster. If you localize for non-English users, leverage validator localization features or post-process the messages for key issues.
-
7. Robust Validation Pipeline Checklist: To ensure nothing is missed, here’s a quick checklist for building your validation workflow:
- ✅ Parse and Schema Check – Confirm basic JSON/XML validity and correct resourceType.
- ✅ Structure & Cardinality – Validate against base FHIR structure (no unknown elements, required ones present, cardinalities respected).
- ✅ Data Types & Formats – Verify all values match expected formats (dates, codes, IDs, etc.). Use regex/pattern checks.
- ✅ Terminology – If using required valuesets, validate codes or plan a fallback if code not found (like mapping).
- ✅ Invariants & Business Rules – Check co-occurrence and logical rules (either via FHIRPath invariants or custom scripts).
- ✅ Profiles – If applicable, validate against each relevant profile, ensuring slices and extensions are correct.
- ✅ Cross-resource consistency – If relevant, verify references resolve and key data is consistent across related resources (especially in transactions or documents).
- ✅ Output OperationOutcome – Collect all issues into a coherent report to return or log.
- ✅ Error Handling Path – If errors, decide automated rejection vs. queuing for manual review. Implement the chosen path (e.g., return 400 with OperationOutcome, or accept but flag record).
- ✅ Logging & Monitoring – Store validation outcomes and monitor trends.
-
8. Stay Updated and Embrace Future Trends: The world of FHIR (and its validation) continues to evolve:
- FHIR R5 and Beyond: New versions bring new resources and changes. For example, FHIR R5 introduces some new invariants and changes in terminology bindings for certain resources. Ensure you update your validators to support new versions if you plan to adopt them. The core principles remain, but specifics can change – like a new required element or different code system version.
- Shifting to R4B/R5: Many are still on R4 (4.0.1), but R4B (4.3.0) and R5 are emerging. If you plan to validate resources from mixed versions (say a server supporting both R4 and R5), you might need separate validation flows or a validator that can handle multiple versions (HL7’s can if pointed to correct definitions).
- Artificial Intelligence and Validation: Looking ahead, there’s exploration around using AI/ML to identify data anomalies that deterministic validation might not catch (like context-based errors). While AI isn’t going to replace formal validation rules, it could supplement them (e.g., an AI might flag that an Observation value seems out of normal range even if it formally “validates”). Keep an eye on research in this area; in a few years, validation toolsets might include an AI-driven suggestion component (there’s even talk of “FHIR-GPT” for semantic checks).
- Automated Fixes: The future might see validators not just flag errors but also attempt fixes (with permission). For instance, a validator could automatically correct common format issues (like trim spaces in IDs, or add a default meta.profile if obvious). Some early versions of this concept exist (e.g., IBM’s data quality tool tries to autocorrect minor issues). When these become robust, incorporating auto-fix for minor issues can streamline pipelines further – with oversight.
- Community & Tools: The community around FHIR is active. Tools like Inferno are updated to test not just validation but workflow expectations (e.g., sequences of requests). Staying engaged (HL7 forums, chat.fhir.org) will alert you to known validation pitfalls and updates (for example, if a particular invariant is known to be problematic and might be reworked in future).
-
9. Documentation and Education: Ensure that those providing you data (and those consuming your APIs) know about your validation requirements. Action: Publish guidelines or a “Conformance Expectations” document for your API: list the profiles supported, examples of required fields, and maybe even sample OperationOutcome for common mistakes. This can prevent a lot of trial-and-error for integrators. Additionally, train your internal team on reading validation errors – for instance, how to interpret a location like
Observation.component[1].code
in an error and quickly find it in the resource. -
10. Don’t Let Perfect be the Enemy of Good Data: Ultimately, validation is about ensuring interoperability – data should be understandable and useful on the receiving end. It’s not about passing a test for its own sake. Sometimes that means making pragmatic calls – like accepting slightly imperfect data because it’s better to have it than not, and working to improve it over time. Use validation to enable interoperability, not unintentionally hinder it. As HL7’s guidance implies, be strict in what you send (if you’re the source) and forgiving in what you accept (if you’re the receiver), within reason.
In closing, implementing FHIR validation is both an art and a science. The science lies in the well-defined rules of the specification and the precise error messages produced. The art comes in deciding how to apply those rules in real-world ecosystems – when to enforce, when to warn, how to streamline performance, and how to evolve with changing standards. By understanding common pitfalls and solutions (the 20 errors we detailed), using the right tools for the job, and building a thoughtful validation workflow, you can significantly enhance the quality of data in your healthcare integrations.
Future Outlook: As FHIR adoption grows (with national programs and new domains like genomics or social data coming into play), validation will only become more crucial. We may see more domain-specific profiles (e.g., for public health, research, etc.) with their own rules – validators will integrate those. Collaboration between systems will likely drive more uniform validation standards (perhaps a certification for validators themselves to ensure consistency). Performance will improve with community contributions (e.g., more pre-built caches, cloud-based validation microservices). And importantly, validation will shift “left” – meaning it will become a common part of development practice (not an afterthought) as more developers learn about FHIR.
By following the guidance in this comprehensive tutorial, you’ll be well-equipped to implement FHIR validation that is thorough, efficient, and aligned with your organization’s interoperability goals. Remember, quality health data is the foundation of quality care – and robust validation is one of the tools that help us achieve it.