Skip to main content
Version: v0.1.0

Guides

The tasks readers actually arrive with, each one end to end.

Accept JSON and XML without writing the code twice​

Both codecs produce the same model, so the format belongs at the edge of your program and nowhere else. Sniff it once, and everything downstream is format-blind:

import { parseResource, parseResourceXml, readSafety } from "@cosyte/fhir";
import type { ReadResult } from "@cosyte/fhir";

const read = (payload: string): ReadResult =>
payload.trimStart().startsWith("<") ? parseResourceXml(payload) : parseResource(payload);

const fromJson = read('{"resourceType":"Observation","status":"final"}');
const fromXml = read('<Observation xmlns="http://hl7.org/fhir"><status value="final"/></Observation>');

readSafety(fromJson.resource).status; // => "final"
readSafety(fromXml.resource).status; // => "final"

Both readers hand back diagnostics on the same channel, so one logging path covers both.

Decide whether a resource is safe to summarize​

Before a resource is rolled into a summary, a card or a feed, ask whether anything in it changes the meaning of what you are about to show. readSafety answers that in one call, over the whole document rather than its root alone:

import { parseResource, readSafety } from "@cosyte/fhir";

const { resource } = parseResource(
'{"resourceType":"Observation","status":"entered-in-error","code":{"text":"synthetic"}}',
);

const safety = readSafety(resource);

safety.status; // => "entered-in-error"
safety.retracted; // => true
safety.negations; // => ["entered-in-error"]

negations is the authoritative field. The single-valued conveniences beside it, such as status and retracted, describe the resource you handed in, while negations covers every resource inside it too, so a retracted entry in a Bundle is visible there and nowhere else.

Some elements change the meaning of a record without being a status. A patient recorded as deceased, a patient record replaced by another (link), and a subpotent immunization dose are each reported on modifierElements with their location, whatever value they carry, and each makes the resource unsafe to summarize. A medication request's intent is mandatory, so it is surfaced instead: intents pairs the code with its location, and only an intent that is not one of the eight R4 codes refuses.

import { parseResource, readSafety } from "@cosyte/fhir";

const patient = readSafety(
parseResource('{"resourceType":"Patient","deceasedBoolean":true}').resource,
);

patient.modifierElements; // => [{ element: "deceased", location: "Patient.deceasedBoolean" }]
patient.safeToSummarize; // => false

const request = readSafety(
parseResource('{"resourceType":"MedicationRequest","status":"active","intent":"proposal"}')
.resource,
);

request.intents; // => [{ code: "proposal", location: "MedicationRequest.intent" }]
request.safeToSummarize; // => true

An identifier, a name, an address or a contact point can be marked old or temp, and FHIR treats that use as a modifier: an old address is not where the patient lives now. datatypeUses pairs each use with its location, on every identifier of the resource types the safety readout models and on a patient's name, telecom and address, including those of each contact. A readable code leaves the resource safe to summarize, old included, because which entry to show is your decision: nothing is filtered, reordered or marked current for you. A use that is not one of the codes FHIR defines for that datatype, such as "OLD" or "maiden" on an identifier, is located on unreadableDatatypeUses instead and the resource is not safe to summarize.

import { parseResource, readSafety } from "@cosyte/fhir";

const person = readSafety(
parseResource('{"resourceType":"Patient","address":[{"use":"old","city":"Nowhere"}]}').resource,
);

person.datatypeUses; // => [{ code: "old", location: "Patient.address[0].use" }]
person.safeToSummarize; // => true

const unreadable = readSafety(
parseResource('{"resourceType":"Patient","identifier":[{"use":"OLD","value":"SYN-0001"}]}').resource,
);

unreadable.unreadableDatatypeUses; // => ["Patient.identifier[0].use"]
unreadable.safeToSummarize; // => false

What this does not read: a practitioner's identifiers, names and addresses (a practitioner's identifier use is reported on modifierElements instead, whatever its value), a use carried in an extension value, a contained resource of a type the safety readout does not model, such as an organization, and an ended period on an entry whose use is current.

