FHIR Profiling

FHIR Profiling

FHIR profiling is how you turn “base FHIR” into a precise, testable contract: which elements are required, which codes are allowed, how repeating elements are structured, and which extensions are part of the agreement. In most real-world integrations, profiles and implementation guides (IGs) are the difference between “valid FHIR” and “interoperable FHIR”.

First-time reader? Read Why Base FHIR Isn’t Enough → Quick Reference → Profiles (with examples) → Anti-Patterns. Implementing a profile? Read everything, especially Interoperability Considerations and Testing Strategy.

Quick reference:

  • Cardinality: min/max (makes things required/optional/forbidden)
  • Must Support: capability flag—systems must be able to handle this element (not always “required in every instance”)
  • Slicing: structure repeating arrays (e.g., MRN vs national ID in identifier[])
  • Binding: attach value sets to coded elements (controls which codes are allowed)
  • Profile: one StructureDefinition that constrains a resource
  • IG: the package containing profiles, extensions, value sets, documentation, and versioning

Need a companion concept? Extensions add new data elements; profiling constrains and standardizes them. See FHIR Extensions and FHIR Terminology.

Why Base FHIR Isn’t Enough

Base FHIR is intentionally broad:

  • Many elements are optional so the same resource can work across countries, care settings, and workflows.
  • The same concept can be represented in multiple valid ways (different code systems, different reference patterns, different levels of detail).
  • “Valid JSON” is not the same as “useful clinical meaning” or “queryable data”.

Profiling is how communities and programs create shared expectations, such as:

  • “We always require Patient.identifier and Patient.name.”
  • Observation.code must come from this value set, and status must be one of these codes.”
  • Patient.identifier is sliced into MRN vs national identifier by system.”
  • “This extension is part of the contract and must be supported.”

Profiles

A profile is a single StructureDefinition that constrains one base resource or data type (e.g., “Patient”, “Observation”, “Identifier”). Profiles can:

  • Make elements required/optional (cardinality)
  • Restrict datatypes (including choice types like value[x])
  • Fix or pattern-match values (e.g., system, code, status)
  • Slice repeating elements (arrays) into named parts
  • Bind elements to value sets
  • Require and constrain extensions
  • Add invariants (FHIRPath expressions)

Profile vs IG: A profile is one artifact. An Implementation Guide (IG) is the package that bundles profiles, extensions, value sets, documentation, examples, and versioning together. When you “implement US Core,” you’re implementing dozens of profiles from the US Core IG.

In most ecosystems, “implement FHIR” really means “implement these profiles from these IGs”.

Constraints: Cardinality, Must Support, Fixed Values

Profiles apply constraints via ElementDefinition entries. The most common ones show up over and over:

ConstraintWhere you see itWhat it doesTypical use
Cardinalitymin / maxRequires or forbids dataMake identifiers mandatory; forbid fields you don’t support
Must SupportmustSupport: trueFlags required capability (not always required presence)Data elements implementers must handle
Fixed valuefixed[x]Exact valueForce resourceType, identifier.system, meta.profile patterns
Patternpattern[x]Must match a pattern (often partial)Require a Coding.system; allow any Coding.code in that system
Invariantconstraint.expression (FHIRPath)Rule beyond simple min/maxCross-field rules (e.g., if A then B)

Example: a profile requiring at least one identifier, and flagging it as “must support”:

{
  "id": "Patient.identifier",
  "path": "Patient.identifier",
  "min": 1,
  "mustSupport": true
}

Practical notes:

  • mustSupport is not “must be present in every instance”. It usually means “systems claiming conformance need to be able to send/receive/process this element” according to the IG’s rules.
  • Prefer pattern[x] for “must have this system” or “must have this code system”, and fixed[x] when the value must be exact.

Slicing and Discriminator Choices

Slicing is how profiles impose structure on repeating elements. In plain language: slicing says “this identifier[] array must have an MRN at position X and a national ID at position Y”—discriminators tell you how to identify which is which (usually by matching a field value like system).

It’s most commonly used on:

  • identifier[] (MRN vs national ID vs payer member ID)
  • telecom[] (phone vs email, different uses)
  • coding[] (multiple codings, but with rules)
  • extension[] (slicing by url)

A slice is identified by one or more discriminators. Common discriminator types include:

Discriminator typeRough meaningExample path
valueMatch by exact valuesystem
patternMatch by patterntype or a coding pattern
typeMatch by datatypevalue[x]
existsMatch by presencevalue
profileMatch by profile conformancereference

Example: slicing Patient.identifier into a required “mrn” slice based on system:

