Profiling and Validation

Overview

FHIR resources are very loose in requirements which gives FHIR its flexibility. For example, all elements are optional in the Patient resource, and it's possible to create a Patient resource without any data which does not make much sense. So, sometimes there is a need to constraint resources. In FHIR, you need to create a StructureDefinition resource and describe the requirements for a resource you want to restrict. And it is definitely not an easy task. There are special tools developed specifically for this. And there is an alternative — custom profiling in Aidbox which is, in fact, a well-known JSON Schema.

Request
Response
Request
POST /Patient
Response
Status: 201
id: 6c2b9ea9-ea57-4f9a-9c9a-6c46cbad43da
resourceType: Patient
meta:
...

For custom profiling, Aidbox provides additional resource AidboxProfile. This resource specifies resource type and JSON Schema which validates the specified resource type.

AidboxProfile Resource Structure

bind

The bind element is of the type Reference. It specifies the resource type which the profile will be applied to.

Example: Binding to Practitioner resource.

YAML
JSON
YAML
bind:
id: Practitioner # Target resource type "Practitoner"
resourceType: Entity
JSON
{
"bind": {
"id": "Practitioner",
"resourceType": "Entity"
}
}

schema

It's a plain JSON Schema object which validates a resource.

Example: Require the name attribute

YAML
JSON
YAML
schema:
type: object
required:
- name
JSON
{
"schema": {
"type": "object",
"required": ["name"]
}
}

Examples

Require Properties

Let's validate newly created Patient resources by specifying that name and gender properties are required. First, we need to create the appropriate AidboxProfile resource.

Request YAML
Request JSON
Response
Request YAML
POST /AidboxProfile
resourceType: AidboxProfile
id: custom-patient-constraint
bind:
id: Patient
resourceType: Entity
schema:
type: object
required:
- name
- gender
Request JSON
POST [base]/AidboxProfile
{
"resourceType": "AidboxProfile",
"id": "custom-patient-constraint",
"bind": {
"id": "Patient",
"resourceType": "Entity"
},
"schema": {
"type": "object",
"required": [
"name",
"gender"
]
}
}
Response
STATUS: 201
{
"bind": {
"id": "Patient",
"resourceType": "Entity"
},
"schema": {
"required": [
"name",
"gender"
]
},
"id": "custom-patient-constraint",
"resourceType": "AidboxProfile",
"meta": {
"lastUpdated": "2018-10-10T14:45:43.801Z",
"versionId": "2",
"tag": [{
"system": "https://aidbox.io",
"code": "created"
}
]
}
}

If you are using Aidbox.Dev below 0.3.1 version, then after creating an AidboxProfile resource, you will need to restart your Aidbox.Dev server.

$ docker-compose down && docker-compose up -d

Now, let's try to create a Patient resource without name and/or gender . You will receive the error.

Request YAML
Request JSON
Response
Request YAML
POST /Patient
resourceType: Patient
birthDate: '1985-01-11'
Request JSON
POST [base]/Patient
{
"resourceType": "Patient",
"birthDate": "1985-01-11"
}
Response
STATUS: 422
{
"resourceType": "OperationOutcome",
"errors": [{
"path": [],
"message": "Property name is required",
"profile": {
"id": "custom-patient-constraint",
"resourceType": "AidboxProfile"
}
}, {
"path": [],
"message": "Property gender is required",
"profile": {
"id": "custom-patient-constraint",
"resourceType": "AidboxProfile"
}
}
],
"warnings": []
}

Require Nested Properties

Let's require given and family elements of the name property. In this case, we are expecting that name attribute of the type HumanName will contain elements given and family. Let's create the AidboxProfile resource with the code below. Then you will need to restart server if you're on Aidbox.Dev.

Request YAML
Request JSON
Response
Request YAML
POST /AidboxProfile
resourceType: AidboxProfile
id: custom-patient-constraint
bind:
id: Patient
resourceType: Entity
schema:
type: object
required:
- name
properties:
name:
type: array
minItems: 1
items:
type: object
required:
- given
- family
properties:
given:
type: array
minItems: 1
items:
type: string
family:
type: string
Request JSON
POST [base]/AidboxProfile
{
"resourceType": "AidboxProfile",
"id": "custom-patient-constraint",
"bind": {
"id": "Patient",
"resourceType": "Entity"
},
"schema": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": [
"given",
"family"
],
"properties": {
"given": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"family": {
"type": "string"
}
}
}
}
}
}
}
Response
STATUS: 201
{
"bind": {
"id": "Patient",
"resourceType": "Entity"
},
"schema": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "array",
"items": {
"type": "object",
"required": [
"given",
"family"
],
"properties": {
"given": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
},
"family": {
"type": "string"
}
}
},
"minItems": 1
}
}
},
"id": "custom-patient-constraint",
"resourceType": "AidboxProfile",
"meta": {
"lastUpdated": "2018-10-11T09:47:18.147Z",
"versionId": "12",
"tag": [{
"system": "https://aidbox.io",
"code": "created"
}
]
}
}

