Skip to content

FHIR for DHIS2 people

Who this is for: anyone who knows DHIS2 - data elements, organisation units, option sets, tracker - and has never touched FHIR.

Before you start: nothing to install; this page is pure reading.

You will be able to:

  • name the handful of FHIR resources the rest of this series keeps using, in DHIS2 terms
  • read generated FSH and FHIR JSON without treating it as noise
  • follow any 201/301/401 page in this series without stopping to look a term up

Depth lives in the linked specification pages; this page says what each term means, what it is closest to in DHIS2, and what d2w fhir does with it. Read it once and the rest of the series reads as a guide rather than as a vocabulary test. When you only need one term settled, the glossary is the quick lookup; this page is the tour that makes the terms stick.

The shape of FHIR

Resources, elements, and R4

FHIR is a health-data exchange standard built out of resources: around 145 named document shapes - Patient, Organization, Location, Questionnaire, CodeSystem - each with a fixed set of typed elements, served over REST at /<ResourceType>/<id> and travelling as JSON. A resource type is close to a DHIS2 metadata class and a resource is close to one object of it, with the same REST-and-JSON feel as /api/dataSets/<uid>. R4 is FHIR Release 4, the widely deployed version, and the one this toolkit targets throughout - the models live in dhis2w_fhir/r4/, and the scaffolded project publishes JSON only, because XML and Turtle would add a file and a rendered page per resource for content everything reads as JSON anyway.

Further reading: FHIR R4, the resource list.

id versus identifier

Every resource has an id: the token it is served under, unique within one server and meaningless outside it. Separately, most resources carry identifier - a repeating element of {system, value} pairs holding the business identifiers the object is known by elsewhere. The distinction is exactly DHIS2's UID versus code, with one addition: FHIR insists the system travels with the value, because "HIV001" alone is ambiguous and http://dhis2.org/fhir/id/data-set-code + "HIV001" is not.

This toolkit exposes both DHIS2 identifiers wherever FHIR gives it a slot - {base}/id/<kind> for the UID, {base}/id/<kind>-code for the DHIS2 code - and uses the bare UID as the resource id, so Location/ImspTQPwCqd reads straight back to the instance.

Further reading: Resource.id, the Identifier datatype.

References, including the ones that do not resolve

One resource points at another through a Reference. The usual spelling is a literal reference - "reference": "Location/ImspTQPwCqd" - which a client can fetch. But a Reference may instead carry identifier and no reference at all: a logical reference, naming the thing unambiguously by business identifier while admitting this server holds no such resource. DHIS2 has no real analogy, because DHIS2 objects always live in one database; the closest feeling is a UID pointing at an object in a different instance.

The toolkit uses both. An aggregate or event response's subject is a literal Reference(Location/<uid>) into the published registry. A tracker event's is a logical reference - subject.type = "Patient", subject.identifier.system fixed to {base}/id/tracked-entity, no reference element - because the published guide is a document about forms and terminology, and it carries no people. The person lives in DHIS2, and a logical reference names them without pretending the guide holds them.

A running d2w fhir serve is the other case: it does answer GET /Patient, serving the instance's tracked entities as a register beside the guide's artifacts. The reference stays logical either way, because what it names is a DHIS2 tracked entity, not a resource the guide published.

Further reading: References between resources.

Canonical URLs

Definitional resources - profiles, extensions, CodeSystems, ValueSets, Questionnaires - each carry a url, their canonical URL. It is a globally unique name, not an address: nothing has to be reachable at http://dhis2.org/fhir/CodeSystem/x. It identifies the artifact so two guides cannot collide and a binding can name one exactly. The DHIS2 analogy is a UID that happens to look like a URL - an opaque unique handle - except that a URL is unique across every FHIR guide on earth, which a UID is not.

Every IG picks a canonical base and hangs its artifacts off it. Here that is [ig] canonical in fhir.toml, so a generated Questionnaire's url is <canonical>/Questionnaire/<stem>, where the stem is the identity stem [generate.naming] source resolves - the DHIS2 id by default. The separate identifier_system_base (default http://dhis2.org/fhir) is the base for the DHIS2 identifier systems rather than for artifacts; the two are configured independently.

Further reading: Canonical URLs.

What an Implementation Guide is