[
  {
    "id": "Patient.identifier",
    "path": "Patient.identifier",
    "slicing": {
      "discriminator": [{ "type": "value", "path": "system" }],
      "rules": "open"
    }
  },
  {
    "id": "Patient.identifier:mrn",
    "path": "Patient.identifier",
    "sliceName": "mrn",
    "min": 1,
    "max": "1",
    "patternIdentifier": {
      "system": "http://hospital.example.org/mrn"
    }
  }
]

Reading tip: element IDs like Patient.identifier:mrn indicate you’re looking at a slice (:mrn) of a repeating element (identifier).

Terminology Bindings

Terminology bindings are how profiles make data computable and comparable: they attach a value set to an element like CodeableConcept, Coding, or code.

Binding strength is a key part of conformance:

StrengthWhat senders should doWhat receivers should do
requiredUse only codes in the value setReject codes outside the value set
extensibleUse value set if possible; otherwise send a code outside itAccept outside codes, but prefer value set codes
preferredUse value set codes when you canAccept anything; use value set for guidance
exampleNo conformance impactNo conformance impact

Example: binding a vital-signs code element to a value set:

{
  "id": "Observation.code",
  "path": "Observation.code",
  "binding": {
    "strength": "required",
    "valueSet": "https://example.org/fhir/ValueSet/vital-signs"
  }
}

Terminology is usually where “it validates locally but fails in production” problems come from. If you’re implementing a profile, treat terminology services and caching as first-class concerns. See FHIR Terminology.

Extensions (When and How)

Profiles don’t just “allow extensions”—they define which extensions are part of the contract and where they may appear.

The typical pattern is:

  1. Publish the extension definition (StructureDefinition type Extension) with a stable url.
  2. In the profile, slice extension[] by url.
  3. Constrain the slice to the extension definition and set cardinality / must support.

Example: slicing Patient.extension by url and constraining the slice to a specific extension definition:

[
  {
    "id": "Patient.extension",
    "path": "Patient.extension",
    "slicing": {
      "discriminator": [{ "type": "value", "path": "url" }],
      "rules": "open"
    }
  },
  {
    "id": "Patient.extension:race",
    "path": "Patient.extension",
    "sliceName": "race",
    "min": 0,
    "max": "1",
    "mustSupport": true,
    "type": [
      {
        "code": "Extension",
        "profile": [
          "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"
        ]
      }
    ]
  }
]

For deeper extension design guidance (including modifier extensions), see FHIR Extensions.

Implementation guides

An Implementation Guide (IG) is the packaging and publishing mechanism that turns profiles into something you can adopt consistently. IGs provide:

  • Machine-readable artifacts (profiles, extensions, value sets, etc.)
  • Human-readable documentation (narratives, guidance, examples)
  • Versioning and dependencies (so ecosystems can evolve)

Package Contents (Artifacts and Narratives)

Most IG packages include a mix of:

  • StructureDefinition (profiles and extensions)
  • ValueSet, CodeSystem, ConceptMap
  • SearchParameter, OperationDefinition
  • CapabilityStatement (conformance expectations, sometimes tests)
  • Example instances (often critical for interpretation)
  • Narrative pages explaining “what do we actually mean by must support?”

If you’re implementing an IG, plan for both sides: load the artifacts into validators and read the narratives to interpret the intent.

Versioning and Dependency Graph

IGs form a dependency graph:

  • An IG depends on a specific FHIR version (R4 vs R5 matters).
  • IGs may depend on other IGs (e.g., national base IGs, terminology packages).
  • Your implementation effectively pins to a set of package versions—even when you don’t realize it.

Operational guidance:

  • Pin versions of IG packages in tooling and CI; avoid “latest” in production validation.
  • Validate with the full dependency set loaded (profiles + extensions + value sets).
  • Treat upgrades as migrations: run validation and integration tests against representative data before switching.

R4 vs R5 Differences

The profiling mechanism is fundamentally the same in R4 and R5, but there are notable improvements:

  • R5 adds more precise slicing discriminator types (especially for extensions and references)
  • R5 improves invariant capabilities with enhanced FHIRPath functions
  • R5 provides better tooling for managing large snapshots
  • Profiles authored for R4 need to be regenerated for R5 (different base resource structures)

If you’re targeting both R4 and R5 systems, maintain separate IG versions but align design patterns where possible. Many IGs (including US Core) publish parallel R4 and R5 versions.

Reading a profile

Profiles are easiest to understand when you treat them like code: start with the summary, then inspect the diffs and the “hot spots”.

