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.

Read vs. write split. The IG separates the two directions. Reading DSP from FHIR is a canonical $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-ref extension per index. Each entry carries its own version-pinned DocumentReference reference plus the valueInteger turn 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. verificationStatus vs dsp-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 conceptWhy GraphQL alone is insufficientRemedy
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.
Spec status caveat. FHIR $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.
Bottom line. Treat the canonical DSP GraphQL query as a published artifact of the DSP-FHIR IG. Versioning the query versions DSP. Any partner with an R4 server that implements $graphql + a handful of DSP extensions can speak DSP natively — no parallel schema required, no FML engine required for the read path.