When a summary must refuse rather than warn, assertSafeToSummarize is the executable form of the same rule: it throws for an unhandled modifier, a value the document left ambiguous, and the other shapes that make an affirmative summary unsafe.

Validate against your own profiles​

No profile content is bundled. US Core and vendor profiles are supplied by the caller, which means the library never claims conformance to a version of a profile you did not choose. Load a published StructureDefinition with loadStructureDefinition, or author one in code:

import { defineProfile, parseResource, validateResource } from "@cosyte/fhir";

const localObservation = defineProfile({
url: "http://example.org/StructureDefinition/local-observation",
name: "LocalObservation",
type: "Observation",
differential: [{ path: "Observation.subject", min: 1, max: 1, mustSupport: true }],
});

const { resource } = parseResource(
'{"resourceType":"Observation","status":"final","code":{"text":"synthetic"}}',
);

const outcome = validateResource(resource, { profiles: [localObservation] });

outcome.valid; // => false
outcome.issues.map((issue) => issue.code); // => ["MUST_SUPPORT_ABSENT", "CARDINALITY_MIN"]

Two things to read off that result. Must-support is reported as informational, because must-support is an obligation on a system, not a requirement that an instance carry the element. The cardinality finding is the error, and it is the one that moves the verdict.

You do not need a profile for the constraints R4 itself declares. On the eight modeled resource types, validateResource evaluates them with no options at all: a patient contact with no name, telecom, address or organization breaks pat-1, an extension with both a value and nested extensions breaks ext-1, and an element with no value and no children breaks ele-1:

import { parseResource, validateResource } from "@cosyte/fhir";

const { resource } = parseResource('{"resourceType":"Patient","contact":[{"gender":"other"}]}');

const outcome = validateResource(resource);

outcome.valid; // => false
outcome.issues.map((issue) => issue.constraint); // => ["pat-1"]

A profile that carries one of those constraints again does not double the finding: each violation is reported once per occurrence. And a profile carrying R4's dom-3 as written, as every R4-derived snapshot does, draws no finding for it on a resource with nothing contained, because that constraint holds there by its own terms.

A starter kit of small, specification-grounded profiles ships with the package as worked examples. They are built through the same public authoring call you would use, so there is nothing privileged about them:

import { STARTER_PROFILES, parseResource, validateResource } from "@cosyte/fhir";

const { resource } = parseResource(
'{"resourceType":"Observation","status":"final","code":{"text":"synthetic"}}',
);

const outcome = validateResource(resource, { profiles: [...STARTER_PROFILES] });

outcome.issues.some((issue) => issue.code === "CARDINALITY_MIN"); // => true

Check codes without a terminology server​

Terminology checks are content-free by default. The library knows code system identities, not their contents, so with no service supplied it verifies that a coding names a system it recognises and that a binding's strength is respected, and it never invents a membership answer:

import { isKnownSystem, LOINC_SYSTEM } from "@cosyte/fhir";

isKnownSystem(LOINC_SYSTEM); // => true
isKnownSystem("http://example.org/not-a-code-system"); // => false

Supply a terminology service through the validation options when you need real membership answers. Findings that came from a service can carry the code system release the answer was made against, so a verdict can be re-read later against the version that produced it.

Preserve what you cannot interpret​

A read is lenient and a write is conservative, which together give a useful property: a document you do not fully understand still round-trips. An element the reader did not expect is preserved and reported rather than dropped:

import { parseResource, serializeResource, validateResource } from "@cosyte/fhir";

const source =
'{"resourceType":"Observation","status":"final","code":{"text":"synthetic"},"vendorField":"kept"}';
const { resource } = parseResource(source);

validateResource(resource).issues.map((issue) => issue.code); // => ["UNKNOWN_ELEMENT"]
serializeResource(resource) === source; // => true

UNKNOWN_ELEMENT is a warning on a lenient read and an error in strict mode, which is the mode to use when you are the one emitting a document rather than the one receiving it.

If the write cannot be done without inventing or losing content, it is refused with a coded reason instead. Troubleshooting lists the refusals and what each one means.