Profiles
FHIR provides a broad variety of resources types to cover as many different types of healthcare data as possible, favoring generality over specificity. For example, the Observation
resource type is used to record many different kinds of data: a patient's smoking status might be recorded using the valueCodeableConcept
field, while the measurement data for blood pressure would exist as two separate entries under component.valueQuantity
.
To meet more specific use cases, FHIR allows developers to author resource profiles to layer additional validation rules onto the base specification for a resource type. This is similar to "subclassing" in object oriented programming languages.
Medplum developers can take advantage of FHIR profiles in the following ways:
- Complying to a certain data quality standard. In the United States, the US Core profiles specify a minimum set of required data (called USCDI) for interoperating with other US healthcare entities.
- Enforcing an internal schema (i.e. an organization's own data quality rules),
- Fulfilling data requirements of third-party APIs
For example, the US Core Blood Pressure profile requires that component
contains two
correctly-coded entries: one systolic pressure measurement, and one diastolic measurement. If a resource under
this profile contains just one measurement, or uses an incorrect code for either component, it will be rejected by the
server. This helps ensure data quality by preventing data that does not match the expected schema from being written.
Creating Profiles
The schema for each FHIR resource type is defined by a StructureDefinition
resource. By default, Medplum ships with StructureDefinitions
for each FHIR base resource type and for Medplum defined resource types. The source data for these StructureDefinitions
can be found the @medplum/definitions package.
FHIR profiles are also stored as StructureDefinition
resources that inherit from the base schemas. You can create a new profile in your Medplum project simply by uploading the corresponding StructureDefinition
to your project.
Authoring profiles from scratch can be complicated and time consuming. Many organizations publish implementation guides with collections for FHIR profiles, tailored to specific healthcare domains.
For example:
- US Core: Establishing the “floor” of standards to promote interoperability throughout the US healthcare system.
- PDex Payer Networks: Health insurers' insurance plans, their associated networks, and the organizations and providers that participate in these networks.
- US Drug Formulary: Health insurers' drug formulary information for patients/consumers.
- Dental Data Exchange: Standards for bi-directional information exchange dental providers.
Profile adoption
Using a pre-existing profile is simple: placing the canonical URL of the profile in a resource's meta.profile
field
will cause the server to attempt to validate the resource against that profile when the resource is written. Normally,
the Patient
resource type has no required fields, but the US Core Patient profile specifies that
at least name
, gender
, and identifier
must be populated. Uploading a profiled Patient
resource without those
fields will produce an error:
{
"resourceType": "Patient",
"meta": {
"profile": ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"]
}
}
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "structure",
"details": { "text": "Missing required property" },
"expression": ["Patient.identifier"]
},
{
"severity": "error",
"code": "structure",
"details": { "text": "Missing required property" },
"expression": ["Patient.name"]
},
{
"severity": "error",
"code": "structure",
"details": { "text": "Missing required property" },
"expression": ["Patient.gender"]
}
]
}
To satisfy the profile declared in meta.profile
, values for the required fields must be included in the resource:
{
"resourceType": "Patient",
"meta": {
"profile": ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"]
},
"identifier": [
{
"system": "http://example.com/mrn",
"value": "12345"
}
],
"name": [
{
"given": ["John", "Jacob"],
"family": "Jingleheimer-Schmidt"
}
],
"gender": "male"
}
The corresponding StructureDefinition
resource for the profile (i.e. one with a url
matching that in
meta.profile
) must be present in your Project: make sure to upload the resource JSON for any profiles you
plan to use.
Searching by Profile
You can use the _profile
search parameter to retrieve all resources of a given type that conform to a certain FHIR profile.
Refer to the Advanced Search Parameters guide for more information.
Handling missing data
Sometimes, a profile requires a field that cannot be populated due to missing data: the system may not have the required data available, or it may be unknown. In these cases, the Data Absent Reason extension should be used to satisfy the field presence requirement, while also denoting why the data is missing:
{
"resourceType": "Patient",
"meta": {
"profile": ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"]
},
"identifier": [
{
"system": "http://example.com/mrn",
"value": "12345"
}
],
// For fields with complex (object) data types, add the `extension` field where necessary to indicate absent data
"name": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "masked"
}
]
}
],
// For primitive type fields, use the underscore-prefixed field name to add an object with the `extension` field
"_gender": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "asked-declined"
}
]
}
}
Updating Profiles
Updating FHIR profiles is different than updating other resources in FHIR. The process is more similar to a database migration, and the profiled resources will need to be revalidated once the update is complete. This revalidation does not happen automatically, but will be checked the next time the resource is written to.
This section offers some best practices to make updating profiles as smooth and painless as possible when using Medplum.
When updating a profile, you should create a new StructureDefinition
resource for the updated profile. This will be similar to the original, but it will have the changes you've made, as well as an updated url
field. This allows you to make changes to your profile without invalidating resources that do not yet comply to the new profile.
For example, say you have a patient profile that requires patients to have an associated email address and you want to update it so that they must also have an associated phone number. The steps for this would be the following:
- Check the
url
of the current profile and note the version. - Define a new
StructureDefinition
resource with the desired changes to the profile. - Update the
url
on the newStructureDefinition
with an appropriate new version number. - Create the updated `StructureDefinition.
- Validate and update your
Patient
resources to adhere to the new profile.
Example: Update your profile
// The initial profile represented on a patient resource
{
resourceType: 'Patient',
meta: {
profile: ['https://example.com/profiles/foo-patient/1.0.0'],
},
};
// Update your StructureDefinition
const updatedProfile: StructureDefinition = {
resourceType: 'StructureDefinition',
url: 'http://www.example.com/profiles/foo-patient/2.0.0',
name: 'Patient Profile',
status: 'active',
kind: 'resource',
type: 'Resource',
abstract: false,
};
// Create the new profile
await medplum.createResource(updatedProfile);
In the above example, we have a Patient
with an initial profile. In the url, the profile name is foo-patient
and the version is 1.0.0
.
We then define the new StructureDefinition
. Here you would add any changes to the profile you want. Note that the url
field is the same, except we have updated the version to 2.0.0.
.
When updating FHIR profiles, you should use semantic versioning to assign values to your versions. In the above example, requiring a phone number is a breaking change as it may cause some resources to fail validation. Any backwards-incompatible changes should be considered major changes.
Using versions allows resources to attest to different versions of the profile, so if a patient does not have a phone number they can still reference version 1.0.0
until they are updated. To check which resources need to be changed before updating their profile, you can use the $validate operation.
When you update a profile, you can loop over each resource, validate it, and update the profile if it passes. To help with this, Mepdlum provides the validateResource
helper function.
Example: Validate a new profile against all Patient resources in the system
// Get all the patients in the system
const allPatients = await medplum.searchResources('Patient');
// Loop through the patients
for (const patient of allPatients) {
// Run the validation operation on each patient
const res = validateResource(patient, { profile: updatedProfile });
// If the patient passes validation, update the profile and the patient
if (res.length !== 0) {
if (patient.meta) {
patient.meta.profile = [updatedProfile.url];
} else {
patient.meta = {
profile: [updatedProfile.url],
};
}
await medplum.updateResource(patient);
} else {
// If the patient does not pass validation, implement custom logic to notify the relevant users to update.
console.log(patient, res);
}
}