- hapi-fhir-base (JavaDoc, Source)
- hapi-fhir-structures-r4 (JavaDoc)
- hapi-fhir-structures-r5 (JavaDoc)
- org.hl7.fhir.core (Source)
GitHub: hapifhir/hapi-fhir
- HAPI base classes and interfaces: ca.uhn.fhir.model.api
- HAPI Annotations: ca.uhn.fhir.model.api.annotation
- HAPI FHIR primitive definition classes (XxxxDt): ca.uhn.fhir.model.primitive
- FHIR base interfaces: org.hl7.fhir.instance.model.api
GitHub: hapifhir/org.hl7.fhir.core
-
Core FHIR R4: org.hl7.fhir.r4
- Element model classes: org.hl7.fhir.r4.elementmodel
- FHIR datatype and resource classes: org.hl7.fhir.r4.model
- Includes
XxxxType
that extendsPrimitiveType<T>
that extendsType
- Includes
-
Core FHIR R5: org.hl7.fhir.r5
- Element model classes: org.hl7.fhir.r5.elementmodel
- Extensions: org.hl7.fhir.r5.extensions
- FHIR datatype and resource classes: org.hl7.fhir.r5.model
- A TypeScript Runtime Data Validators Comparison
- NPM Trends: ajv vs io-ts vs joi vs superstruct vs yup vs zod
- Head-to-Head: io-ts vs Zod vs superstruct Analysis
- zod
- Documentation Zod
- GitHub colinhacks/zod
- Tutorial Zod
- Zod Related Libraries
- Zod API Libraries
- Zod Zod to X
- Zod X to Zod
- Zod Mocking
- Zod Utilities for Zod
- zod-validation-error
- zod-error
- zod-metadata - metadata support for Zod schemas
- zod-opts - parsing and validating command-line arguments
- validatorjs/validator.js - use in conjunction with
Refinements
- NPM zod Search
- Articles
- superstruct
- Documentation Superstruct
- FHIR Types (using io-ts)
- Handle FHIR Objects with Typescript (and Javascript)
- typescript-fhir-types (2019)
- typescript-fhir-types (2022 - forked from above)
- io-ts
- io-ts-types
- Documentation Modules
- io-ts-extra
- Articles
- How I use io-ts to guarantee runtime type safety in my TypeScript
- Bridging Static and Runtime Types with io-ts
- Unlocking the Power of Type Encoding / Decoding with io-ts
- Run-Time Type Checking in TypeScript with io-ts
- Creating Custom io-ts Decoders for Runtime Parsing
- Typescript Runtime Validation With io-ts
- Printing Useful io-ts Errors
- Using fp-ts and io-ts: types and implementation
- Bounded types in io-ts
- Simple Examples
- Tools
- Transform Tools JSON to io-ts
- jsDocs.io io-ts
-
FHIR JSON representation of primitive elements
-
Representation
- FHIR elements with primitive datatypes are represented in two parts:
- A JSON property with the name of the element, which has a JSON type of
number
,boolean
, orstring
- a JSON property with _ prepended to the name of the element, which, if present, contains the value's
id
and/orextensions
- A JSON property with the name of the element, which has a JSON type of
- The FHIR types
integer
,unsignedInt
,positiveInt
anddecimal
are represented as aJSON number
, the FHIR typeboolean
as aJSON boolean
, and all other types (includinginteger64
) are represented as aJSON string
which has the same content as that specified for the relevant datatype. Whitespace is always significant (i.e. no leading and trailing spaces for non-strings).
- FHIR elements with primitive datatypes are represented in two parts:
-
Repeating Primitives (
x..*
)-
In the case where the primitive element may repeat, it is represented in two arrays.
JSON null
values are used to fill out both arrays so that theid
and/orextension
are aligned with the matching value in the first array."code": [ "au", "nz" ], "_code": [ null, { "extension" : [ { "url" : "http://hl7.org/fhir/StructureDefinition/display", "valueString" : "New Zealand a.k.a Kiwiland" }] } ]
-
-
- One naïve man’s struggle with TypeScript class serialization
- Mastering Type-Safe JSON Serialization in TypeScript
- Consider superstruct library above?
- Deserializing JSON in TypeScript
- typescript-json-serializer - uses experimentalDecorators
- ts-jackson - uses experimentalDecorators
- ts-serializable - uses experimentalDecorators
- serializr - uses experimentalDecorators
- TypeSerializer - VERY OLD!
- NPM keywords:JSON reviver
- DeepKit Framework - High-quality TypeScript libraries and next-gen backend framework; Perpetual "alpha" since 2020
- Documentation
- Deepkit Runtime Types - @deepkit/type; Rich runtime type system for TypeScript with reflection, serialization/deserialization, validation, and many more features
- Runtime Types Documentation - uses experimentalDecorators
- deepkit/deepkit-framework
- Typia - customizable serializer/deserializer/validator for TypeScript; Active development since before 2022
- TypeScript Enums and Serialization
- Recreating advanced Enum types in Typescript
- ts-typed-json
- typedjson
- typescript-json-decoder
NOTE: Where a timezone (+zz:zz
) is specified, UTC (Z
) may also be specified
- date
YYYY
YYYY-MM
YYYY-MM-DD
- dateTime
YYYY
YYYY-MM
YYYY-MM-DD
YYYY-MM-DDThh:mm:ss+zz:zz
/YYYY-MM-DDThh:mm:ssZ
YYYY-MM-DDThh:mm:ss.sss+zz:zz
/YYYY-MM-DDThh:mm:ss.sssZ
- instant
YYYY-MM-DDThh:mm:ss.sss+zz:zz
/YYYY-MM-DDThh:mm:ss.sssZ
-
References
- NPM Luxon
- Home Luxon
- Changing the default zone
Settings.defaultZone = "utc";
DateTime.local().zoneName;
//=> 'UTC
- Luxon Validity
- Luxon API
- Some Luxon examples
- Changing the default zone
-
Notes
- Immutable, chainable, unambiguous API
- Native time zone and Intl support (no locale or tz files)
- Months in Luxon are 1-indexed instead of 0-indexed like in Moment and the native Date type
- Luxon has both a Duration type and an Interval type
- Luxon parsers are very strict
- Luxon uses getters instead of accessor methods, so
dateTime.year
instead ofdateTime.year()
- Luxon centralizes its "setters", like
dateTime.set({year: 2016, month: 4})
instead ofdateTime.year(2016).month(4)
Luxon Parsing (ISO 8601)
- All supported FHIR formats are directly parsable by Luzon
const dt: DateTime = DateTime.fromISO("2016");
const dt: DateTime = DateTime.fromISO("2016-05");
const dt: DateTime = DateTime.fromISO("2016-05-25");
const dt: DateTime = DateTime.fromISO("2016-05-25T09:24:15Z");
const dt: DateTime = DateTime.fromISO("2016-05-25T09:24:15-04.00");
const dt: DateTime = DateTime.fromISO("2016-05-25T09:24:15.123Z");
const dt: DateTime = DateTime.fromISO("2016-05-25T09:24:15.123-04.00");
- DateTime.fromISO() will default to the system's local timezone unless an offset is included in the dateTime string
or a timezone option is provided to override the default:
const dt: DateTime = DateTime.fromISO("2016-05-25T09:24:15Z");
const dt: DateTime = DateTime.fromISO("2016-05-25T09:24:15-04.00");
const dt: DateTime = DateTime.fromISO("2016-05-25", { zone: "utc" });
const dt: DateTime = DateTime.fromISO("2016-05-25", { zone: "America/New_York" });
DateTime.now().toISO()
will default to the system's local date/time and timezone in ISO formatDateTime.utc().toISO()
will default to UTC and timezone in ISO format
Luxon Formatting (ISO 8601)
- to ISO
dt.toISO();
//=> '2017-04-20T11:32:00.000-04:00'dt.toISO({ suppressMilliseconds: true });
//=> '2017-04-20T11:32:00-04:00'dt.toISODate();
//=> '2017-04-20'
- to Format
dt.toFormat("yyyy");
//=> '2017'dt.toFormat("yyyy-MM");
//=> '2017-04'
- NOTE: HAPI FHIR has been designed to "auto create" data type elements rather than to return
null
. See class header content in Configuration.java - Therefore, we will follow suit and not return
undefined
for allgetXxxxElement
methods.
Based on past experience with FHIR data model generators for TypeScript, I wanted to define at least FHIR primitive data types using TypeScript runtime data validators. I was somewhat familiar with io-ts and wanted to gain a better understanding of that library. In my investigation, I ran across A TypeScript Runtime Data Validators Comparison. The article defines a set of desired design goals a runtime data validator library should satisfy. Subsequent articles provided reviews of six (6) popular runtime data validator libraries that documented how well the libraries satisfied the design goals.
Based on these reviews and my subsequent "deep dive", I decided to use the Zod library.
After researching this subject in many articles on the web and after investigating the various serialization/deserialization libraries (see above), I came to the conclusion that a "generic" approach to serialization/deserialization was overkill in this use case. While complex and primitive data type models are handcrafted, the ultimate goal is to use a code generator to create all FHIR resource data models for each version of FHIR. From experience, creating class templates for a code generator is a straight-forward process. Trying to generalize the serialization/deserialization functionality adds unnecessary complexity to the templates and the generated code.
My goal for the serialization/deserialization functionality was to access the functionality from the FHIR resource data
model classes - not from a separate "tool"/"parser" as is the case in the HAPI FHIR project. To that end, I decided to
provide a public toJSON()
instance method on each FHIR resource data model class to perform the serialization. This
allows consumers of these data model classes to manipulate the data models instances as needed and then when ready,
execute the toJSON()
method of the instance to render the FHIR compatible JSON data object. For the deserialization
process, I decided to provide a public static parse(sourceJson: JSON.Object)
method that can consume the provided
JSON data object and return a fully populated FHIR resource data model instance.
Unit testing best practices recommend testing the public interfaces and not the abstract or private interfaces. While I agree with this recommendation, I have chosen to ignore this recommendation for needs of this proof-of-concept project. The primary goal of this project is to create a set of hand-crafted data models that can be used to create templates from which a production-level project can be created to generate FHIR data models using FHIR StructureDefinitions. Therefore, the hand-crafted data models must be fully documented with significant unit testing to verify all functionality to be generated. The abstract core FHIR models will be created with a minimal set of functionality to define a minimal viable product. As FHIR models are hand-crafted for data types and resources, additional functionality will be added to meet design needs. The desire is to write all unit tests as these data models evolve into a final state.
Therefore for convenience, it will be easier to test the abstract classes rather than replicating tests across all hand-crafted derived classes.
Most Node-based projects make use of JSDoc for documenting the code base and optionally
generating project documentation.
For projects using TypeScript, Microsoft provides the JSDoc Reference.
This combination works for many cases.
In my case, this was not a satisfactory approach. My biggest issue is the inability to provide documentation
for TypeScript type
definitions.
Generating the project documentation using JSDoc and TypeScript felt like a kluge.
As an alternative, Microsoft provides TSDoc as a replacement for JSDoc in TypeScript projects. "TSDoc is a proposal to standardize the doc comments used in TypeScript code, so that different tools can extract content without getting confused by each other's markup. Microsoft also provides a library package that provides an open source reference implementation of a parser. Using this library is an easy way to ensure that your tool is 100% compatible with the standard."
I decided to investigate TSDoc for my use in this project. I noticed the TSDoc documentation mentions various tools that interact with the JSDoc/TSDoc notations and tags. One that caught my eye is TypeDoc. It generates project documentation based on JSDoc/TSDoc notations and tags. Its home page is very brief. It provides a 2-step process to use TypeDoc out of the box:
# Install
npm install --save-dev typedoc
# Execute typedoc on your project
npx typedoc src/index.ts
Following the above steps, I was pleasantly surprised by the quality of the automatic generation of the project's documentation. The current state of the project had very limited JSDoc notations, but TypeDoc generated very good documentation based on the actual TypeScript code. Where JSDoc notation existed, TypeDoc parsed that content and added it to the generated documentation. I was immediately sold on TypeDoc!
The TypeDoc ecosystem includes plugins for various uses. I was thrilled when I discovered a plugin for Zod (described above). TypeDoc provides extensive configuration, but in my case, I only needed to included five (5) options!
Therefore, I am using TypeDoc to generate project documentation!
-
We will need to check header descriptive content for relative links (e.g.,
[Location](location.html#)
) to the FHIR specification to change them to absolute links (e.g.,[Location](https://hl7.org/fhir/location.html#)
). -
We will need to check header descriptive content for whitespace characters (e.g.,
\t
,\n
, etc.) replacing them with aspace
:/** * Replace line breaks (i.e., '\n' and '\r') with a single space (i.e., ' '). * Replace with a space character rather than an empty string to prevent combining * text where the only separating whitespace is '\n' or '\r' (observed in various * StructureDefinition descriptions). * * @param str - source string value * @returns new string value with each line break replaced with a space character */ export const stripLineBreaks = (str: string | undefined | null): string => { const tempValue = str || ''; const regex = /[\r\n]+/g; return tempValue.replace(regex, ' '); };
/**
* <StructureDefinition.type> Class
*
* @remarks
* <StructureDefinition.description>
*
* <StructureDefinition.purpose>
*
* **FHIR Specification**
* - **Short:** <StructureDefinition.snapshot.element[0]?.short>
* - **Definition:** <StructureDefinition.snapshot.element[0]?.definition>
* - **Comment:** <StructureDefinition.snapshot.element[0]?.comment>
* - **Requirements:** <StructureDefinition.snapshot.element[0]?.requirements>
* - **FHIR Version:** <StructureDefinition.fhirVersion>
*
* @privateRemarks
* Loosely based on HAPI FHIR org.hl7.fhir-core.r4.model.<StructureDefinition.type>
*
* @category Resource Models | Datatypes: Complex
* @see [FHIR <StructureDefinition.type>](<StructureDefinition.url>)
*/
The <StructureDefinition.snapshot.element[i].path>
will be reformatted to <ReformattedPathComponent>
as follows:
- The
path
value will be PascalCase - The
.
separator will be removed - 'Component' will be appended to the reformatted
path
/**
* <ReformattedPathComponent> Subclass for `<StructureDefinition.snapshot.element[i].path>`
*
* @remarks
* **FHIR Specification**
* - **Short:** <StructureDefinition.snapshot.element[i]?.short>
* - **Definition:** <StructureDefinition.snapshot.element[i]?.definition>
* - **Comment:** <StructureDefinition.snapshot.element[i]?.comment>
* - **Requirements:** <StructureDefinition.snapshot.element[i]?.requirements>
*
* @category Resource Models
* @see [FHIR <StructureDefinition.type>](<StructureDefinition.url>)
*/
/**
* <StructureDefinition.snapshot.element[i].path> Element
*
* @remarks
* **FHIR Specification**
* - **Short:** <StructureDefinition.snapshot.element[i]?.short>
* - **Definition:** <StructureDefinition.snapshot.element[i]?.definition>
* - **Comment:** <StructureDefinition.snapshot.element[i]?.comment>
* - **Requirements:** <StructureDefinition.snapshot.element[i]?.requirements>
* - **FHIR Type:** `<StructureDefinition.snapshot.element[i].type.code[0]>`
* - _TargetProfiles_: [ <StructureDefinition.snapshot.element[i].type.code[0].taretProfile[?]> ]
* - **Cardinality:** <StructureDefinition.snapshot.element[i].min>..<StructureDefinition.snapshot.element[i].max>
* - **isModifier:** <StructureDefinition.snapshot.element[i].isModifier>
* - **isModifierReason:** <StructureDefinition.snapshot.element[i].isModifierReason?>
* - **isSummary:** <StructureDefinition.snapshot.element[i].isSummary>
*/
/**
* <StructureDefinition.snapshot.element[i].path> Element
*
* @remarks
* **FHIR Specification**
* - **Short:** <StructureDefinition.snapshot.element[i]?.short>
* - **Definition:** <StructureDefinition.snapshot.element[i]?.definition>
* - **Comment:** <StructureDefinition.snapshot.element[i]?.comment>
* - **Requirements:** <StructureDefinition.snapshot.element[i]?.requirements>
* - **FHIR Types:**
* - `<StructureDefinition.snapshot.element[i].type.code[j]>`
* - _TargetProfiles_: [ <StructureDefinition.snapshot.element[i].type.code[0].taretProfile[?]> ]
* - **Cardinality:** <StructureDefinition.snapshot.element[i].min>..<StructureDefinition.snapshot.element[i].max>
* - **isModifier:** <StructureDefinition.snapshot.element[i].isModifier>
* - **isModifierReason:** <StructureDefinition.snapshot.element[i].isModifierReason?>
* - **isSummary:** <StructureDefinition.snapshot.element[i].isSummary>
*/
References:
- How to fix nasty circular dependency issues once and for all in JavaScript & TypeScript
- Tired of circular dependency in Typescript/Node.js?
- Detect, Prevent, and Fix: Circular Dependencies In JavaScript and TypeScript
The FHIR specification defines data types and resources (data models) based on the FHIR Type Framework. Ultimately, the specification defines these data structures for exchanging healthcare data using JSON and/or XML. By design, circular references in the specification cannot be prevented but are not a problem in the actual data structures. Examples of circular references in the FHIR specification include:
- The FHIR Reference data type includes the Identifier data type while the Identifier data type includes the Reference data type.
- All data types inherit from the FHIR Element that includes a property for FHIR Extensions while the FHIR Extension references almost all FHIR data types.
When designing data structures using TypeScript, these circular references cannot be avoided and must be resolved. Various strategies exist to resolve circular references such as:
- Use relative imports inside of the same module
- Move common code into a separate file to be imported
- Move code from one module to another
- Last resort: Combine files with circular dependencies into one file
Unfortunately, due to the FHIR specification, the "last resort" above has been occasionally used in this project. When this has been done, a file/module header block documents these cases.