Current limits
What the package does not do, in its own words. Everything here is a deliberate boundary, and each one is stated because working around a limit you know about is cheap, while discovering one in production is not.
The gaps, in the order the README lists them under "Not covered yet":
- typed per-resource models;
typeandprofileslicing discriminators, reportedPROFILE_SLICE_UNCHECKED, and FHIRPath outside the subset we evaluate, reportedINVARIANT_UNCHECKED(verdicts that say "not checked");- bundled terminology or US Core content;
- validation of R4B, R5 and DSTU2 content against its own version's definitions;
- spec-clean JSON written from a model read out of XML;
- unit conversion;
- executing a transaction Bundle.
No terminology content is bundled
The library knows code system identities, not their contents. There is no bundled SNOMED CT, LOINC, CPT, RxNorm, or ICD content, and there never will be: those are licensed distributions, and vendoring them into a parser is both a licensing problem and a staleness problem.
What that means in practice:
- a coding is checked for a system this library recognizes, and for the binding strength the element declares;
- membership of a code in a value set is not answered unless you supply a terminology service;
- with no service supplied, the checks degrade to that content-free level and never produce a false error. Silence about membership is not a claim of validity.
No profile content is bundled
US Core and vendor StructureDefinitions are caller-supplied. The package validates against
whatever profiles you hand it, with snapshot generation, slicing, fixed and pattern values, and
must-support treated as a system obligation rather than an instance requirement.
The consequence to plan for: "validated" here means "validated against the profiles you supplied". The library never asserts conformance to a version of US Core you did not pass in, because the alternative is a claim about an implementation guide release that may not be the one your trading partner is on.
No typed per-resource models
There is no generated TypeScript interface per resource type, and no Patient or Observation
class. The model is schema-free, and elements are reached by name through small helpers.
This is the cost side of the trade that makes a lenient read safe: a schema-free tree cannot drop an
element it has no field for, but it also cannot offer compile-time completions for a resource's
elements. If you want typed access to a handful of elements, wrap the helpers in your own accessors
at the edge of your program. If you want the compile-time shape of a whole resource,
Type a parsed resource with the community R4 types types a
plain JSON copy with @types/fhir, and says what that copy loses: exact decimals become JavaScript
numbers.
Verdicts that say "not checked" rather than passing
Each of these is a finding the validator emits rather than silently letting something through. If you treat any non-error finding as a pass, you will misread them.
INVARIANT_UNCHECKED: a constraint whose FHIRPath expression is outside the bounded expression subset this library evaluates, whether a profile supplied it or R4 declares it on the resource type. It is reported as informational, and it means the invariant was not evaluated, not that it held. With no profile, the one base constraint reported this way isdom-3on a resource that carries a contained resource: checking that every contained resource is referenced needs reference resolution the subset does not have. With nothing contained,dom-3holds by its own terms and nothing is reported, also when a supplied profile carries R4'sdom-3as written. A profile's constraint under that key written any other way is its own expression, and is reported unchecked like any other the subset cannot evaluate.PROFILE_SLICE_UNCHECKED: a slice whose discriminator this library cannot apply: atypeorprofilediscriminator, or thepositiondiscriminator, which is not part of the R4 discriminator set. Again, it means the slice membership was not decided, not that it matched. A slice's own constraints apply only to the occurrences assigned to it, so under an undecided slicing they are not checked either.PROFILE_SLICE_NO_SLICING: a profile naming a slice (Observation.component:coded) under an element that declares noslicing, or that is missing from the snapshot. Nothing in the profile says which occurrences belong to the slice, so none is assigned to it, and the slice's own constraints are not checked. It is reported at information, once per present parent.
All of them exist because the alternative, staying quiet, is indistinguishable from a pass to every consumer downstream.
R4 first
FHIR R4 (4.0.1) is the modeled target. R4B (4.3.0), R5 and DSTU2 documents can be read, because
the model is schema-free, but they are not validated against their own definitions. Treat
cross-version reads as data recovery, not conformance.
Nothing in a document tells the package which version it is: no read looks at a fhirVersion, a
MIME type parameter or a profile URL to pick a version. So an R4B resource is read and validated as
if it were R4:
- its values are read exactly as with R4, every
decimalkept as written, and the safety readout reads its status, modifiers and negations by the same rules; - the safety readout does not assess a resource of a type it does not model, including one R4B
added: it lists the resource on
notAssessedand never calls it safe to summarize, whatever status it carries, and it finds a resource by its position alone, so aresourceTypewritten on a Bundle entry or another element that is not a resource is listed there too (a resource type the readout does not model); - a resource of one of the eleven types we model (
Patient,Observation,DiagnosticReport,Condition,MedicationRequest,AllergyIntolerance,Immunization,MedicationStatement,Encounter,ProcedureandDocumentReference) is validated against R4's definition of that type, so an element R4B changed is judged by R4's rule; - a resource of any other type, including one R4B added, gets the information finding
RESOURCE_NOT_MODELED: only the elements every resource has are checked, and none of the type's own; validateResourcereports an unmodeled type only at the top level, so a document can be valid and still decline inreadSafetyfor a type it does not model incontainedor a Bundle entry;- inside a resource of a type the safety readout assesses, every element at every depth is checked
against the union of what R4 and R4B define there, so an element either release defines is read
as written, and one neither defines is listed on
notAssessedand reported asUNKNOWN_ELEMENT(an element the format does not define); - an XML element in a namespace other than its parent's is declined and reported the same way, and
the reader still models it under its local name. Neither writer can spell the namespace, so both
refuse the model (
UNSERIALIZABLE_FOREIGN_NAMESPACE) rather than write the element as FHIR. A document whose root is in a namespace other than FHIR's is listed onnotAssessedby its root's location and refused by both writers (UNSERIALIZABLE_FOREIGN_ROOT); a root in no namespace at all is read as FHIR. So a write and a re-read, in either format, cannot turn a declined element into one that reads safe.
Here a synthetic NutritionProduct, a type R4B added, carries a status outside any code list and an
element no version defines. Validation reports neither, and the safety readout does not assess the
resource, so it is not safe to summarize:
import { parseResource, readSafety, validateResource } from "@cosyte/fhir";
const { resource } = parseResource(
JSON.stringify({
resourceType: "NutritionProduct",
id: "syn-np-0001",
status: "not-a-status",
code: { text: "Synthetic formula" },
notAnElement: "kept as written",
}),
);
const result = validateResource(resource);
result.valid; // => true
result.issues.map((issue) => issue.code); // => ["RESOURCE_NOT_MODELED"]
readSafety(resource).status; // => "not-a-status"
readSafety(resource).notAssessed; // => ["NutritionProduct"]
readSafety(resource).safeToSummarize; // => false
The same status on an Observation, a type we model, is an error:
import { parseResource, validateResource } from "@cosyte/fhir";
const { resource } = parseResource(
'{"resourceType":"Observation","id":"syn-0002","status":"not-a-status","code":{"text":"Synthetic reading"}}',
);
validateResource(resource).issues.map((issue) => issue.code); // => ["CODE_INVALID"]
To hold R4B content to its own rules, pass the R4B definitions you need as profiles: see Validate against your own profiles.
No unit is converted
A quantity is read with its magnitude exactly as written and its UCUM code, and the package never
converts it to another unit, compares two quantities in different units, or computes an
interpretation flag from a reference range. A vital sign's unit is checked against the units R4
allows for it; it is not converted to one of them.
Read measured values and their units shows what is read.
A transaction Bundle is read, never executed
readBundle tells a transaction (all or nothing) from a batch (each entry on its own) and describes
the entries. Nothing in the package applies either: there is no server, no storage and no request.
Read Bundles and resolve references shows what is read.
JSON written from an XML read keeps primitives as text
The XML codec is schema-free like the JSON one, so a primitive read from XML is kept as the text of
its value attribute. serializeResource writes such a model faithfully, but not in spec-clean JSON:
a boolean or a number read from XML is written as a JSON string, because choosing the JSON type
needs the datatype of every element, which the schema-free model does not carry. A model read from
JSON writes back to JSON with its types, and the XML writer takes a model from either read.
import { parseResourceXml, serializeResource } from "@cosyte/fhir";
const { resource } = parseResourceXml(
'<Patient xmlns="http://hl7.org/fhir"><id value="syn-0001"/><active value="true"/></Patient>',
);
serializeResource(resource); // => '{"resourceType":"Patient","id":"syn-0001","active":"true"}'
A narrative is checked, not sanitized
validateResource reads each Narrative.div as XHTML and checks it against the R4 narrative
rules: a single div root in the XHTML namespace, only the elements and attributes R4 allows, no
script, embedding element, event handler or script URL, no block element inside a paragraph, and
some content. Each broken rule is an error at the location of the div, and no finding names
anything written inside it. The string itself is kept exactly as written, and the writers emit it
unchanged.
import { parseResource, validateResource } from "@cosyte/fhir";
const { resource } = parseResource(
JSON.stringify({
resourceType: "Patient",
id: "syn-0003",
text: {
status: "generated",
div: '<div xmlns="http://www.w3.org/1999/xhtml"><p onclick="track()">Synthetic summary</p></div>',
},
}),
);
validateResource(resource).issues.map((issue) => `${issue.code} ${issue.expression}`); // => ["NARRATIVE_ACTIVE_CONTENT Patient.text.div"]
What it does not check:
- where a table or list part may sit, such as a
troutside atableor anlioutside a list; - a URL beyond its scheme, and whether an internal image reference such as
src="#a"points at anything. Only ajavascript:orvbscript:scheme is reported, read the way a browser reads it (a tab or line break inside the scheme does not hide it); adata:URL, includingdata:text/html, is not reported; - the content of a
styleattribute, and duplicateidvalues, so aurl(...)inside astylevalue,javascript:included, is not reported; - an HTML named entity such as
is not read at all: XHTML in FHIR has only the five XML entities, so such a narrative is reportedNARRATIVE_UNREADABLE, as the HL7 reference validator reports it.
A narrative with no finding has passed the rules R4 states, which is not the same as being safe to render. If you display narratives as HTML, run them through your renderer's own sanitizer as well.
Reported, not repaired
Several non-conformant shapes are read, reported, and deliberately not fixed. Character data written directly on an element in XML is the clearest example. FHIR carries a primitive's value in an attribute, so text in that position is non-conformant. On a primitive element the reader takes the text as the value, so the safety readout still sees a status or a dose written that way, and the read is reported: validation raises an error, the resource is not safe to summarize, and both writers refuse it. On a complex element the text has no slot in the model and is dropped, with a finding. Either way the package reports the shape rather than guessing what the sender meant.
The same posture governs the writers. When a model cannot be spelled back into the target format without inventing or losing content, the write is refused with a coded reason instead of emitting something plausible. Troubleshooting covers what each refusal means.
Put it to use
- Check codes without a terminology server shows the content-free checks and where a terminology service of your own plugs in.
- Validate against your own profiles shows how to supply the profiles this package does not bundle.
- Type a parsed resource with the community R4 types shows the typed copy, and which values to keep reading from the model.
- Troubleshooting has an entry for each finding above that means a check did not run.