Now, on the Patient resource creation we will be receiving the validation error. Let's try to create a Patient resource without a family name. You will receive the error.

Request YAML
Request JSON
Response
Request YAML
POST /Patient
name:
- text: John Malcovich
given:
- John
Request JSON
POST [base]/Patient
{
"name": [
{
"text": "John Malcovich",
"given": [
"John"
]
}
]
}
Response
STATUS: 422
{
"resourceType": "OperationOutcome",
"errors": [
{
"path": [
"name",
0
],
"message": "Property family is required",
"profile": {
"id": "custom-patient-constraint",
"resourceType": "AidboxProfile"
}
}
],
"warnings": []
}

Validation with zen

Zen validation is currently available in the EDGE version of Aidbox

Aidbox supports an alternative yet very powerful profile validation mechanism powered by Zen language. You can just define a set (or multiple sets) of validation profiles in EDN format and let your Aidbox server know its location.

Configuration

To enable zen powered validation, you need to specify zen libraries in the AIDBOX_ZEN_DEPS environment variable. Aidbox validates resources of every resourceType with aidbox/profile tagged zen schema.

The value of the AIDBOX_ZEN_DEPS variable must be a comma-separated list of @-separated pairs of [email protected] or just core-zen-project-ns without url, if url is not specified, Aidbox will search this zen ns in its classpath. For example: [email protected]://.../foo.zip,[email protected]://.../bar.zip,quux. Zen ns foo will be downloaded from https://.../foo.zip Zen ns bar will be downloaded from https://.../bar.zip Zen ns quux will be read from classpath

core-zen-project-ns should include a zen schema with an aidbox/profile tag specified. Schemas tagged with aidbox/profile should conform this schema:

{:zen/tags #{zen/tag zen/schema}
:type zen/map
:keys {:resourceType {:type zen/string}
:profile-definition {:type zen/string}
:format {:type zen/keyword
:enum [{:value :aidbox}
{:value :fhir}]}
:severity {:type zen/string
:enum [{:value "required"}
{:value "supported"}]}}}

Keyword

Explanation

:type

The full list of zen supported types is here

:resourceType

The profile is applied for resources of this type

:profile-definition

Is the string which should be referenced in the Resource.meta.profile[] for supported profiles validation

:format

format of the data. Default is :aidbox

:format :aidbox

schema is designed to validate data in the aidbox format

:format :fhir

schema is designed to validate data in the FHIR format

:severity

Is related to FHIR profile usage

:severity "required"

The profile is applied to validate all resources of such type

:severity "supported"

The profile is applied only when referenced in Resource.meta.profile[]

:validation-type :open

Optional.

API

Method

Description

GET /$zen-ctx

Returns zen ctx. Useful for debug

GET /$zen-errors

Returns :errors key of zen ctx

GET /$reload-zen-deps

Reloads deps specified in AIDBOX_ZEN_DEPS variable

GET /$zen-get

Search parameters:

tag returns symbols tagged with this tag

symbol returns symbol definition Required to have either tag or symbol parameter (not both)

inline when true substitutes symbols with their definition

A basic step by step guide

Let's define your own basic profile for the Devbox.

For an instance, for any Patient in the system, the gender property is allowed to be male, female, other or unknown. All other values are not valid.

  • Create a file called zen-test-ns.edn with the following content

{ns zen-test-ns
import #{aidbox}
patient
{:zen/tags #{zen/schema aidbox/profile}
:type zen/map
:resourceType "Patient"
:severity "required"
:validation-type :open
:keys {:gender {:type zen/string
:enum [{:value "male"}
{:value "female"}
{:value "other"}
{:value "unknown"}]}}}}
  • Create a zip archive with that file, with the name zen-test-ns.zip

  • Make the file available for downloading by a public URL (without some authentication required). For example, upload it to some cloud storage.

  • Update your .env with AIDBOX_ZEN_DEPS

  • If you have already running Devbox container, you can just reload Aidbox deps by executing a request GET /$reload-zen-deps in the Aidbox Rest Console. Otherwise, just run your Aidbox container as usual.