A practical way to read StructureDefinition

  1. Identify the base: baseDefinition tells you what it constrains (e.g., http://hl7.org/fhir/StructureDefinition/Patient).
  2. Look for the diff: differential.element[] is usually the “what changed” view.
  3. Use snapshot for full context: snapshot.element[] includes inherited elements and is what validators actually use.
  4. Scan for slicing: find elements with slicing and then inspect each slice (:sliceName).
  5. Scan for bindings: elements with binding often drive the hardest validation issues.
  6. Scan for invariants: constraint.expression (FHIRPath) is where cross-field rules live.

Differential vs snapshot

  • Differential: “only the constraints we added/changed.”
  • Snapshot: “the complete structure after applying all parent constraints.”

Tooling often generates snapshots automatically; humans usually author differentials (or author in higher-level languages like FSH and compile to differentials).

Example: A profile that makes Patient.identifier required and adds a race extension.

Differential (what you author):

{
  "differential": {
    "element": [
      {
        "id": "Patient.identifier",
        "path": "Patient.identifier",
        "min": 1
      },
      {
        "id": "Patient.extension:race",
        "path": "Patient.extension",
        "sliceName": "race",
        "min": 0,
        "max": "1"
      }
    ]
  }
}

Snapshot (what validators use—includes all inherited elements from base Patient):

{
  "snapshot": {
    "element": [
      { "id": "Patient", "path": "Patient", "min": 0, "max": "*" },
      { "id": "Patient.id", "path": "Patient.id", "min": 0, "max": "1" },
      { "id": "Patient.meta", "path": "Patient.meta", "min": 0, "max": "1" },
      {
        "id": "Patient.identifier",
        "path": "Patient.identifier",
        "min": 1,
        "max": "*"
      },
      { "id": "Patient.name", "path": "Patient.name", "min": 0, "max": "*" },
      {
        "id": "Patient.extension:race",
        "path": "Patient.extension",
        "sliceName": "race",
        "min": 0,
        "max": "1"
      }
    ]
  }
}

Notice the snapshot includes all elements (id, meta, name, etc.) while the differential only shows what changed.

Tooling for Authoring and Validation

Most humans don’t write StructureDefinition JSON directly. Use specialized tools:

Authoring:

  • FSH (FHIR Shorthand): A domain-specific language for writing profiles, extensions, and value sets in a readable format
  • Sushi: Compiler that transforms FSH into StructureDefinition JSON
  • IG Publisher: Builds complete IG packages with documentation, examples, and validation artifacts

Validation:

  • HAPI FHIR Validator: Java-based validator supporting profiles, terminology, and custom rules
  • Firely Terminal: .NET-based CLI tool for validation and IG management
  • Validator.js: JavaScript validator for browser and Node.js environments

Workflow: Author in FSH → compile with Sushi → build IG package with IG Publisher → validate instances with HAPI/Firely/validator.js (with IG packages loaded).

Conformance: What “Supports” Actually Means

Conformance has both a data side (resources) and a server/system side (capabilities). Understanding both prevents “we support that profile” misunderstandings.

Data-Side Conformance

meta.profile on an instance is an assertion, not proof. It means “this resource claims to conform to this profile.”

{
  "resourceType": "Patient",
  "meta": {
    "profile": [
      "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
    ]
  }
}

In practice, you’ll see three common levels of conformance claims:

  • Produces/consumes: a client or server can send/receive resources that conform to the profile.
  • Stores and returns: a server can persist the data and return it later (not necessarily enforcing every constraint).
  • Validates and enforces: a server rejects non-conforming resources and guarantees responses meet the profile.

Key point: Producers should only claim meta.profile when they actually conform; consumers should validate if correctness matters.

Server-Side Conformance (CapabilityStatement)

FHIR servers advertise capabilities via CapabilityStatement (typically at GET /metadata). For profiling-related expectations, look for:

  • Supported interactions: read, search, create, etc.
  • Supported search parameters: often IG-required for interoperability programs (e.g., searching US Core Patient by identifier)
  • Declared profile expectations:
    • rest.resource.profile (default/expected profile)
    • rest.resource.supportedProfile (profiles the server claims to support)

Reality check: Many servers under-report or over-report here. Treat CapabilityStatement as a starting point, then verify through validation and test suites. “Supports US Core” often means “accepts US Core instances” rather than “enforces US Core constraints.”

Anti-patterns

Avoid these common profiling mistakes that break interoperability or create unnecessary complexity:

  1. Making everything required because “we always collect it”: Your organization’s data practices aren’t universal. Other implementers might have valid use cases where that element is unknown or not applicable. Over-constraining creates brittle integrations.

  2. Over-slicing arrays: Slicing identifier[] into 12 named slices when 3 would suffice. Each slice adds cognitive load and validation complexity. Slice only when you need to enforce specific rules on specific entries.

  3. Claiming meta.profile without validation: Adding meta.profile to instances without actually validating them is worse than omitting it—consumers trust the claim and then encounter unexpected validation failures.

  4. Using valueString for coded concepts: Profiling Observation.code with a value set binding of “example” strength and accepting free text. This destroys computability. Use required or extensible bindings with proper code systems.

  5. Creating profiles that are too loose: A profile with no required elements, no bindings, and no constraints isn’t a profile—it’s just base FHIR with extra metadata. Profiles should meaningfully narrow the conformance surface.

  6. Creating profiles that are too tight: Fixing values that should be variable (like Patient.gender or Observation.status) prevents valid use cases. Prefer pattern or value set bindings over fixed unless you’re certain.

  7. Ignoring existing IGs: Creating custom profiles when a widely-adopted IG (US Core, IPA, national base) already defines what you need. Reuse reduces fragmentation and improves interoperability.

Interoperability considerations

Optionality and “Must Support” Pitfalls

Common profiling misunderstandings that cause integration failures:

  • Treating mustSupport as “always required in every instance” (it’s usually a capability requirement).
  • Ignoring slicing rules and sending “valid but unsliceable” data.
  • Using valueString where a coded element is expected (no semantics, hard to map).
  • Forgetting that terminology validation may require a terminology service or pre-expanded value sets.
  • Claiming conformance via meta.profile without actually meeting the profile constraints.

Testing and Validation Strategy

A practical validation strategy mirrors how issues surface in production:

  1. Structural validation: JSON shape and required FHIR fields.
  2. Profile validation: validate against the correct StructureDefinition snapshot(s).
  3. Terminology validation: enforce required bindings and confirm code systems/versions.
  4. Invariant validation: evaluate FHIRPath constraints.
  5. Contract tests: validate representative real payloads and edge cases (missing data, multiple identifiers, extensions, etc.).

Where to apply validation:

  • At ingestion (fail fast with OperationOutcome)
  • In CI (prevent regressions when profiles or packages upgrade)
  • In batch QA (catch issues that slip through)

See also: Data Quality and Validation and FHIRPath for understanding invariant expressions.

What validation errors look like

Understanding common profile validation failures helps debug integration issues:

Cardinality violation (missing required element):

OperationOutcome: Patient.identifier - minimum required = 1, but only found 0

Fix: The profile requires at least one identifier. Add an identifier to your Patient instance or verify you’re validating against the correct profile.

Terminology binding violation:

OperationOutcome: The code 'active' is not in the value set
'http://example.org/fhir/ValueSet/patient-status' (required binding)

Fix: Use a code from the required value set. If your code is semantically correct but not in the value set, either: (1) expand the value set, (2) request an extensible binding instead of required, or (3) reconsider your data model.

Slicing violation (discriminator mismatch):

OperationOutcome: Unable to find matching slice for Patient.identifier -
system value 'http://unknown.org/id' doesn't match any slice discriminator

Fix: The profile defines specific slices (e.g., MRN, national ID) identified by system values. Either use one of the expected system values or ensure the profile allows “open” slicing (additional unsliced entries).

Invariant failure (FHIRPath constraint):

OperationOutcome: Constraint 'us-core-8' failed:
Patient.name.given or Patient.name.family or both SHALL be present

Fix: The invariant requires at least a given name or family name. Add the missing data or verify the constraint matches your use case.

Examples

US Core as a Reference Pattern

US Core is a widely used IG that demonstrates common profiling patterns:

  • Resource profiles that constrain cardinality and terminology
  • Extensive slicing (especially identifier, category, coding, and extension)
  • A strong interpretation of “must support” geared toward real interoperability programs

If you’re new to profiling, US Core is a useful reference even outside the US because it shows how a community turns base FHIR into a predictable contract.

Example: a Patient instance asserting US Core Patient conformance (details omitted for brevity):

{
  "resourceType": "Patient",
  "id": "example",
  "meta": {
    "profile": [
      "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
    ]
  },
  "identifier": [
    { "system": "http://hospital.example.org/mrn", "value": "12345" }
  ],
  "name": [{ "family": "Smith", "given": ["Jane"] }]
}

See also

Section: fhir Content Type: reference Audience: technical
FHIR Versions:
R4 R5
Published: 22/07/2023 Modified: 13/12/2025 18 min read
Keywords: profiling profiles Implementation Guide conformance StructureDefinition slicing
Sources: