Skip to main content
Version: v0.0.7

De-identifying FHIR

The @cosyte/deid/fhir adapter is the FHIR R4 binding of the de-identification core. It locates PHI structurally in a parsed FHIR resource - a name under a Patient is the patient's name because the FHIR spec says so, never because a string "looked like" a name - applies the configured policy, and returns a transformed resource model plus the core's value-free manifest.

@cosyte/fhir is an optional peer dependency. Install it alongside @cosyte/deid to use this subpath; a consumer who only de-identifies FHIR pays for nothing else, and the core stays third-party-dependency-free. The adapter reaches FHIR data only through @cosyte/fhir's exported model (FhirComplex / FhirList / FhirPrimitive, getProperty, resourceType, the node constructors) and its parseResource / serializeResource codec - it never imports a third-party JSON substrate.

npm install @cosyte/deid @cosyte/fhir

Quickstart

import { parseResource, serializeResource } from "@cosyte/fhir";
import { deidentifyFhir } from "@cosyte/deid/fhir";
import { createDeidContext } from "@cosyte/deid";

const context = createDeidContext({ key: process.env.DEID_KEY! });
const { resource } = parseResource(json);
const { document, manifest } = deidentifyFhir(resource, { context });

serializeResource(document); // spec-clean, de-identified FHIR JSON
manifest; // value-free audit: category + locus + disposition, never a value

A convenience wrapper parses, de-identifies, and re-serializes in one call:

import { deidentifyFhirJson } from "@cosyte/deid/fhir";
import { createDeidContext } from "@cosyte/deid";

const { json, manifest } = deidentifyFhirJson(input, {
context: createDeidContext({ key: process.env.DEID_KEY! }),
});

A keyed transform (identifier pseudonymization) requires a context; calling without one when the resource needs it is a fatal DEID_NO_KEY - the engine never falls back to an unkeyed surrogate.

What is located, and how it is transformed

FHIR is a graph of typed resources, so the locus map splits by resource role. A name / address / telecom is PHI inside a person resource; the same datatype in Location or Organization is facility/administrative data and is left to the clinical-retain path - the FHIR analogue of the C-CDA adapter sweeping only the header participations, never the clinical body.

ScopeLociTransform
Person resources - Patient / RelatedPerson / Practitioner / Person (+ nested Patient.contact, a relative)name, telecom, photo, address, birthDate, deceasedDateTimename/telecom/photo removed; address → safe 3-digit ZIP (or 000 for a restricted prefix), finer geography dropped; dates → year
Every resource (the universal PHI vectors)identifier, PHI-bearing dates, narrative text.div, extension / modifierExtension values, Reference.displayidentifier → consistent surrogate by system (keyed HMAC; a US-SSN system removed); dates → year; narrative div / extension values / reference labels blocked
Contained resources & Bundle entrieseach nested resourcewalked - the resource role is re-derived at every resourceType, so a contained RelatedPerson or a Bundled Patient is de-identified too
Clinical resources - Observation, Condition, Encounter, …codes, values, units, statuses, reference ranges, reference wiringretained untouched (the over-scrub guard)

An identifier's Safe Harbor category is read from its system URI - the US-SSN system (http://hl7.org/fhir/sid/us-ssn or its OID form) routes to removed, every other identifier to a consistent surrogate with the system retained. A Reference.display (usually a person's name) is blocked, while a Coding.display (a coded term such as Sodium) is retained - the two are distinguished structurally, not by the property name.

Dates are detected by value shape: any primitive whose whole value is a real calendar date (YYYY-MM, YYYY-MM-DD, or a full instant) with a valid month/day is generalized to its year, wherever it sits - so a date in an unexpected element is caught too. A bare four-digit year is already Safe-Harbor-safe and is left as-is; a clinical code that merely looks date-ish (2951-2, 1234-56) is not mistaken for a date, so it survives.

Fail closed

  • A bare unrecognized string at a person resource's top level is blocked - a positive allow-list of recognized coded/administrative elements governs the person sweep, so a vendor <Patient>-level field cannot ride through in the clear (an open-ended allow-list can never satisfy Safe Harbor's open-ended category (R)).
  • A display that is not on a Coding is a Reference person-label and is blocked - including a display-only ({ display }) or type+display reference that names no reference/identifier target. A Coding is identified positively (a code/system sibling), so Coding.display (a coded term) is retained; every other display fails closed.
  • Every extension value is dropped - a complex valueAddress / valueHumanName / valueIdentifier, a modifierExtension, a deeply nested extension, and a primitive-level _-sibling extension alike. Extensions are the FHIR leak frontier; the url skeleton is kept, the payload is not.
  • Free-text prose is blocked by default - the note element (Annotation.text + author), a Communication/message contentString, and an uncoded valueString (the FHIR analogue of an HL7 OBX-5 typed ST, which the sibling HL7 adapter also fails closed on; a structured valueQuantity / valueCodeableConcept / valueDateTime result is retained).
  • The narrative text.div is blocked at any depth (resource-, section-, entry-level).

The two guarantees

  • No leak. Every seeded PHI sentinel across the person resources, the universal vectors, the nested contact relative, and a contained resource is gone from the serialized output. An unmapped element or extension that could carry PHI is blocked, never passed through in the clear.
  • No over-scrub. Clinical resources - observation and medication codes, values, units, result statuses, reference ranges, coded displays - are retained, and reference wiring (Reference.reference pointers) is preserved, so linkage survives identifier pseudonymization.

Known limitations (this release)

  • Extension values are block-only - there is no profile-aware retention, so a us-core-* demographic extension is dropped rather than kept.
  • Reference wiring and resource logical ids are preserved structurally; coordinated pseudonymization of resource ids across a corpus (so the same patient links across documents) is not performed.
  • Free-text prose loci (note, contentString, uncoded valueString) fail closed by default; a semantic (NLP) narrative scrub, contentAttachment binary content, and person names embedded in non-person resources (Organization.contact.name, Location.address) remain out of scope.

The honesty line is unchanged: the output is "Safe-Harbor-transformed per the configured policy," never "de-identified" and never "HIPAA-compliant."