Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Overview

FHIR Specifications
       ↓
    Fetch & Cache
       ↓
IR Construction & Resolution
       ↓
Language-Specific Code Generation
       ↓
Formatted Code Output

InkGen operates in stages:

  1. Fetch: Download and cache FHIR specification packages from canonical registries
  2. Parse: Convert FHIR StructureDefinitions into an Internal Representation (IR)
  3. Resolve: Build genealogy chains, resolve inheritance, and validate constraints
  4. Generate: Render language-specific code from templates
  5. Output: Format and write code to your project

Support

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)
  • just command 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

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.

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

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

OptionTypeDescription
inputstringDirectory containing FHIR Shorthand files
outputstringDirectory for generated code
backendsarrayList of code backends to use
fhirVersionstringFHIR version (r4, r5, etc.)
strictbooleanEnable 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 directory
  • INKGEN_OUTPUT - Output directory
  • INKGEN_BACKEND - Default backend
  • FHIR_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

  1. Separation of Concerns: FHIR parsing, IR construction, and code generation are independent
  2. Extensibility: Add new language backends without modifying core
  3. Performance: Caching, parallel processing, and lazy evaluation where possible
  4. 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 TypeJavaScriptJavaNotes
stringstringStringText data
booleanbooleanBooleanTrue/false
integernumberintWhole numbers
decimalnumberBigDecimalPrecise decimals
dateDateLocalDateCalendar date
dateTimeDateZonedDateTimeWith timezone
codestringStringCoded value

FHIR Complex Types

FHIR TypeDescription
CodeableConceptCoding with text
CodingCode system reference
IdentifierBusiness identifier
QuantityNumeric value with units
PeriodStart and end time
RatioNumeric 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:

  1. Receive the parsed IR and type information
  2. Render templates (usually Tera or language-specific)
  3. 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

OptionTypeDefaultDescription
output_dirstring“./generated”Output directory for generated files
modestring“interface”Generation mode (interface, class, class_with_builder)
naming_conventionstring“pascal”Field naming style (pascal, camel, snake)
structural_guardsbooleantrueGenerate type guard functions
generate_profilesbooleantrueGenerate profile types
generate_valuesetsbooleantrueGenerate valueset types
max_valueset_codesnumbernoneSkip valuesets with more codes than this limit
output_structurestring“flat”Output structure (flat, by_package)

Profile Method Options

OptionTypeDefaultDescription
extension_accessorsbooleantrueGenerate extension getter/setter methods
extension_stylestring“both”Extension accessor style: “typed”, “raw”, or “both”
serializationbooleantrueGenerate toJson/toObject methods
validationbooleantrueGenerate fromJson/fromObject methods
zod_schemasbooleantrueGenerate 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_codes to avoid extremely large union types
  • Use output_structure = "by_package" for large codebases to improve IDE performance
  • Disable unused features (e.g., generate_profiles = false if 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 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

  1. Handle all FHIR types - Support all types in the IR
  2. Generate idiomatic code - Follow language conventions
  3. Include validation - Add runtime type checking
  4. Document output - Generate comments explaining types
  5. 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:

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

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

OptionTypeDescription
custom_class_namestringOverride the generated class name
generate_interfacesbooleanGenerate interface definitions
add_validationbooleanAdd validation methods
generate_builder_patternbooleanGenerate 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

  1. Keep profiles focused - Each profile should represent a single clinical concept
  2. Document constraints - Explain why constraints exist
  3. Test thoroughly - Validate profiles against sample data
  4. 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

  1. Use selectors - Only generate code for profiles you need
  2. Lazy load - Generate code on-demand rather than upfront
  3. Compress output - Enable compression for generated files
  4. 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:

  1. Check that the profile file exists and is in the input directory
  2. Verify the spelling of the profile name
  3. Ensure all imports are correct: import { Profile } from ...
  4. 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:

  1. Increase Node.js heap: NODE_OPTIONS="--max-old-space-size=4096" npm run build
  2. Split profiles into smaller batches
  3. Use incremental generation: --incremental

TypeScript Generation Issues

Type Not Found

Problem: Generated code references undefined types.

Solution:

  1. Ensure FHIR type definitions are installed
  2. Check tsconfig.json includes: @types/fhir
  3. Verify imports in generated files

Validation Function Errors

Problem: Validation functions fail at runtime.

Solution:

  1. Update FHIR libraries to latest version
  2. Check that input data matches profile constraints
  3. Enable debug logging: INKGEN_DEBUG=true

Performance Issues

Slow Generation

Problem: Code generation takes too long.

Solution:

  1. Enable parallelization: "parallel": true in config
  2. Use incremental builds
  3. Profile the build: --profile
  4. Check disk I/O performance

Getting Help

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

  1. Fork the repository
  2. Clone your fork: git clone https://github.com/YOUR-USERNAME/inkgen.git
  3. Create a feature branch: git checkout -b feature/my-feature
  4. Follow the Development Setup

Development Setup

Prerequisites

  • Rust stable toolchain (edition 2024)
  • just command runner
  • cargo-insta for 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

  1. Ensure just review passes locally
  2. Push your branch to your fork
  3. Create a PR against main
  4. Describe your changes clearly
  5. Fill out the PR template
  6. Ensure all CI checks pass
  7. 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:

  1. Check existing issues first
  2. Provide a minimal reproduction case
  3. Include environment details
  4. 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:

  1. Create a new crate in crates/inkgen-<language>
  2. Implement the LanguageGenerator trait from inkgen-core
  3. Add configuration support
  4. Integrate with the CLI
  5. Write comprehensive tests

For detailed step-by-step instructions, see the CONTRIBUTING.md in the repository root.

Questions?

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