fml2java is a command line tool to transform FML maps into Java code.
The FML (FHIR Mapping Language) is used to define mappings between different healthcare data formats, such as CDA (Continuity of Care Document) and FHIR (Fast Healthcare Interoperability Resources). FML is specified here: https://build.fhir.org/ig/HL7/mapping-language-ig/en/fml.html
Disclaimer: This implementation has only been tested with Austrian (ELGA) CDA documents – the laboratory reports and immunization status documents in
test/input– and the CDA2FHIR maps of HL7 Austria intest/maps. The supported subset of FML and FHIRPath, the CDA model (ELGA extensions of CDA R2) and the reproduced behaviour of the reference implementation are driven by these maps and documents. Other maps or documents may use constructs that are not supported or behave differently; check the results carefully before using it for anything else.
The CLI tool can be called like this:
java -jar fml2java.jar -m test/maps/CdaToFhirBundle.4.map -o src/main/java/at/nonblocking/CdaToFhirBundle.java -p at.nonblocking
and outputs a single Java Class with the following signature (the class name is taken from the output file name):
package at.nonblocking;
import java.io.InputStream;
import org.hl7.fhir.r4.model.Bundle;
public class CdaToFhirBundle {
/**
* CDA XML 2 FHIR Mapper
*
* @param cda Input CDA XML
* @return FHIR R4 Bundle
* @throws Exception
*/
public Bundle mapCDA2FHIR(InputStream cda) throws Exception {
// ... generated mapping
}
}| Option | Description |
|---|---|
-m, --map <file> |
Entry FML map. Maps it imports are looked up by their canonical URL (map "<url>" = …) in the directory of the entry map. |
-i, --import-path <dir> |
Additional directory to search for imported maps (can be repeated). |
-o, --output <file> |
Java file to generate; the class name is derived from the file name. Missing directories are created. |
-p, --package <name> |
Java package of the generated class. |
--method <name> |
Name of the mapping method (default: derived from the models, mapCDA2FHIR). |
--dump |
Print the resolved maps, groups and default groups instead of generating code (no -o/-p needed). |
-h, --help |
Print the usage. |
Errors in the maps are reported with file:line:column; the exit code is 0 on success, 1 on errors and 2 on
invalid usage.
The generated class is self-contained: the CDA parser, the FHIRPath runtime, the concept maps and the CDA type metadata are embedded. Its only dependency is the HL7 FHIR R4 model in the version used by fml2java:
implementation 'ca.uhn.hapi.fhir:org.hl7.fhir.r4:6.10.4'
// only needed to serialize bundles with org.hl7.fhir.r4.formats.JsonParser (an optional dependency of the R4 model)
implementation 'com.google.code.gson:gson:2.13.1'var mapper = new CdaToFhirBundle();
try (var cda = Files.newInputStream(Path.of("document.xml"))) {
Bundle bundle = mapper.mapCDA2FHIR(cda);
String json = new JsonParser().setOutputStyle(IParser.OutputStyle.PRETTY).composeString(bundle);
}- Thread-safe: the generated class has no mutable state; one instance can be used concurrently (verified by
EndToEndTest.isThreadSafe). - Performance: the 15 test documents are mapped in ~0.4 s including class loading and ~85 ms warm (~6 ms per document). For comparison, the Python code generated by MaLaC-HD needs ~7 s for them (one Python process per document).
- Readable: every rule is preceded by a comment with its FML source position (
// CdaToFhirBundle.4.map:901:3 "rule name").
Requires Java 25 or newer.
./gradlew build # fat jar build/libs/fml2java.jar, unit tests and end-to-end test
./gradlew test # unit tests only
./gradlew e2eTest # end-to-end test only
The end-to-end test (EndToEndTest, see AGENTS.md) generates the mapper for test/maps/CdaToFhirBundle.4.map,
compiles it, transforms all test/input/**/*.xml and compares the bundles with test/output/**/*.json (UUIDs and the
bundle timestamp normalized). Outputs and normalized copies for diffing are written to build/e2e-output.
The reference outputs in test/output are produced by MaLaC-HD 1.8.0. After
changing the maps, regenerate them with:
cd test/python-maps
./bin/malac-hd -m ../maps/CdaToFhirBundle.4.map -co CdaToFhirBundle.4.py -s # requires the virtualenv created by transform.sh
zsh ./transform.sh
The FML subset used by the test maps:
map,uses,imports(also with wildcards),let, inlineconceptmaps (==,!=,<=,>=,~,-)- groups with typed or untyped parameters,
extends,<<types>>default groups - rules with one source:
src.element : Type first|not_first|last|not_last as v where (…);wherealso without parentheses - targets
tgt.element = value as v,tgt.element as v, implicit copies (src.a -> tgt.b), choice elements with type suffix (valueQuantity),tgt = (expr)on primitives - transforms
create,copy(implicit),append,uuid,cast,truncate,translate(code,Coding,CodeableConcept) and FHIRPath expressions - dependent rules
then { … }and group invocationsthen Group(a, b) - FHIRPath: paths, indexers,
$this,%var, literals,and/or/xor/implies,=/!=/</<=/>/>=,+,-,&,|,in,containsand the functionsexists,empty,not,where,count,first,last,descendants,startsWith,endsWith,contains,matches,indexOf,substring,lower,upper,length,toString,now
Not supported (reported as error with the source position): several sources per rule, check, log, default,
only_one, target list modes (share, collate, …), <<type+>> groups, the transforms reference, evaluate, cc,
c, qty, id, cp, dateOp, escape, pointer, FHIRPath type operators (is, as), arithmetic (*, /, div,
mod) and date/time literals.
The source model is CDA R2 with the ELGA extensions (src/main/resources/at/nonblocking/fml2java/cda/cda-types.txt,
extracted from the CDA model of MaLaC-HD with tools/extract_cda_types.py); the target model is FHIR R4.
The generated mappers reproduce the output of MaLaC-HD 1.8.0 for the test data
(see test/), except where MaLaC-HD contradicts the FHIR specification or fails:
- Decimal precision: decimals keep the precision of the input (
1.00, as recommended by FHIR); MaLaC-HD writes the Python float representation (1.0). - Extensions on repeating primitives: the
_xarray is aligned with the value array as required by the FHIR JSON format ("given": ["a", "b"], "_given": [{...}, null]); MaLaC-HD only writes the extensions ("_given": [{...}]). - HL7 v3 timestamps that MaLaC-HD can not parse (times with hours only, fractional seconds, a time zone without
time) or returns unconverted (negative time zones such as
-0500) are converted to valid FHIR date/time values. then Group(...) { rules }: the nested rules run for each source item (FML semantics); MaLaC-HD runs them once after all items, with the values of the last item.Bundle.timestamp(now()) has millisecond precision; MaLaC-HD writes microseconds.
Behaviour of MaLaC-HD that is reproduced although it is unusual: the string value of a CDA element is MaLaC-HD's XML
serialization of it (e.g. ServiceRequest.reasonCode.text), CDA booleans are written as True/False by
cast(…, 'string'), and translating a code with more than one equivalent mapping fails.