Let's see how does the defined profile work.

Open the Aidbox Rest Console. Try to create a patient with gender "foo".

Request
Response
Request
POST /Patient
gender: foo
meta:
profile: ["myprofile-definition-url"]
Response
#Status: 422
resourceType: OperationOutcome
text:
status: generated
div: Invalid resource
issue:
- severity: fatal
code: invalid
expression:
- Patient.gender
diagnostics: "Expected 'foo' in #{\"male\" \"female\" \"unknown\" \"other\"}"

As you can see, the patient is not created and there is an explanation, that the expected value should be among the defined list of values.

Now try to create a patient with gender "male".

Request
Response
Request
POST /Patient
gender: male
meta:
profile: ["myprofile-definition-url"]
Response
#Status: 201
meta:
profile:
- myprofile-definition-url
lastUpdated: '2021-04-22T09:31:33.483398Z'
createdAt: '2021-04-22T09:31:33.483398Z'
versionId: '286'
gender: male
id: bdaa680f-2a07-49dd-9131-0882c753bd16
resourceType: Patient

Finally, the validation passed and the patient is created.

IG Profiling step by step guide

Here is a zip archive with a Zen project generated from the US Core IG Repository with some generated Zen projects for popular IGs is here Tools for Zen projects generation from any StructureDefinition will be available later. You can write to us if you want to generate Zen project from some IG

You can load this zip archive into Aidbox using that link or by mounting folder with unarchived zen project into Aidbox classpath

Zip URL example

1. Declare this env variable in your Aidbox:[email protected]://storage.googleapis.com/aidbox-public/zen-profiles/us-core.zip

2. Start Aidbox

3. Import terminology bundle

POST /$import
{"source": "https://storage.googleapis.com/aidbox-public/zen-profiles/us-core-terminology-bundle.ndjson.gz"}

4. Diagnostics

You can check which profiles are currently loaded into your Aidbox
GET /$zen-get?tag=aidbox/profile
// response
[
"us-core.v1.us-core-diagnosticreport-lab/DiagnosticReport",
"us-core.v1.us-core-ethnicity/Extension",
...
"us-core.v1.us-core-smokingstatus/Observation",
"us-core.v1.pediatric-bmi-for-age/Observation"
]
You can check if there are any errors
GET /$zen-errors
// response if everything is ok
{
"errors": null
}
// response if Aidbox couldn't find zen project
{
"errors": [
{
"message": "No file for ns 'us-core.v1"
}
]
}

5. If eveything is ok, now you have validation enabled. You can test it by creating resource which references profile URL in the meta.profile attribute

POST /Patient
POST /Patient/$validate
POST /Patient
Empty us-core-patient resource POST should return validation errors
POST /fhir/Patient
{
"meta": {
"profile": [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
]
}
}
// response
{
"resourceType": "OperationOutcome",
"text": {
"status": "generated",
"div": "Invalid resource"
},
"issue": [
{
"severity": "fatal",
"code": "invalid",
"expression": [
"Patient.name"
],
"diagnostics": ":name is required (http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient)"
},
{
"severity": "fatal",
"code": "invalid",
"expression": [
"Patient.identifier"
],
"diagnostics": ":identifier is required (http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient)"
},
{
"severity": "fatal",
"code": "invalid",
"expression": [
"Patient.gender"
],
"diagnostics": ":gender is required (http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient)"
}
]
}
POST /Patient/$validate
You can use validation without inserting resource into the database
POST /fhir/Patient/$validate
{
"meta": {
"profile": [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
]
}
}
// response
{
"resourceType": "OperationOutcome",
"text": {
"status": "generated",
"div": "Invalid resource"
},
"issue": [
{
"severity": "fatal",
"code": "invalid",
"expression": [
"Patient.name"
],
"diagnostics": ":name is required (http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient)"
},
{
"severity": "fatal",
"code": "invalid",
"expression": [
"Patient.identifier"
],
"diagnostics": ":identifier is required (http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient)"
},
{
"severity": "fatal",
"code": "invalid",
"expression": [
"Patient.gender"
],
"diagnostics": ":gender is required (http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient)"
}
],
"id": "validationfail"
}