DSP as a FHIR $graphql projection
The cleanest mental model for DSP: it is a shaped projection of FHIR resources
associated with an encounter, plus a small sidecar envelope. FHIR's
$graphql operation is the canonical way to get exactly that.
$graphql
query — no FML inverse engine, no opaque source blob. Writing DSP into FHIR
is a profile projection, optionally driven by the reference FML maps shipped in the
IG download. Together they round-trip every DSP-originated field
through native FHIR slots and dsp-* extensions.
Why $graphql fits DSP almost perfectly
Shape matches
DSP's polymorphic resources[] array is a hand-rolled version
of what GraphQL produces natively: one query, many resource types, nested children.
Projection matches
DSP only wants a subset of FHIR fields. GraphQL lets the client pick precisely those —
no bloat, no _elements gymnastics.
Traversal matches
DSP's parent_reference / child_references
map directly to GraphQL resource-reference resolution. No _include/_revinclude.
Extensions are first-class
extension(url:"...") selectors expose DSP-specific fields
(confidence, spoken forms, transcript turn refs) through the same query.
Conventions used by every canonical query
- Alias every
extension(url:)(e.g.confidence: extension(url: "...dsp-confidence-score") { valueDecimal }) so the response shape stays predictable across server implementations and post-processors. - Treat
extension(url:)as a list. FHIR GraphQL returns each call as an array even when cardinality is 0..1; adapters access the first element (.[0]?.valueDecimal). - One
dsp-transcript-turn-refextension per index. Each entry carries its own version-pinnedDocumentReferencereference plus thevalueIntegerturn index. Repeating extensions are queryable, ordered, and re-transcription-safe (per R1 — Turn-index stability). - Reconstruction precedence. When a value lives in both a native FHIR field and a
dsp-*extension (e.g.verificationStatusvsdsp-assertion-category), the adapter prefers the extension to avoid lossy terminology canonicalization.
A canonical DSP query (R4 + DSP-FHIR IG)
The query is executed at the system level: POST [fhir-base]/$graphql
with the encounter id passed as a GraphQL variable. FHIR's GraphQL binding uses
*List root fields for lists and takes _reference
arguments for reverse-reference searches.
The per-resource field selections (Condition, MedicationRequest, ServiceRequest, …) live on the matching resource mapping pages as the FHIR → DSP tab, so each query stays next to the field-by-field landing table it queries. The shape below is the envelope-level skeleton; substitute the per-resource fragments from each mapping page.
query DSP($encId: ID!) {
encounter: Encounter(id: $encId) {
id
status
class { code display }
period { start end }
subject { resource { ... on Patient { id identifier { system value } name { given family } gender birthDate } } }
participant {
individual { resource { ... on Practitioner { id name { given family } identifier { system value } qualification { code { coding { system code display } } } } } }
}
reasonCode { coding { system code display } }
# DSP envelope extensions (defined by the DSP-FHIR IG)
payloadVersion: extension(url: "https://dsp-fhir.org/StructureDefinition/payload-version") {
major: extension(url: "major") { valueInteger }
minor: extension(url: "minor") { valueInteger }
revision: extension(url: "revision") { valueInteger }
quality: extension(url: "quality") { valueCode }
}
callbackUrl: extension(url: "https://dsp-fhir.org/StructureDefinition/external-callback-url") { valueUrl }
}
# Resource lists — one root field per DSP content_type. The selection set for each list
# is the canonical query published on its mapping page.
conditions: ConditionList(encounter: $encId) { id } # see /mapping/condition
medications: MedicationRequestList(encounter: $encId){ id } # see /mapping/medication
services: ServiceRequestList(encounter: $encId) { id } # see /mapping/lab|imaging|procedure|referral|follow-up|therapy|study
nutrition: NutritionOrderList(encounter: $encId) { id } # see /mapping/dietary
immunizations: ImmunizationList(encounter: $encId) { id } # see /mapping/immunization
devices: DeviceRequestList(encounter: $encId) { id } # see /mapping/device
carePlans: CarePlanList(encounter: $encId) { id } # see /mapping/activity
compositions: CompositionList(encounter: $encId) { id } # see /mapping/document-section
transcripts: DocumentReferenceList(encounter: $encId, category: "https://dsp-fhir.org/CodeSystem/doc-category|transcript") {
id status type { coding { code display } }
content { attachment { url contentType language } }
context { period { start end } }
speakerCount: extension(url: "https://dsp-fhir.org/StructureDefinition/speaker-count") { valuePositiveInt }
}
} What the response looks like
One HTTP call, one JSON document, grouped by resource type — the same polymorphic
grouping DSP's resources[] uses. Per-resource field landings
(which native FHIR field or dsp-* extension a DSP value lives in)
are documented on each mapping page's field-by-field table.
Fallbacks (when $graphql isn't available)
Before going to REST _include, try
Encounter/$everything — one call, widely implemented,
returns a Bundle of the encounter and its referenced resources:
GET [fhir-base]/Encounter/{id}/$everything
It's coarser than the canonical $graphql query (no
field shaping, no filter on DocumentReference category) but it beats six round-trips.
If even $everything isn't available:
REST _include fan-out
$graphql is an optional FHIR operation. Many production servers don't
expose it. The DSP-FHIR IG defines two layered fallbacks, in preference order.
Prior art for the whole "canonical projection" pattern is IPS
Patient/$summary
— DSP's canonical query is morally the same shape scoped to an encounter.
GET [fhir-base]/Encounter/{id}?_include=Encounter:subject
&_include=Encounter:participant
&_include=Encounter:location
GET [fhir-base]/Condition?encounter={id}
GET [fhir-base]/MedicationRequest?encounter={id}
GET [fhir-base]/ServiceRequest?encounter={id}
GET [fhir-base]/Composition?encounter={id}&_include=Composition:section:entry
GET [fhir-base]/DocumentReference?encounter={id}
&category=https://dsp-fhir.org/CodeSystem/doc-category|transcript Six REST calls versus one GraphQL call. The IG publishes both shapes so implementers pick based on their server. DSP's projection shape is the same either way.
What $graphql does not solve on its own
| DSP concept | Why GraphQL alone is insufficient | Remedy |
|---|---|---|
| Write path (partner → Dragon updates) | R4 $graphql is read-only in practice. | Use REST transaction Bundle or resource-level PUT/POST for writes. Keep $graphql as the read contract only. The IG ships reference FML maps that describe a write projection, but partners may hand-write the equivalent. |
payload_version (major/minor/revision/quality) | Not a resource element. | DSP extension on Encounter or Bundle.meta; surface via GraphQL extension() selector — see mapping catalog → Envelope. |
update_status: NEW / UPDATED / DELETED | FHIR uses meta.versionId + history, not an in-line enum. | Derive from meta.versionId + Provenance.activity; or pin to a DSP extension if producer-driven. |
| Push delivery of DSP | GraphQL is pull. | SubscriptionTopic (R4B/R5) with a topic bound to "encounter.dsp-ready"; payload URL fires the canonical query. |
$graphql is an optional
operation and has been draft/trial-use through
R4 and R5. Many production servers don't implement it. Treat the GraphQL query here as the
preferred read shape for DSP — not the only one. The REST fallback
above is required-level in the IG.
$graphql + a handful
of DSP extensions can speak DSP natively — no parallel schema required, no FML engine
required for the read path.