Skip to main content
Version: v0.1.16

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":

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 is dom-3 on 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-3 holds by its own terms and nothing is reported, also when a supplied profile carries R4's dom-3 as 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: a type or profile discriminator, or the position discriminator, 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 no slicing, 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 decimal kept 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 notAssessed and never calls it safe to summarize, whatever status it carries, and it finds a resource by its position alone, so a resourceType written 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, Procedure and DocumentReference) 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;
  • validateResource reports an unmodeled type only at the top level, so a document can be valid and still decline in readSafety for a type it does not model in contained or 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 notAssessed and reported as UNKNOWN_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 on notAssessed by 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 tr outside a table or an li outside a list;
  • a URL beyond its scheme, and whether an internal image reference such as src="#a" points at anything. Only a javascript: or vbscript: scheme is reported, read the way a browser reads it (a tab or line break inside the scheme does not hide it); a data: URL, including data:text/html, is not reported;
  • the content of a style attribute, and duplicate id values, so a url(...) inside a style value, javascript: included, is not reported;
  • an HTML named entity such as &nbsp; is not read at all: XHTML in FHIR has only the five XML entities, so such a narrative is reported NARRATIVE_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​