11# FHIR Profiles Representation
22
3- Status: Implemented (TypeScript). See ` examples/typescript-r4/ ` for working examples.
3+ Status: Implemented (TypeScript). See ` examples/typescript-r4/ ` and ` examples/typescript-us-core/ ` for working examples.
44
55This document covers the representation of FHIR profiles in generated code: resource profiles, extension profiles, and their relationship to base resources.
66
@@ -82,16 +82,21 @@ Problem: if we have several levels of profiles on top of the resource how they s
8282Arbitrary JSON can be converted to:
8383
84841 . Resource type, independently from the profiles.
85- 1 . Profile type via ` ProfileClass.from(resource) ` .
85+ 1 . Profile type via ` ProfileClass.from(resource) ` or ` ProfileClass.apply(resource) ` .
8686
8787``` typescript
8888// Parse as resource
8989const obs: Observation = JSON .parse (json )
9090
91- // Wrap with profile
91+ // from() validates meta. profile and runs validate(), throws on errors
9292const bodyweight = observation_bodyweightProfile .from (obs )
93- bodyweight .getVSCat () // access slice
94- bodyweight .toResource () // back to Observation (same object)
93+
94+ // apply() stamps meta.profile without validation, for incremental construction
95+ const bodyweight = observation_bodyweightProfile .apply (obs )
96+
97+ bodyweight .getVSCat () // access slice (flat by default)
98+ bodyweight .getVSCat (" raw" ) // access raw FHIR element
99+ bodyweight .toResource () // back to Observation (same object)
95100```
96101
97102### Mutable/Immutable Representation
@@ -100,7 +105,7 @@ The profile class holds a mutable reference to the underlying resource. Mutation
100105
101106``` typescript
102107const obs: Observation = { resourceType: " Observation" , status: " preliminary" , ... }
103- const profile = observation_bodyweightProfile .from (obs )
108+ const profile = observation_bodyweightProfile .apply (obs )
104109profile .setStatus (" final" )
105110obs .status // "final" — same object
106111```
@@ -120,38 +125,44 @@ export interface observation_bodyweight extends Observation {
120125}
121126```
122127
123- 2 . ** Profile class** — wraps the resource with typed getters/setters and slice accessors:
128+ 2 . ** Profile class** — wraps the resource with factory methods, typed getters/setters, slice accessors, extension accessors, and validation :
124129
125130``` typescript
126131export class observation_bodyweightProfile {
132+ static readonly canonicalUrl = " http://hl7.org/fhir/StructureDefinition/bodyweight"
127133 private resource: Observation
128134
129135 constructor (resource : Observation ) { ... }
130- static from(resource : Observation ): observation_bodyweightProfile { ... }
131- static createResource(args : observation_bodyweightProfileParams ): Observation { ... }
132- static create(args : observation_bodyweightProfileParams ): observation_bodyweightProfile { ... }
136+
137+ // Factory methods
138+ static from(resource : Observation ): observation_bodyweightProfile { ... } // validates, throws on error
139+ static apply(resource : Observation ): observation_bodyweightProfile { ... } // stamps meta.profile, no validation
140+ static createResource(args : observation_bodyweightProfileRaw ): Observation { ... }
141+ static create(args : observation_bodyweightProfileRaw ): observation_bodyweightProfile { ... }
133142
134143 // Typed getters/setters for constrained fields
135144 getStatus(): (...) | undefined { ... }
136145 setStatus(value : ...): this { ... }
137146
138- // Slice accessors (see slices.md)
139- setVSCat(input ? : Observation_bodyweight_Category_VSCatSliceInput ): this { ... }
140- getVSCat(): Observation_bodyweight_Category_VSCatSliceInput | undefined { ... }
141- getVSCatRaw(): CodeableConcept | undefined { ... }
147+ // Slice accessors with mode overloads (see slices.md)
148+ setVSCat(input ? : VSCatSliceFlat | CodeableConcept ): this { ... }
149+ getVSCat(): VSCatSliceFlat | undefined { ... } // flat (default)
150+ getVSCat(mode : ' flat' ): VSCatSliceFlat | undefined { ... }
151+ getVSCat(mode : ' raw' ): CodeableConcept | undefined { ... }
142152
143153 // Conversion
144154 toResource(): Observation { ... }
145- toProfile(): observation_bodyweight { ... }
155+
156+ // Validation
157+ validate(): { errors: string []; warnings: string [] } { ... }
146158}
147159```
148160
1491613 . ** Params type** — lists fields for the ` createResource ` /` create ` factory methods. Array fields with required slices are optional -- stubs are auto-merged:
150162
151163``` typescript
152- export type observation_bodyweightProfileParams = {
164+ export type observation_bodyweightProfileRaw = {
153165 status: (...);
154- code: CodeableConcept <(...)>;
155166 subject: Reference <" Patient" >;
156167 category? : CodeableConcept <(...)>[]; // optional -- required slice stubs auto-merged
157168}
@@ -166,27 +177,24 @@ Extension profiles constrain the `Extension` type. They come in two forms:
166177A simple extension carries one ` value [x ]` field (e.g., ` patient -birthPlace ` carries ` valueAddress ` ):
167178
168179` ` ` typescript
169- export type birthPlaceProfileParams = {
180+ export type birthPlaceProfileRaw = {
170181 valueAddress: Address ;
171182}
172183
173184export class birthPlaceProfile {
185+ static readonly canonicalUrl = " http://hl7.org/fhir/StructureDefinition/patient-birthPlace"
174186 private resource: Extension
175187
176- static createResource(args : birthPlaceProfileParams ): Extension {
177- return {
178- url: " http://hl7.org/fhir/StructureDefinition/patient-birthPlace" ,
179- valueAddress: args .valueAddress ,
180- } as unknown as Extension
181- }
182-
183- static create(args : birthPlaceProfileParams ): birthPlaceProfile { ... }
184- static from(resource : Extension ): birthPlaceProfile { ... }
188+ static from(resource : Extension ): birthPlaceProfile { ... } // validates
189+ static apply(resource : Extension ): birthPlaceProfile { ... } // no validation
190+ static createResource(args : birthPlaceProfileRaw ): Extension { ... }
191+ static create(args : birthPlaceProfileRaw ): birthPlaceProfile { ... }
185192
186193 getValueAddress(): Address | undefined { ... }
187194 setValueAddress(value : Address ): this { ... }
188195
189196 toResource(): Extension { ... }
197+ validate(): { errors: string []; warnings: string [] } { ... }
190198}
191199```
192200
@@ -206,36 +214,70 @@ const patient: Patient = {
206214A complex extension has nested extension elements instead of a single value (e.g., ` patient-nationality ` has ` code ` and ` period ` sub-extensions):
207215
208216``` typescript
217+ // Raw input — pass extension[] directly
218+ export type nationalityProfileRaw = { extension? : Extension [] }
219+
220+ // Flat input — typed sub-extension fields
221+ export type nationalityProfileFlat = { code? : CodeableConcept ; period? : Period }
222+
209223export class nationalityProfile {
224+ static readonly canonicalUrl = " http://hl7.org/fhir/StructureDefinition/patient-nationality"
210225 private resource: Extension
211226
212- static createResource(): Extension {
213- return {
214- url: " http://hl7.org/fhir/StructureDefinition/patient-nationality" ,
215- } as unknown as Extension
216- }
227+ static from(resource : Extension ): nationalityProfile { ... }
228+ static apply(resource : Extension ): nationalityProfile { ... }
229+
230+ // create/createResource accept both raw and flat input
231+ static createResource(args ? : nationalityProfileRaw | nationalityProfileFlat ): Extension { ... }
232+ static create(args ? : nationalityProfileRaw | nationalityProfileFlat ): nationalityProfile { ... }
217233
218- // Sub-extension accessors
234+ // Sub-extension accessors with mode overloads
219235 setCode(value : CodeableConcept ): this { ... }
236+ getCode(): CodeableConcept | undefined { ... } // flat (default)
237+ getCode(mode : ' flat' ): CodeableConcept | undefined { ... }
238+ getCode(mode : ' raw' ): Extension | undefined { ... } // raw sub-extension
239+
220240 setPeriod(value : Period ): this { ... }
221- getCode(): CodeableConcept | undefined { ... }
222- getCodeExtension(): Extension | undefined { ... } // raw access
223241 getPeriod(): Period | undefined { ... }
224- getPeriodExtension( ): Extension | undefined { ... }
242+ getPeriod( mode : ' raw ' ): Extension | undefined { ... }
225243
226244 toResource(): Extension { ... }
245+ validate(): { errors: string []; warnings: string [] } { ... }
227246}
228247```
229248
230249Usage:
231250
232251``` typescript
233- const profile = nationalityProfile .create ()
234- .setCode ({ coding: [{ system: " urn:iso:std:iso:3166" , code: " US" }] })
235- .setPeriod ({ start: " 2000-01-01" })
252+ // Flat input
253+ const profile = nationalityProfile .create ({
254+ code: { coding: [{ system: " urn:iso:std:iso:3166" , code: " US" }] },
255+ period: { start: " 2000-01-01" },
256+ })
257+
258+ // Read values back
259+ profile .getCode () // { coding: [...] }
260+ profile .getCode (" raw" ) // { url: "code", valueCodeableConcept: { coding: [...] } }
261+
236262const ext: Extension = profile .toResource ()
237263```
238264
265+ ### Extension Accessors on Resource Profiles
266+
267+ Resource profiles that declare extensions (e.g., US Core Patient with ` us-core-race ` ) generate multi-form setters and overloaded getters:
268+
269+ ``` typescript
270+ // Setter accepts flat input, profile instance, or raw Extension
271+ patient .setRace ({ ombCategory: { code: " 2028-9" }, text: " Asian" }) // flat input
272+ patient .setRace (USCoreRaceExtensionProfile .create ({ ... })) // profile instance
273+ patient .setRace ({ url: " http://.../us-core-race" , extension: [... ] }) // raw Extension
274+
275+ // Getter with mode overloads
276+ patient .getRace () // flat: { ombCategory: ..., text: "Asian" }
277+ patient .getRace (" profile" ) // USCoreRaceExtensionProfile instance
278+ patient .getRace (" raw" ) // raw FHIR Extension
279+ ```
280+
239281## TypeSchema Representation
240282
241283Profiles use ` kind = "constraint" ` in TypeSchema.
@@ -246,20 +288,38 @@ Current approach: to collect all profile elements we traverse the inheritance tr
246288
247289Profile classes depend on a generated ` profile-helpers.ts ` module that provides:
248290
291+ Slice helpers:
249292- ` applySliceMatch(input, match) ` — merges discriminator values into a slice element
250- - ` matchesSlice(value, match) ` — checks if an element matches a slice discriminator
251- - ` extractSliceSimplified(slice, matchKeys) ` — strips discriminator keys from a slice for the simplified input type
252- - ` wrapSliceChoice(input, choiceVariant) ` — wraps flat input fields under a single choice variant key (for setter)
253- - ` flattenSliceChoice(slice, matchKeys, choiceVariant) ` — strips discriminator keys and flattens a single choice variant into parent (for getter)
254- - ` mergeMatch(target, match) ` — deep-merges match values into target
293+ - ` matchesValue(value, match) ` — recursive structural match test
294+ - ` setArraySlice(list, match, value) ` — find-or-insert in array by discriminator
295+ - ` getArraySlice(list, match) ` — find first matching element
296+ - ` ensureSliceDefaults(items, ...matches) ` — ensure required slices have stubs
297+ - ` stripMatchKeys(slice, matchKeys) ` — remove discriminator keys from getter result
298+ - ` wrapSliceChoice(input, choiceVariant) ` — wrap flat input fields under a single choice variant key (for setter)
299+ - ` unwrapSliceChoice(slice, matchKeys, choiceVariant) ` — inverse of wrap
300+
301+ Extension helpers:
302+ - ` ensurePath(root, path) ` — navigate/create nested paths for deep extensions
255303- ` extractComplexExtension(extension, config) ` — extracts typed values from nested extension elements
256- - ` validateRequired(r, field, path) ` — checks that a required field is present
257- - ` validateMustSupport(r, field, path) ` — checks that a must-support field is populated (warning, not error)
258- - ` validateExcluded(r, field, path) ` — checks that a forbidden field is absent
259- - ` validateFixedValue(r, field, expected, path) ` — checks that a field matches a fixed/pattern value
260- - ` validateSliceCardinality(items, match, sliceName, min, max, path) ` — checks min/max counts for a named slice
261- - ` validateEnum(value, allowed, field, path) ` — checks that a value is within a required value set (supports primitives, Coding, CodeableConcept)
262- - ` validateReference(value, allowed, field, path) ` — checks that a reference targets an allowed resource type
304+ - ` isExtension(input, url?) ` — type guard for raw Extension detection
305+ - ` isRawExtensionInput(input) ` — discriminate raw vs flat input for extension profile factories
306+ - ` getExtensionValue(ext, field) ` — read a typed value field from Extension
307+ - ` pushExtension(target, ext) ` — push extension onto target.extension array
308+
309+ Factory helpers:
310+ - ` buildResource(obj) ` — cast object to resource type
311+ - ` ensureProfile(resource, canonicalUrl) ` — add profile URL to meta.profile
312+ - ` mergeMatch(target, match) ` — deep-merges match values into target
313+
314+ Validation helpers:
315+ - ` validateRequired(res, profileName, field) ` — checks that a required field is present
316+ - ` validateMustSupport(res, profileName, field) ` — checks that a must-support field is populated (warning, not error)
317+ - ` validateExcluded(res, profileName, field) ` — checks that a forbidden field is absent
318+ - ` validateFixedValue(res, profileName, field, expected) ` — checks that a field matches a fixed/pattern value
319+ - ` validateSliceCardinality(res, profileName, field, match, sliceName, min, max) ` — checks min/max counts for a named slice
320+ - ` validateChoiceRequired(res, profileName, choices) ` — checks that at least one choice variant is present
321+ - ` validateEnum(res, profileName, field, allowed) ` — checks that a value is within a value set (supports primitives, Coding, CodeableConcept)
322+ - ` validateReference(res, profileName, field, allowed) ` — checks that a reference targets an allowed resource type
263323
264324## Configuration
265325
@@ -308,6 +368,14 @@ const { errors, warnings } = bp.validate();
308368// Required slices (VSCat, SystolicBP, DiastolicBP) are auto-populated by create()
309369```
310370
371+ ` from() ` uses ` validate() ` internally — it throws on errors but allows warnings:
372+
373+ ``` typescript
374+ const profile = observation_bodyweightProfile .from (obs )
375+ // throws if meta.profile is missing or validate().errors is non-empty
376+ // warnings are not thrown — retrieve them via profile.validate().warnings
377+ ```
378+
311379Validation helpers are emitted into ` profile-helpers.ts ` alongside the existing slice helpers.
312380
313381## Future Work
0 commit comments