Raw FHIR is deliberately under-specified: it says what a Questionnaire may contain, not what your programme's forms look like. An Implementation Guide is the published document that closes that gap - profiles, extensions, terminology, examples, and prose, compiled into a website and a downloadable package, saying "here is how FHIR is used here". DHIS2 has no equivalent artifact; the nearest thing is a metadata package plus its documentation.

That is why this toolkit generates one. d2w fhir generate reads a DHIS2 instance and writes IG source; the toolchain compiles it into a guide a third party can read without access to your repository, your DHIS2 metadata API, or you.

Further reading: ImplementationGuide.

Terminology

CodeSystem

A CodeSystem defines codes: it is where concepts are declared, each with a code, a display, and optional extras. The DHIS2 analogy is exact - an option set is the source of its option codes, and nothing else may mint them.

This toolkit emits one CodeSystem per DHIS2 option set, one per DHIS2 category, and one per attribute category combo, plus a fixed set of shared ones: D2DE_CS over every data element the generated forms reference, D2TEA_CS over every tracked entity attribute they ask for, D2COC_CS over every category option combo, D2TET_CS over the tracked entity types, D2OU_Level_CS over the organisation-unit levels, D2PeriodType_CS, and D2FormType_CS. A level concept is coded level-<n> and displayed under the name the instance gives that depth in /api/organisationUnitLevels, falling back to Level <n> where it names none. By default the concept code is the DHIS2 UID; concept_code_source = "code" swaps in the DHIS2 code.

D2FormType_CS names the five kinds of DHIS2 form the generator recognises: #aggregate for a data set, #event for an event program, #tracker for a tracker program's registration form, #tracker-event for one of its stages, and #tracked-entity for a tracked entity type's own registration form.

Further reading: CodeSystem.

ValueSet

A ValueSet selects codes: it defines nothing, it composes a selection out of one or more CodeSystems, and it is a ValueSet - never a CodeSystem - that an element is bound to. DHIS2 collapses the two, because pointing a data element at an option set both defines and selects. FHIR splits the roles so one field can draw from several code systems, or from part of one.

Every CodeSystem this toolkit writes ships with a matching ValueSet, and that is what a question binds to: an option-set-bound data element becomes an item of type = #choice with answerValueSet = Canonical(D2OS_<UID>_VS).

Further reading: ValueSet, terminology bindings.

Coding and CodeableConcept

A Coding is one coded value: a system (the CodeSystem's canonical URL), a code, and usually a display. A CodeableConcept wraps zero or more Codings plus free text, for when a concept may be expressed in several vocabularies at once. The DHIS2 lesson is the one identifiers already taught: an option code is only unique inside its option set, so FHIR carries the system beside the code everywhere. In a generated example response, an answer to an option-set-bound question is a valueCoding naming a concept in that set's own CodeSystem - the very concept code the terminology target wrote, fall-backs included.

Further reading: Using codes in resources.

Concept properties and designations

A CodeSystem concept carries two kinds of extra. A property is a typed named fact, declared once on the CodeSystem and valued per concept. A designation is an alternate rendering of the concept's display - which is where translations go. DHIS2 has both shapes: the option's other identifier is property-shaped, and translations are designation-shaped.

The toolkit uses properties for the complementary DHIS2 identifier on every concept (dhis2-code in id mode, dhis2-id in code mode), plus level and parent on the organisation-unit terminology and domain on D2DE_CS; each gets a <identifier_system_base>/property/<code> URI so it means something outside this guide. DHIS2 NAME translations become designations where the target is a concept, and the standard translation extension where it is a title, a name, or the text of a question. See Translations for both shapes side by side.

Further reading: concept properties.

ConceptMap

A ConceptMap states a relationship between concepts in two different code systems - "this code over here means that code over there". It is the FHIR-native place for a crosswalk, and a terminology server can answer $translate from one.

This toolkit publishes one ConceptMap per option set, one per category, and one per attribute category combo. Each maps every generated concept code back onto the DHIS2 UID and, where the member carries one, the DHIS2 code - one group per identifier system. An option-set map answers "which DHIS2 option is this answer?"; a category map answers "which DHIS2 category option is this disaggregation?".

One more map points the other way. D2TET_CM maps each DHIS2 tracked entity type onto the FHIR resource type its registrations are published as, so a client can ask what a "Person" is here rather than assuming; a type the project maps to nothing is a Patient. d2w fhir serve answers $translate over all of them, so a client holding a generated coding asks the server what it stands for instead of reading concept properties itself.

Further reading: ConceptMap, and Terminology and ConceptMaps for the emitted shape and the $translate calls.

Constraining and extending

Profiles

A profile is a StructureDefinition that narrows a base resource: it raises minimum cardinalities, forbids elements, fixes values, restricts which resource types a Reference may point at, and binds a coded element to a ValueSet. It never invents elements - it only says which of the base resource's elements are acceptable here.

The honest DHIS2 analogy is the constraint layer DHIS2 already puts over a generic form: compulsoryDataElementOperands make a question mandatory, a valueType restricts what may be entered, an option set restricts it further. None of those change what a data value is; they say which data values are acceptable.

This toolkit generates D2Organization and D2Location over the registry, and D2AggregateResponse / D2EventResponse / D2TrackerRegistrationResponse / D2TrackerEventResponse / D2TrackedEntityResponse over QuestionnaireResponse - the capture contract. Each response profile pins the extensions its form kind must carry, requires questionnaire and subject, and restricts what subject may point at.

Further reading: Profiling FHIR, StructureDefinition.

Extensions

Because a profile cannot add elements, FHIR carries local data in extensions: a {url, value[x]} pair hung off any element, where the url is the canonical URL of a StructureDefinition defining it. A complex extension nests sub-extensions instead of carrying a single value, and every extension declares a context - the element types it may legally hang off.

The DHIS2 analogy is excellent, because DHIS2 solved the same problem the same way: attributeValues is exactly an extension mechanism. FHIR prefers extensions over custom fields for the reason DHIS2 prefers attributes over forking the schema - a consumer that does not recognise the URL skips it and still reads the rest.

d2w fhir defines nineteen of its own, all in foundation/. Each one exists because a DHIS2 fact has no FHIR element to sit in:

Extension The DHIS2 fact it carries
D2Period, D2PeriodType The reporting period a response covers, and the reporting frequency the data set collects at.
D2FormType Which kind of DHIS2 form this is.
D2OrganisationUnit The organisation unit an event was captured at, as a reference to its Location.
D2OrganisationUnitAssignment Which organisation units may report this form, as a reference to a List of their Locations.
D2OrganisationUnitLevel The hierarchy level a place sits at.
D2AttributeOptionCombo, D2AttributeOptionCombos The attribute option combo an aggregate response's values are keyed under, and the set a form allows.
D2AttributeValue One DHIS2 attribute value on any object.
D2TrackedEntityAttributeValue One tracked entity attribute value on a registration response.
D2TrackerEnrollment, D2EnrolledAt, D2IncidentAt, D2CollectsIncidentDate The enrollment a response belongs to, its enrollment and incident dates, and whether the program collects an incident date at all.
D2EntityLevel Whether a registration answer belongs to the tracked entity or only to the enrollment.
D2SubjectExists Whether the person a registration names is already stored by the instance, so the response enrols rather than creates.
D2Repeatable Whether one enrollment may capture this program stage more than once.
D2Description, D2DateLabels The instance's own guidance text on a question or section, and the words it puts on enrollment, incident, and event dates.

D2Period is the clearest example of why the mechanism is needed. A FHIR Period is a pair of instants; a DHIS2 period is a typed interval - 202401 is the January instance of Monthly, and the type is what makes it comparable and round-trippable.

Further reading: Extensibility.

Slicing, in one breath

A repeating element carries a list, and slicing is how a profile names positions in that list so it can constrain them individually - slice identifier on system and you can say "the slice whose system is {base}/id/org-unit is mandatory". Extensions are pre-sliced by their URL, which is why a profile writes extension[D2Period] 1..1 and an instance addresses it by name. The toolkit slices identifier into dhis2id 1..1 and dhis2code 1..1 on both organisation-unit profiles, and slices the extensions each response profile requires - which is what lets an example write * extension[D2Period].extension[iso].valueString = "202607".

Further reading: Slicing.

The capture pair

Questionnaire

A Questionnaire is a form definition: a nested tree of items, each with a linkId, a text, and a type (group, string, integer, choice, date, ...). It is the closest thing in FHIR to a DHIS2 data set or program stage data entry form.

d2w fhir generate questionnaires writes one per aggregate data set, one per event program, one per tracker program (the registration that enrols a person), one per stage of a tracker program, and one per tracked entity type (registering the entity with no programme attached). Sections become #group items, data elements become questions typed from their DHIS2 valueType, and a non-default category combo on a data set turns a question into a group with one child per category option combo. Which kind a form is shows up twice: as Questionnaire.code and on the D2FormType extension its responses carry.

Further reading: Questionnaire.

linkId and subjectType

Each item carries a linkId, a string unique within the form; an answer refers back to its question by repeating it, so it is the join key between a form and every submission against it. The toolkit uses DHIS2 UIDs, which is what makes a response readable back into DHIS2 without consulting the form: a section's linkId is the section UID, a question's is the data element UID, and a disaggregated cell's is <deUid>.<cocUid> - exactly the (dataElement, categoryOptionCombo) key a DHIS2 data value carries.

Questionnaire.subjectType states what kind of resource the form is answered for. A DHIS2 data set and event program declare #Location, because a DHIS2 form is answered for an organisation unit; a tracker form declares whatever the program's tracked entity type is, because it is answered for the enrolled entity - and the organisation unit moves onto the response's D2OrganisationUnit extension instead. That is #Patient unless the project says otherwise: a DHIS2 tracked entity type is not always a person, and a project tracking herds or water points maps its types onto #Group or #Location in fhir.toml.

QuestionnaireResponse

A QuestionnaireResponse is one submission: it points at a Questionnaire through questionnaire, names its subject, carries a status, and answers item by item on the same linkIds. The DHIS2 analogy is one data value set for a given (organisation unit, period, attribute option combo), or one event.

d2w fhir generate examples writes one per form so an implementer can see what an answer looks like, and d2w fhir serve receives them - POST /QuestionnaireResponse is the one write the facade accepts.

Further reading: QuestionnaireResponse.

The operational resources

CapabilityStatement

A CapabilityStatement declares what a FHIR server supports: which resource types, which interactions (read, search-type, create), which profiles it accepts. Every FHIR server answers one at GET /metadata. DHIS2 has no equivalent; the nearest thing is /api/schemas plus prose. Two appear here: the IG publishes D2CaptureServer with kind = #requirements - what a conforming server must support - and a running d2w fhir serve answers /metadata with a kind = #instance statement instantiating it, narrowed to the types that store actually holds.

Further reading: CapabilityStatement.

Bundle

A Bundle is a container of resources with a type saying what the container means. A search never returns a bare list: it returns a Bundle of type = "searchset", with a total, a self link, and one entry per match. d2w fhir serve answers every search that way, and its self link echoes back only the parameters the server actually applied, so a client can see what it got.

Further reading: Bundle.

OperationOutcome

An OperationOutcome is FHIR's structured error - and structured acknowledgement. It carries a list of issues, each with a severity, a code, optional diagnostics prose, and an expression locating the problem as a FHIRPath into the submitted document. The capture facade answers with one on every path: an accepted capture gets an information issue naming the receipt, a refused one gets errors that name the offending linkId through expression.

Further reading: OperationOutcome.

NamingSystem

A NamingSystem declares that an identifier namespace exists and says what identifiers under it mean. It registers nothing with anybody - it is the guide stating its own convention, so a validator meeting {base}/id/org-unit has a definition to resolve instead of warning on every artifact carrying one.

foundation/d2-naming-systems.fsh emits twenty-six, one per DHIS2 identifier system: a UID and a code system each for the organisation unit, option set, option, category, category option, category combo, category option combo, data set, program, program stage, data element, and tracked entity type, plus a UID system alone for the tracked entity and the tracker enrollment - DHIS2 gives those two no code attribute, so there is no code system to declare.

Further reading: NamingSystem.

FSH and the toolchain

FSH, the FHIR Shorthand language

FSH (pronounced "fish") is a compact domain-specific language for authoring FHIR artifacts. Writing a profile as raw JSON means hand-building a StructureDefinition element by element; FSH is a few lines of text that compile to the same thing. Every line inside a definition starts with * and names a path. Here is a trimmed generated Questionnaire, one of the shapes d2w fhir generate writes:

Instance: Questionnaire-BfMAe6Itzgt      // the FSH-level name of this artifact
InstanceOf: Questionnaire                // which resource type it is
Title: "Questionnaire - Child Health"
Usage: #definition                       // a definitional artifact, not an example
* id = "BfMAe6Itzgt"                     // the bare DHIS2 UID
* extension[D2FormType].valueCode = #aggregate      // it came from a data set
* extension[D2PeriodType].valueCode = #Monthly      // reported monthly
* url = "https://example.org/fhir/Questionnaire/BfMAe6Itzgt"
* identifier[+].system = $DHIS2-DS       // $-prefixed names are FSH aliases
* identifier[=].value = "BfMAe6Itzgt"    // [+] opens a new slot, [=] stays in it
* identifier[+].system = $DHIS2-DS-CODE
* identifier[=].value = "DS_359711"
* name = "D2DS_BfMAe6Itzgt"
* title = "Child Health"
* status = #draft                        // # marks a code, not a string
* subjectType = #Location                // answered for an organisation unit
* code = D2FormType_CS#aggregate
* item[+].linkId = "Y2rk0vzgvAx"         // a DHIS2 section
* item[=].text = "Immunization"
* item[=].type = #group
* item[=].item[+].linkId = "s46m5MS0hxu" // a data element inside it
* item[=].item[=].code = D2DE_CS#s46m5MS0hxu "BCG doses given"
* item[=].item[=].text = "BCG doses given"
* item[=].item[=].type = #group          // a group, because it disaggregates
* item[=].item[=].item[+].linkId = "s46m5MS0hxu.Prlt0C1RF0s"   // one cell
* item[=].item[=].item[=].code = D2COC_CS#Prlt0C1RF0s "Fixed, <1y"

A data element that does not disaggregate is a question directly, typed from its DHIS2 valueType - #integer, #string, #date, #choice. This one carries a category combo, so it becomes a group with one child per category option combo, and each child's linkId is the <dataElement>.<categoryOptionCombo> pair a DHIS2 data value is keyed by.

Four pieces of syntax carry most of the weight. #code is a code rather than a string. $NAME is an alias, declared once and expanded everywhere - here to a DHIS2 identifier system URL. [+] appends a new entry to a repeating element and [=] continues addressing the one just opened, which is how a nested tree is written as a flat list of paths. And the keyword on line one picks the artifact kind: Instance: for a concrete resource, Profile:, Extension:, CodeSystem:, ValueSet: for definitions.

Further reading: FHIR Shorthand, FSH School.

SUSHI and the IG Publisher

Two tools turn that text into a guide, and the scaffolded project runs both through Docker. SUSHI compiles FSH to FHIR JSON, and running it on its own is the fast gate - it says whether the FSH is valid without publishing anything. The HL7 IG Publisher turns the JSON into the website, validating every resource, rendering a page per artifact, and building the downloadable package. The publisher runs its own SUSHI over the same FSH, so a chain that calls SUSHI and then the publisher compiles everything twice - iterate on SUSHI alone and run the publisher when you are ready to publish.

Further reading: SUSHI, IG Publisher.

fsh-generated/ versus input/resources/

FSH is not the only way into an IG. Anything already in FHIR JSON can be dropped into input/resources/ as a predefined resource: SUSHI loads it verbatim, with no FSH parse and no conversion pass. Artifacts SUSHI compiles from FSH land in fsh-generated/resources/ instead.

That split is a performance decision here, not a stylistic one. The organisation-unit registry and the option-set and category terminology are the largest things in a DHIS2-derived IG and are pure data - so d2w fhir generate writes them as pre-built JSON under ig/input/resources/, keeping them out of the compile entirely, while the forms, profiles, and extensions stay FSH under ig/input/fsh/.

How DHIS2 maps onto FHIR here

This is the table to keep open while reading the rest of the series. Every row is something d2w fhir generate actually emits, and the last column says where to look at it - a path on a running d2w fhir serve, or the page the compiled guide renders it on. Paths use the ids from a real run; <uid> is the DHIS2 UID.

Metadata that becomes vocabulary

DHIS2 object What it becomes Where to see it
Option set A CodeSystem defining its options, plus a ValueSet selecting them GET /CodeSystem/d2-os-<uid>-cs, and the guide's Terminology page
Option A concept inside that CodeSystem - never an artifact of its own Inside its option set's CodeSystem
Category, with its category options The same CodeSystem + ValueSet pair GET /CodeSystem/d2-cat-<uid>-cs
Attribute category combo A CodeSystem + ValueSet + ConceptMap over its attribute option combinations GET /CodeSystem/d2-aoc-<uid>-cs
Data element a form asks for A concept in D2DE_CS GET /CodeSystem/d2-de-cs
Tracked entity attribute a form asks for A concept in D2TEA_CS GET /CodeSystem/d2-tea-cs
Category option combo A concept in D2COC_CS GET /CodeSystem/d2-coc-cs
Tracked entity type A concept in D2TET_CS, and a D2TET_CM entry naming the FHIR resource type it publishes as GET /CodeSystem/d2-tet-cs, GET /ConceptMap/d2-tet-cm
Organisation unit level A concept in D2OU_Level_CS, bound to Organization.type, displayed under the instance's own name for that depth GET /CodeSystem/d2-ou-level-cs
Period type A concept in D2PeriodType_CS GET /CodeSystem/d2-period-type-cs
Every identifier convention above One NamingSystem declaration each - twenty-six of them The guide's artifact index

The organisation unit hierarchy

DHIS2 object What it becomes Where to see it
Organisation unit An Organization and a Location - the same unit twice, because FHIR splits the responsible body from the place it sits at, and DHIS2 uses one object for both GET /Organization/<uid> and GET /Location/<uid>
Parent organisation unit Organization.partOf and Location.partOf On each resource
Organisation unit geometry Location.position for a point, plus the location-boundary-geojson extension for a boundary On the Location
The organisation units a form may be reported by A List of their Locations, referenced from the form's D2OrganisationUnitAssignment GET /List/d2-ds-<uid>-org-units

Forms and what is captured against them

DHIS2 object What it becomes Where to see it
Aggregate data set One Questionnaire, subjectType = #Location, code = #aggregate GET /Questionnaire/<uid>
Event program One Questionnaire, subjectType = #Location, code = #event GET /Questionnaire/<uid>
Tracker program One Questionnaire for the registration that enrols a person, code = #tracker GET /Questionnaire/<uid>
Tracker program stage One Questionnaire per stage, code = #tracker-event, subjectType the program's tracked entity type GET /Questionnaire/<stageUid>
Tracked entity type One Questionnaire registering the entity with no programme attached, code = #tracked-entity GET /Questionnaire/<uid>
Section An item of type = #group Inside the Questionnaire
Data element on a form A child item, linkId the data element UID, typed from its DHIS2 valueType Inside the Questionnaire
Disaggregated data element A group with one child per category option combo, linkId <deUid>.<cocUid> Inside the Questionnaire
Data values for one (organisation unit, period, attribute option combo) One QuestionnaireResponse GET /QuestionnaireResponse
Event One QuestionnaireResponse GET /QuestionnaireResponse
Tracked entity, registered One QuestionnaireResponse against the registration form GET /QuestionnaireResponse
Tracked entity, as a subject A logical subject - subject.identifier, no reference. A running serve also publishes the instance's tracked entities as a register GET /Patient on a running serve

DHIS2 facts that ride along as extensions

DHIS2 object What it becomes Where to see it
Reporting period The D2Period extension - ISO identifier, type, and resolved date range On every aggregate response
Which kind of form this is Questionnaire.code and the D2FormType extension On the form and its responses
Attribute value One D2AttributeValue extension On the object carrying it
Tracked entity attribute value One D2TrackedEntityAttributeValue extension, unless DHIS2 declares the attribute unique - then it is an identifier On a registration response
Enrollment The D2TrackerEnrollment extension, as a valueIdentifier On a tracker response
Organisation unit of a tracker event The D2OrganisationUnit extension On a tracker event response
UID and code Two identifier slices: {base}/id/<kind> and {base}/id/<kind>-code On every generated instance
NAME translation A concept designation, or the standard translation extension on _title / _name On the concept or the artifact
FORM_NAME translation The standard translation extension on a question's _text On the Questionnaire item

Further reading

See also

Next: Quickstart: six commands to a served IG