InkGen: FHIR Code Generation for Modern Languages
Welcome to the InkGen documentation! InkGen is a powerful, extensible code generator that transforms FHIR (Fast Healthcare Interoperability Resources) StructureDefinitions into type-safe, idiomatic code for multiple programming languages.
⚠️ Development Stage: InkGen is currently in active development. The TypeScript backend is functional and tested, but the project is evolving and APIs may change. We welcome early adopters and contributors!
What is InkGen?
InkGen automates the generation of language-specific SDKs from FHIR specifications. Whether you’re building a healthcare application in TypeScript or another language, InkGen ensures your code stays in sync with FHIR standards while maintaining flexibility for custom extensions and profiles.
Key Features
- Multi-Language Support: Currently supporting TypeScript with extensible backend architecture for additional languages
- FHIR Profile Support: Full support for custom profiles, extensions, and constraints on FHIR resources
- Type Safety: Generate strongly-typed code that catches errors at compile time
- Template Customization: Override and extend code generation via template overlays
- Performance Optimized: Efficiently handles large FHIR packages with built-in caching and parallel processing
- CI/CD Ready: Seamless integration with your development pipeline
Getting Started
If you’re new to InkGen, start with the Getting Started guide.
For experienced developers, explore:
- Architecture - Understand how InkGen works internally
- Language Backends - Learn about supported languages and extending support
- Advanced Topics - Customize templates, profiles, and performance
Architecture Overview
FHIR Specifications
↓
Fetch & Cache
↓
IR Construction & Resolution
↓
Language-Specific Code Generation
↓
Formatted Code Output
InkGen operates in stages:
- Fetch: Download and cache FHIR specification packages from canonical registries
- Parse: Convert FHIR StructureDefinitions into an Internal Representation (IR)
- Resolve: Build genealogy chains, resolve inheritance, and validate constraints
- Generate: Render language-specific code from templates
- Output: Format and write code to your project
Quick Links
- CLI Reference: Complete command-line option documentation
- Configuration: How to configure InkGen for your project
- Template Overlays: Customize code generation templates
- Contributing: How to contribute new backends or features
Support
- Troubleshooting Guide: Common issues and solutions
- GitHub Issues: Report bugs or request features
- GitHub Discussions: Ask questions and discuss usage
License
InkGen is licensed under the MIT License. See LICENSE.md for details.
Ready to generate code? → Installation
Getting Started with InkGen
This guide walks you through installing and using InkGen for the first time.
Installation
Prerequisites
- Rust stable toolchain (edition 2024 support)
justcommand runner (recommended)
From GitHub (Source)
InkGen is currently available via GitHub. Clone and build the repository:
git clone https://github.com/octofhir/inkgen.git
cd inkgen
cargo build --release -p inkgen-cli
./target/release/inkgen --version
For development workflows, install just and bootstrap:
cargo install just
just bootstrap
First Steps
1. Create a Configuration File
Create inkgen.toml in your project root:
[packages]
hl7-fhir-us-core = "5.0.1"
[languages.typescript]
output_dir = "./generated"
2. Fetch FHIR Specifications
Download the FHIR packages referenced in your configuration:
inkgen fetch --config inkgen.toml
This downloads and caches FHIR specifications locally. Check ~/.cache/inkgen/ for cached packages.
3. Generate Code
Generate TypeScript code:
inkgen generate typescript --config inkgen.toml
Your generated code appears in ./generated/.
Configuration
Package Selection
Specify which FHIR packages to use:
[packages]
hl7-fhir-us-core = "5.0.1"
hl7-fhir-r4-core = "4.0.1"
Language-Specific Settings
TypeScript
[languages.typescript]
output_dir = "./generated"
# Optional: customize templates
overlays = ["./my-templates"]
# Optional: configure output
package_name = "fhir-us-core"
Caching
InkGen caches downloaded FHIR packages to avoid re-downloading. The cache location is ~/.cache/inkgen/.
To work offline:
inkgen fetch --offline
inkgen generate typescript --offline
To clear the cache:
rm -rf ~/.cache/inkgen/
Project Structure
After generation, your TypeScript project might look like:
project/
├── generated/
│ ├── Patient.ts # Resource definitions
│ ├── Observation.ts
│ ├── index.ts # Barrel export
│ ├── types/ # Type definitions
│ └── validators/ # Validation helpers
├── src/
│ └── my-code.ts # Your code
├── inkgen.toml # InkGen config
└── package.json # npm config
Next Steps
- Configuration Guide: Learn all configuration options
- TypeScript Backend: Details on TypeScript code generation
- Template Overlays: Customize generated code
- Architecture: Understand how InkGen works
Troubleshooting
Q: “No packages found” error
A: Verify your inkgen.toml references valid FHIR packages. Run inkgen fetch --dry-run to test.
Q: “Resource not found” warnings
A: Some dependencies might not be available in your package version. Check the FHIR specification for the package version you’re using.
Q: Generated code has syntax errors
A: Ensure you’re using a supported version of TypeScript (3.9+). Check Troubleshooting for more help.
See Troubleshooting for more common issues and solutions.
Installation
Prerequisites
Before installing InkGen, ensure you have the following installed:
- Rust stable toolchain (edition 2024 support)
- just command runner (recommended for development)
Installation Steps
From GitHub (Source)
InkGen is currently available via GitHub. Clone and build the repository:
git clone https://github.com/octofhir/inkgen.git
cd inkgen
cargo build --release -p inkgen-cli
The binary will be at ./target/release/inkgen.
Install just (Recommended)
For development workflows, install the just command runner:
cargo install just
Then bootstrap the project:
just bootstrap
Verification
To verify the installation was successful:
./target/release/inkgen --version
Or if you added the binary to your PATH:
inkgen --version
You should see the version number of InkGen.
Next Steps
After installation, check out the Quick Start guide to get started with your first FHIR code generation.
Quick Start
Get started with InkGen in just a few minutes!
Your First TypeScript SDK
1. Install InkGen
Install InkGen via Cargo:
cargo install inkgen-cli
Or build from source:
git clone https://github.com/octofhir/inkgen.git
cd inkgen
cargo build --release -p inkgen-cli
2. Initialize Your Project
Create a configuration file:
inkgen config init
This creates an inkgen.toml file. Edit it to specify which FHIR packages you want:
[packages]
hl7-fhir-r4-core = "4.0.1"
[languages.typescript]
output_dir = "./generated"
mode = "interface"
3. Fetch FHIR Specifications
Download the FHIR packages:
inkgen fetch
4. Generate Code
Generate TypeScript code from the FHIR specifications:
inkgen generate typescript
5. Use the Generated Code
Your TypeScript code will be in the ./generated directory:
import { Patient, Observation } from './generated';
const patient: Patient = {
resourceType: 'Patient',
name: [{
family: 'Smith',
given: ['John']
}],
birthDate: '1980-01-01'
};
Learn More
- Configuration - Customize InkGen behavior
- Architecture - Understand how InkGen works
- Advanced Topics - Explore advanced features
Configuration
InkGen can be configured through configuration files and command-line options.
Configuration File
Create an inkgen.config.json file in your project root:
{
"input": "./profiles",
"output": "./generated",
"backends": ["typescript"],
"fhirVersion": "r4",
"strict": true
}
Configuration Options
| Option | Type | Description |
|---|---|---|
input | string | Directory containing FHIR Shorthand files |
output | string | Directory for generated code |
backends | array | List of code backends to use |
fhirVersion | string | FHIR version (r4, r5, etc.) |
strict | boolean | Enable strict validation |
Command-Line Options
npx inkgen generate [options]
Options:
--input, -i Input directory (default: ./profiles)
--output, -o Output directory (default: ./generated)
--backend, -b Code backend to use (default: typescript)
--config, -c Path to config file
--help, -h Show help
--version, -v Show version
Environment Variables
Set these environment variables to configure InkGen:
INKGEN_INPUT- Input directoryINKGEN_OUTPUT- Output directoryINKGEN_BACKEND- Default backendFHIR_VERSION- FHIR version
Next Steps
Check out the Architecture guide to understand how InkGen processes your profiles.
Architecture
InkGen’s modular architecture enables efficient FHIR code generation while maintaining extensibility for new languages and features.
Directory Overview
- overview.md - High-level system design and data flow
- concepts.md - Core concepts: IR, genealogy, profiles, extensions
- ir-design.md - Internal Representation (IR) structure and evolution
- type-system.md - Type mapping and resolution across languages
Key Principles
- Separation of Concerns: FHIR parsing, IR construction, and code generation are independent
- Extensibility: Add new language backends without modifying core
- Performance: Caching, parallel processing, and lazy evaluation where possible
- Correctness: Type safety and constraint validation throughout the pipeline
See Architecture Decision Records for design rationale on major decisions.
Architecture Overview
InkGen is a FHIR code generation tool that transforms FHIR Shorthand (FSH) specifications into implementation-ready code.
High-Level Architecture
FSH Input
↓
FSH Parser
↓
Semantic Analysis
↓
Intermediate Representation (IR)
↓
Backend Codegen
↓
Generated Code
Key Components
1. FSH Parser
Parses FHIR Shorthand syntax into an abstract syntax tree (AST).
2. Semantic Analyzer
Validates profiles and resolves references to FHIR base resources and value sets.
3. Intermediate Representation
Generates a language-agnostic intermediate representation of the profile structure.
4. Backend Code Generators
Transforms the IR into implementation-specific code (TypeScript, Java, etc.).
Design Principles
- Modularity - Each component has a single responsibility
- Extensibility - Easy to add new backends and analyzers
- Performance - Efficient processing of large profile sets
- Correctness - Rigorous validation at each step
See Core Concepts for more details.
Core Concepts
FHIR Profiles
A FHIR profile constrains a base resource to apply domain-specific rules.
Example:
Profile: MyPatient
Parent: Patient
Title: "My custom Patient profile"
Elements and Cardinality
Elements are individual fields within a resource. Cardinality defines min..max occurrences:
* name 1..* MS // 1 or more mandatory
* birthDate 0..1 // 0 or 1 optional
Extensions
Custom data elements that extend the standard FHIR resource:
* extension contains race 0..1
Slicing
Discriminating between different types of the same element:
* component ^slicing.discriminator.type = #pattern
* component ^slicing.discriminator.path = "code"
Value Sets and Codings
Constrain coded elements to specific value sets:
* status from ObservationStatus (required)
For more information, see IR Design.
IR Design
The Intermediate Representation (IR) is InkGen’s internal model for representing FHIR profiles.
IR Structure
The IR provides:
- Resource Definition - Base resource and applied profile
- Elements - Fields with types, cardinality, and constraints
- Extensions - Custom data elements
- Slices - Discriminated element variants
- Bindings - Value set bindings to coded elements
IR to Code Generation
The IR is designed to be:
- Language-agnostic - Can generate code for any language
- Complete - Contains all information needed for validation
- Efficient - Optimized for traversal and code generation
- Extensible - Can be extended with custom information
Example IR
{
"name": "MyPatient",
"parent": "Patient",
"elements": [
{
"path": "name",
"type": "HumanName",
"min": 1,
"max": "*",
"isMustSupport": true
},
{
"path": "birthDate",
"type": "date",
"min": 0,
"max": 1
}
]
}
See Type System for details on type representation.
Type System
InkGen’s type system maps FHIR types to implementation-specific types.
FHIR Primitive Types
| FHIR Type | JavaScript | Java | Notes |
|---|---|---|---|
| string | string | String | Text data |
| boolean | boolean | Boolean | True/false |
| integer | number | int | Whole numbers |
| decimal | number | BigDecimal | Precise decimals |
| date | Date | LocalDate | Calendar date |
| dateTime | Date | ZonedDateTime | With timezone |
| code | string | String | Coded value |
FHIR Complex Types
| FHIR Type | Description |
|---|---|
| CodeableConcept | Coding with text |
| Coding | Code system reference |
| Identifier | Business identifier |
| Quantity | Numeric value with units |
| Period | Start and end time |
| Ratio | Numeric ratio |
Custom Types
Profiles can define custom types that extend or constrain base types:
Profile: CustomQuantity
Parent: Quantity
* system 1..1 MS
* value 1..1 MS
Type Constraints
Cardinality, patterns, and value set bindings form the type constraint system:
* value[x] only Quantity or string
* code from MyValueSet (required)
For more on code generation, see Language Backends.
Language Backends
InkGen’s modular backend architecture allows code generation for multiple languages while sharing the core IR and resolution engine.
Supported Languages
TypeScript (Stable)
Modern TypeScript/JavaScript SDK generation with:
- Strongly-typed resource definitions
- Runtime validators
- Extension and profile support
- Discriminator unions for polymorphic types
See TypeScript Backend for details.
Architecture
Each language backend implements the LanguageGenerator trait:
#![allow(unused)]
fn main() {
pub trait LanguageGenerator<S: StructureDefinitionProvider> {
async fn generate(
&self,
service: &S,
descriptor: &PackageDescriptor,
config: &StructureProviderConfig,
) -> Result<()>;
}
}
Backends:
- Receive the parsed IR and type information
- Render templates (usually Tera or language-specific)
- Output formatted code
Creating a New Backend
See Extending Backends for a step-by-step guide to building a new language backend.
Backend Checklist
For production backends:
- Full resource type coverage
- Profile and extension support
- Value set integration
- Discriminator union handling
- Comprehensive test suite
- Documentation and examples
- CI/CD integration
- Performance benchmarks
TypeScript Backend
Generate TypeScript code from FHIR StructureDefinitions.
Overview
The TypeScript backend generates type-safe code from FHIR specifications:
- TypeScript interfaces for FHIR resources
- ValueSet types: Const arrays, union types, and type guards
- Profile classes: Extension accessors, serialization, and validation
- Zod schemas for runtime validation
- BackboneElement types as separate interfaces
- Structural type guards
Basic Usage
inkgen generate typescript
Generated Output
For a FHIR Patient resource, you get:
export interface Patient {
resourceType: 'Patient';
id?: string;
meta?: Meta;
name?: HumanName[];
birthDate?: string;
gender?: 'male' | 'female' | 'other' | 'unknown';
// ... other fields
}
// Zod schema for validation
export const PatientSchema = z.object({
resourceType: z.literal('Patient'),
id: z.string().optional(),
name: z.array(HumanNameSchema).optional(),
birthDate: z.string().regex(/^\d{4}(-\d{2}(-\d{2})?)?$/).optional(),
// ... other fields
});
// Validation function
export function parsePatient(input: unknown): PatientValidated | false {
const result = PatientSchema.safeParse(input);
return result.success ? result.data : false;
}
Configuration
Configure TypeScript output in inkgen.toml:
[languages.typescript]
output_dir = "./generated"
mode = "interface" # interface, class, or class_with_builder
naming_convention = "pascal" # pascal, camel, or snake
output_structure = "flat" # flat or by_package
# All features are enabled by default; set to false to opt out.
# structural_guards = false
# generate_profiles = false
# generate_valuesets = false
# profile_classes = false
# zod_schemas = false
# Profile method configuration (defaults shown)
[languages.typescript.profile_methods]
extension_accessors = true
extension_style = "both" # "typed", "raw", or "both"
serialization = true
validation = true
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
output_dir | string | “./generated” | Output directory for generated files |
mode | string | “interface” | Generation mode (interface, class, class_with_builder) |
naming_convention | string | “pascal” | Field naming style (pascal, camel, snake) |
structural_guards | boolean | true | Generate type guard functions |
generate_profiles | boolean | true | Generate profile types |
generate_valuesets | boolean | true | Generate valueset types |
max_valueset_codes | number | none | Skip valuesets with more codes than this limit |
output_structure | string | “flat” | Output structure (flat, by_package) |
Profile Method Options
| Option | Type | Default | Description |
|---|---|---|---|
extension_accessors | boolean | true | Generate extension getter/setter methods |
extension_style | string | “both” | Extension accessor style: “typed”, “raw”, or “both” |
serialization | boolean | true | Generate toJson/toObject methods |
validation | boolean | true | Generate fromJson/fromObject methods |
zod_schemas | boolean | true | Generate Zod schemas for runtime validation |
CLI Options
Override configuration via command-line:
# Use a different output directory
inkgen generate typescript --output ./src/fhir
# Change generation mode
inkgen generate typescript --mode class
# Change naming convention
inkgen generate typescript --naming camel
# Dry run (preview without writing files)
inkgen generate typescript --dry-run
ValueSets
ValueSets are generated as type-safe const arrays with union types and type guards.
Generated ValueSet Code
For a FHIR ValueSet like AdministrativeGender:
/**
* Administrative Gender
*
* The gender of a person used for administrative purposes
* @see http://hl7.org/fhir/ValueSet/administrative-gender
*/
export const AdministrativeGenderValues = [
"male", "female", "other", "unknown"
] as const;
// Type alias from const array
export type AdministrativeGender = typeof AdministrativeGenderValues[number];
// Type guard function
export function isAdministrativeGender(value: string): value is AdministrativeGender {
return AdministrativeGenderValues.includes(value as any);
}
// Code definitions with display names and descriptions
export const AdministrativeGenderDefinitions = {
"male": {
code: "male",
display: "Male",
},
"female": {
code: "female",
display: "Female",
},
"other": {
code: "other",
display: "Other",
},
"unknown": {
code: "unknown",
display: "Unknown",
},
} as const;
Binding Strength
ValueSets respect FHIR binding strength:
Required/Extensible Bindings - Closed types (only specified codes):
export type AccountStatus = typeof AccountStatusValues[number];
// = "active" | "inactive" | "entered-in-error"
Preferred/Example Bindings - Open types (allow custom codes):
// Open valueset - allows custom codes
export type ConditionSeverity =
typeof ConditionSeverityValues[number] | (string & {});
// = "mild" | "moderate" | "severe" | (string & {})
The | (string & {}) pattern allows any string while maintaining autocomplete for known values.
Using ValueSets
import {
AdministrativeGender,
isAdministrativeGender,
AdministrativeGenderDefinitions
} from './generated/valuesets';
// Type-safe usage
const gender: AdministrativeGender = "male"; // ✓
const invalid: AdministrativeGender = "invalid"; // ✗ Type error
// Runtime validation
function validateGender(input: string): AdministrativeGender | null {
return isAdministrativeGender(input) ? input : null;
}
// Access metadata
const maleInfo = AdministrativeGenderDefinitions.male;
console.log(maleInfo.display); // "Male"
ValueSet Size Limits
Configure max_valueset_codes to skip large valuesets:
[languages.typescript]
max_valueset_codes = 100 # Skip valuesets with >100 codes
This prevents generating extremely large union types that can slow down TypeScript compilation.
FHIR Profiles
InkGen generates TypeScript classes for FHIR profiles with extension accessors, serialization, and validation.
Generated Profile Class
For a US Core Patient profile:
import type { Coding, Extension } from "./types";
import { z } from 'zod';
import { PatientSchema } from "./Patient";
/**
* US Core Patient Profile
*
* Defines constraints and extensions on the Patient resource for US Core
* @profile http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient
*/
export class USCorePatient extends Patient {
/** Profile URL for runtime validation */
readonly __profile = 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient';
// Fixed value constraints
/** Fixed value: true */
declare active: true;
// Must-support elements (required by profile)
/** Required by profile: Patient.identifier */
declare identifier: NonNullable<Patient['identifier']>;
/** Required by profile: Patient.name */
declare name: NonNullable<Patient['name']>;
// Extension accessors - Typed value access
/**
* Get the race extension value
* @see http://hl7.org/fhir/us/core/StructureDefinition/us-core-race
*/
get uSCoreRace(): Coding | undefined {
const ext = this.extension?.find(
e => e.url === 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race'
);
return ext?.valueCoding;
}
/**
* Set the race extension value
*/
set uSCoreRace(value: Coding | undefined) {
if (!this.extension) {
this.extension = [];
}
const existingIndex = this.extension.findIndex(
e => e.url === 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race'
);
const newExt = value !== undefined
? { url: 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race', valueCoding: value }
: undefined;
if (existingIndex >= 0) {
if (newExt) {
this.extension[existingIndex] = newExt;
} else {
this.extension.splice(existingIndex, 1);
}
} else if (newExt) {
this.extension.push(newExt);
}
}
// Extension accessors - Raw Extension access
/**
* Get the raw race Extension object
*/
get uSCoreRaceExtension(): Extension | undefined {
return this.extension?.find(
e => e.url === 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race'
);
}
/**
* Set the raw race Extension object
*/
set uSCoreRaceExtension(value: Extension | undefined) {
// ... implementation
}
// Serialization methods
/**
* Serialize to JSON string
* @param pretty Whether to pretty-print the JSON
*/
toJson(pretty?: boolean): string {
return pretty ? JSON.stringify(this, null, 2) : JSON.stringify(this);
}
/**
* Convert to plain object (strips __profile metadata)
*/
toObject(): Patient {
const { __profile, ...rest } = this;
return rest as Patient;
}
// Validation methods
/**
* Create instance from JSON string with validation
* @throws {z.ZodError} if validation fails
*/
static fromJson(json: string): USCorePatient {
const parsed = JSON.parse(json);
return USCorePatient.fromObject(parsed);
}
/**
* Create instance from object with validation
* @throws {z.ZodError} if validation fails
*/
static fromObject(obj: unknown): USCorePatient {
const validated = USCorePatientSchema.parse(obj);
return Object.assign(new USCorePatient(), validated);
}
}
/**
* Zod schema for USCorePatient
*/
export const USCorePatientSchema = PatientSchema.extend({
identifier: z.array(z.unknown()).min(1),
name: z.array(z.unknown()).min(1),
});
/**
* Type inferred from Zod schema
*/
export type USCorePatientValidated = z.infer<typeof USCorePatientSchema>;
/**
* Type guard to check if a value is a USCorePatient
*/
export function isUSCorePatient(value: Patient): value is USCorePatient {
return '__profile' in value &&
value.__profile === 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient';
}
Extension Accessor Styles
Configure how extension accessors are generated:
Typed Style (extension_style = "typed"):
get uSCoreRace(): Coding | undefined { /* ... */ }
set uSCoreRace(value: Coding | undefined) { /* ... */ }
Raw Style (extension_style = "raw"):
get uSCoreRaceExtension(): Extension | undefined { /* ... */ }
set uSCoreRaceExtension(value: Extension | undefined) { /* ... */ }
Both Style (extension_style = "both") - Default:
Generates both typed and raw accessors for maximum flexibility.
Using Profile Classes
import { USCorePatient } from './generated/profiles/USCorePatient';
// Create instance
const patient = new USCorePatient();
patient.identifier = [{ system: "http://hospital.example.org", value: "12345" }];
patient.name = [{ family: "Smith", given: ["John"] }];
patient.active = true;
// Use typed extension accessor
patient.uSCoreRace = {
system: "urn:oid:2.16.840.1.113883.6.238",
code: "2106-3",
display: "White"
};
// Serialize
const json = patient.toJson(true);
const plainObject = patient.toObject();
// Deserialize with validation
try {
const validated = USCorePatient.fromJson(jsonString);
console.log(validated.name);
} catch (error) {
console.error("Validation failed:", error);
}
// Runtime type checking
if (isUSCorePatient(somePatient)) {
// TypeScript knows this is USCorePatient
console.log(somePatient.uSCoreRace);
}
Profile Method Configuration
Control which methods are generated:
[languages.typescript.profile_methods]
extension_accessors = false # Disable extension accessors
serialization = false # Disable toJson/toObject
validation = false # Disable fromJson/fromObject
zod_schemas = false # Disable Zod schema generation
Minimal Configuration (only type constraints):
export class USCorePatient extends Patient {
readonly __profile = '...';
declare identifier: NonNullable<Patient['identifier']>;
declare name: NonNullable<Patient['name']>;
}
This provides pure type safety without runtime overhead.
Template Overlays
Customize generated code by providing template overlays:
[languages.typescript]
overlays = ["./my-templates"]
Create a file at ./my-templates/structure.ts.tera to override the default structure template.
See Template Overlays for more details.
Runtime Validation with Zod
Use the generated Zod schemas to validate data at runtime:
import { parsePatient, PatientSchema } from './generated/patient';
import { USCorePatientSchema } from './generated/profiles/USCorePatient';
// Option 1: Use the parse function
const patient = parsePatient(unknownData);
if (patient) {
// patient is validated and typed
console.log(patient.name);
} else {
console.error('Invalid patient data');
}
// Option 2: Use Zod directly for detailed errors
const result = USCorePatientSchema.safeParse(unknownData);
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.issues);
}
// Option 3: Use profile validation method
try {
const validated = USCorePatient.fromObject(unknownData);
console.log("Valid profile:", validated);
} catch (error) {
console.error("Profile validation failed:", error);
}
Output Modes
Interface Mode (Default)
Generates plain TypeScript interfaces with Zod schemas:
export interface Patient {
resourceType: 'Patient';
// ... fields
}
Class Mode
Generates ES6 classes:
export class Patient {
resourceType: 'Patient';
// ... fields
}
Class with Builder Mode
Generates classes with a fluent builder API:
export class Patient {
resourceType: 'Patient';
// ... fields
static builder(): PatientBuilder {
return new PatientBuilder();
}
}
const patient = Patient.builder()
.withName({ family: 'Smith', given: ['John'] })
.withBirthDate('1980-01-01')
.build();
Best Practices
Type Safety
- Use generated type guards for runtime validation
- Leverage TypeScript’s discriminated unions for resource types
- Use Zod schemas for external data validation
Performance
- Set
max_valueset_codesto avoid extremely large union types - Use
output_structure = "by_package"for large codebases to improve IDE performance - Disable unused features (e.g.,
generate_profiles = falseif not using profiles)
Extension Handling
- Use typed accessors for simple value-based extensions
- Use raw accessors for complex extensions with nested structure
- Use both when you need flexibility
Validation Strategy
- Use Zod schemas for user input and external APIs
- Use type guards for narrowing types at runtime
- Consider disabling Zod for internal-only code to reduce bundle size
Next Steps
- Extending Backends - Create custom language backends
- Template Overlays - Customize generated code
- Profiles - Working with FHIR profiles
Extending Backends
Create custom code generators by extending InkGen’s backend architecture.
Backend Structure
A backend is a module that implements the Backend interface:
export interface Backend {
name: string;
generate(ir: IntermediateRepresentation): GeneratedCode[];
validate(): ValidationResult;
}
Creating a Custom Backend
1. Implement the Backend Interface
import { Backend, IntermediateRepresentation } from '@octofhir/inkgen';
export class MyBackend implements Backend {
name = 'mybackend';
generate(ir: IntermediateRepresentation) {
// Your generation logic
return [];
}
validate() {
return { valid: true };
}
}
2. Register the Backend
import { registerBackend } from '@octofhir/inkgen';
import { MyBackend } from './my-backend';
registerBackend(new MyBackend());
3. Use in Configuration
{
"backends": ["mybackend"]
}
Example: Python Backend
Create a simple Python backend:
export class PythonBackend implements Backend {
name = 'python';
generate(ir: IntermediateRepresentation) {
return ir.profiles.map(profile => ({
path: `${profile.name}.py`,
content: `
class ${profile.name}:
def __init__(self):
pass
`
}));
}
validate() {
return { valid: true };
}
}
Best Practices
- Handle all FHIR types - Support all types in the IR
- Generate idiomatic code - Follow language conventions
- Include validation - Add runtime type checking
- Document output - Generate comments explaining types
- Test thoroughly - Validate against sample data
For more details on the IR structure, see IR Design.
Advanced Topics
Explore advanced features and customization options in InkGen.
Overview
This section covers:
- Template Overlays - Customize code generation with overlays
- Profile Customization - Advanced profile design patterns
- Performance Tuning - Optimize your build process
When to Use Advanced Features
Consider advanced features when you need to:
- Customize generated code structure
- Optimize generation performance
- Apply complex profile constraints
- Integrate with custom workflows
Quick Links
- Getting Started - Start here if you’re new
- Architecture - Understand how InkGen works
- Contributing - Help improve InkGen
Next Steps
Choose a topic from the list above, or return to the main documentation.
Template Overlays
Template overlays allow you to customize how InkGen generates code for specific FHIR resources or profiles.
Overview
Overlays provide a way to:
- Customize generated code structure
- Add custom annotations
- Control naming conventions
- Extend generated types with additional properties
Creating an Overlay
Create a overlays.yaml file in your project:
overlays:
Patient:
custom_class_name: "PatientProfile"
generate_interfaces: true
add_validation: true
Observation:
custom_class_name: "ObservationResult"
generate_builder_pattern: true
Using Overlays
Reference the overlay file in your configuration:
{
"overlaysFile": "./overlays.yaml"
}
Common Overlay Options
| Option | Type | Description |
|---|---|---|
custom_class_name | string | Override the generated class name |
generate_interfaces | boolean | Generate interface definitions |
add_validation | boolean | Add validation methods |
generate_builder_pattern | boolean | Generate builder pattern |
Examples
See Profile Customization for more advanced customization examples.
Profile Customization
Learn how to customize FHIR profiles to meet your specific requirements.
Profile Basics
A profile constrains a FHIR resource to add domain-specific requirements.
Extending Resources
Profile: CustomPatient
Parent: Patient
Title: "Custom Patient Profile"
// Add a new extension
* extension contains
race 0..1 MS and
ethnicity 0..1 MS
// Constrain cardinality
* name 1..* MS
* birthDate 1..1 MS
Using Extensions
Extension: PatientRace
Id: patient-race
Title: "Patient Race"
Description: "The patient's race"
* value[x] only CodeableConcept
Slicing
Slice elements to distinguish between different types:
Profile: ComplexObservation
Parent: Observation
* component ^slicing.discriminator.type = #pattern
* component ^slicing.discriminator.path = "code"
* component ^slicing.ordered = false
* component ^slicing.rules = #open
* component contains
systolic 0..1 and
diastolic 0..1
* component[systolic].code = $LOINC#8480-6
* component[diastolic].code = $LOINC#8462-4
Best Practices
- Keep profiles focused - Each profile should represent a single clinical concept
- Document constraints - Explain why constraints exist
- Test thoroughly - Validate profiles against sample data
- Version your profiles - Use semantic versioning for profile changes
See Performance Tuning for optimization tips.
Performance Tuning
Optimize InkGen’s performance for large projects.
Build Performance
Parallel Processing
Enable parallel processing in your configuration:
{
"parallel": true,
"maxWorkers": 4
}
Incremental Generation
Use incremental generation to only process changed files:
npx inkgen generate --incremental
Generated Code Performance
Code Splitting
Split generated code into multiple modules:
{
"codeSplitting": {
"enabled": true,
"moduleSize": 50000
}
}
Tree Shaking
Enable tree shaking in your bundler configuration to remove unused code:
// webpack.config.js
export default {
mode: 'production',
optimization: {
usedExports: true,
}
};
Caching
Enable caching to speed up subsequent builds:
{
"cache": {
"enabled": true,
"directory": "./.inkgen-cache"
}
}
Benchmarking
Profile your generation process:
npx inkgen generate --profile
This will output timing information for each phase of code generation.
Tips and Tricks
- Use selectors - Only generate code for profiles you need
- Lazy load - Generate code on-demand rather than upfront
- Compress output - Enable compression for generated files
- Monitor memory - Use Node.js heap snapshots to identify leaks
For more information, refer to the Architecture section.
Troubleshooting
Common issues and solutions when using InkGen.
Build Errors
“Profile not found” Error
Problem: InkGen reports that a profile or resource cannot be found.
Solution:
- Check that the profile file exists and is in the input directory
- Verify the spelling of the profile name
- Ensure all imports are correct:
import { Profile } from ... - Check FHIR version compatibility
“Invalid cardinality” Error
Problem: InkGen rejects the cardinality specification.
Solution:
- Use correct format:
min..max - Examples:
0..1,1..*,0..* - Ensure min ≤ max
Memory Issues
Problem: InkGen runs out of memory on large profile sets.
Solution:
- Increase Node.js heap:
NODE_OPTIONS="--max-old-space-size=4096" npm run build - Split profiles into smaller batches
- Use incremental generation:
--incremental
TypeScript Generation Issues
Type Not Found
Problem: Generated code references undefined types.
Solution:
- Ensure FHIR type definitions are installed
- Check
tsconfig.jsonincludes:@types/fhir - Verify imports in generated files
Validation Function Errors
Problem: Validation functions fail at runtime.
Solution:
- Update FHIR libraries to latest version
- Check that input data matches profile constraints
- Enable debug logging:
INKGEN_DEBUG=true
Performance Issues
Slow Generation
Problem: Code generation takes too long.
Solution:
- Enable parallelization:
"parallel": truein config - Use incremental builds
- Profile the build:
--profile - Check disk I/O performance
Getting Help
- Check the Documentation
- Review Examples
- Open an issue on GitHub
- Check the Roadmap for planned features
Common Patterns
See Advanced Topics for solutions to common problems.
Contributing
We welcome contributions to InkGen! This guide will help you get started.
Getting Started
- Fork the repository
- Clone your fork:
git clone https://github.com/YOUR-USERNAME/inkgen.git - Create a feature branch:
git checkout -b feature/my-feature - Follow the Development Setup
Development Setup
Prerequisites
- Rust stable toolchain (edition 2024)
justcommand runnercargo-instafor snapshot testing (optional)
Install Dependencies
git clone https://github.com/octofhir/inkgen.git
cd inkgen
just bootstrap
Build
cargo build --all
Test
just test
Or run tests for a specific crate:
cargo test -p inkgen-typescript
Lint and Format
just review
This runs formatting, clippy linting, and all tests.
Making Changes
Code Style
- Follow Rust conventions (use
rustfmt) - Use meaningful variable and function names
- Add documentation comments (
///) to public APIs - Keep functions focused and relatively short
- Add unit tests for new features
Commit Messages
Use conventional commits format:
feat: add new feature
fix: fix bug
docs: update documentation
test: add tests
chore: update dependencies
Pull Requests
- Ensure
just reviewpasses locally - Push your branch to your fork
- Create a PR against
main - Describe your changes clearly
- Fill out the PR template
- Ensure all CI checks pass
- Request review from maintainers
For detailed guidelines on adding language backends, see the main CONTRIBUTING.md in the repository root.
Reporting Issues
Use GitHub Issues to report bugs or suggest features:
- Check existing issues first
- Provide a minimal reproduction case
- Include environment details
- Attach relevant files
Code of Conduct
Please be respectful and constructive in all interactions.
Adding a New Language Backend
InkGen is designed to be extensible. To add a new language backend:
- Create a new crate in
crates/inkgen-<language> - Implement the
LanguageGeneratortrait frominkgen-core - Add configuration support
- Integrate with the CLI
- Write comprehensive tests
For detailed step-by-step instructions, see the CONTRIBUTING.md in the repository root.
Questions?
- Ask in GitHub Discussions
- Check the Documentation
- Review Architecture
- See the main CONTRIBUTING.md for detailed guidelines
Thank you for contributing!
Roadmap
InkGen’s development roadmap and planned features.
Current Version (0.1.0)
- ✅ FSH Parser
- ✅ Semantic Analysis
- ✅ TypeScript Backend
- ✅ FHIR R4 Support
Planned Features
Version 0.2.0
- FHIR R5 Support
- Java Backend
- Python Backend
- JSON Schema Generation
- Performance Optimizations
Version 0.3.0
- GraphQL Schema Generation
- OpenAPI/Swagger Support
- Test Data Generation
- Profile Validation Tool
- VS Code Extension
Version 0.4.0
- Go Backend
- C# Backend
- Rust Backend
- Advanced Type Narrowing
- Custom Transformation Plugins
Future Ideas
- Web-based IDE
- Profile Marketplace
- Interactive Documentation Generator
- HL7 Integration
- Analytics Dashboard
Contributing
Want to help? Check out the Contributing Guide to get involved.
Feedback
Let us know what features matter most to you:
- Open a GitHub Issue
- Create a Discussion
- Vote on existing feature requests
Release Schedule
- New minor versions: Every 2-3 months
- Bug fixes: As needed
- Major version when significant changes occur