Announcing dicom-validator-ts: DICOM Compliance Checking for Node.js, the Way the Standard Actually Works

Tatsuhiko Arai (新井 竜彦)

Tatsuhiko Arai (新井 竜彦)

· 10 min read
Just an image

Why DICOM validation is harder than it looks

DICOM is the lingua franca of medical imaging. Every CT scan, MRI, and X-ray you've ever seen on a hospital monitor was almost certainly stored in DICOM format. The standard has been around since 1993 and is dense: the full specification runs to thousands of pages across seventeen parts, each with its own numbering scheme and cross-references.

What makes validation tricky is that DICOM isn't just a file format — it's a layered set of rules. A file might be parseable (the bytes decode without error) but still non-compliant (wrong value format in a required attribute, missing mandatory tag for this specific type of image). Tools that only check parseability silently pass files that will cause downstream processing failures in clinical systems.

dicom-validator-ts v0.2.0 is a TypeScript library that goes a level deeper. It validates files against the rules that actually matter: PS3.6 (the Data Dictionary) for tag format and multiplicity, and PS3.3 (Information Object Definitions) for which attributes are required, optional, or conditional on the content of other attributes.

npm install dicom-validator-ts

Three things it validates, and why each one matters

1. VR (Value Representation) format

Every DICOM tag has a declared type — its VR. A date tag (DA) must be exactly YYYYMMDD. A time tag (TM) must follow HHMMSS.FFFFFF. A Person Name tag (PN) has a five-component structure separated by ^. These rules are in PS3.6, and many files produced by real-world imaging devices get them subtly wrong.

The library includes individual validators for each VR and validates format precisely — including month/day bounds and leap years for DA, for example. A tag with 20231332 as a date value gets flagged, not silently passed.

2. VM (Value Multiplicity) constraints

DICOM allows attributes to carry multiple values (separated by \). The standard specifies how many values are permitted per tag: exactly 1, exactly 3, 1 or more, 2-n, etc. A tag declared as VM 3 that contains only two values is non-compliant, but a plain DICOM parser won't complain.

3. IOD (Information Object Definition) compliance

This is the deepest layer. PS3.3 defines IODs — structured descriptions of what a specific type of DICOM object (CT Image, MR Image, Secondary Capture Image, etc.) must contain. Each IOD is assembled from modules (e.g., "Patient Module", "General Equipment Module"), and each module lists its attributes with a type:

  • Type 1 — required, must have a value (violation → error)
  • Type 2 — required, may be empty (absence → warning)
  • Type 3 — optional
  • Type 1C / 2C — required if a condition holds

That last category — conditional attributes — is where most validators give up. dicom-validator-ts evaluates conditions using a parsed AST. The condition "Required if Photometric Interpretation is MONOCHROME1 or MONOCHROME2" is stored internally as:

{
  "type": "or",
  "conditions": [
    { "type": "tag_equals", "tag": "(0028,0004)", "value": "MONOCHROME1" },
    { "type": "tag_equals", "tag": "(0028,0004)", "value": "MONOCHROME2" }
  ]
}

The ConditionEvaluator walks this AST against the live dataset at validation time, so conditional rules get correctly enforced rather than skipped.

Quick start: programmatic API

import { validate } from 'dicom-validator-ts';

const result = await validate('/path/to/scan.dcm');

if (result.passed) {
  console.log('Validation passed');
} else {
  console.log(`${result.summary.errors} errors, ${result.summary.warnings} warnings`);
  for (const finding of result.findings) {
    console.log(`[${finding.severity}] ${finding.tag}: ${finding.message}`);
  }
}

You can also pass a Buffer or ArrayBuffer directly — useful when the file is already in memory or comes from a network stream:

import { validate } from 'dicom-validator-ts';
import { readFileSync } from 'node:fs';

const buffer = readFileSync('/path/to/scan.dcm');
const result = await validate(buffer);

Tuning what gets checked

All three check categories are on by default, but each can be disabled independently:

const result = await validate(file, {
  checks: {
    vr: true,
    vm: true,
    iod: false,   // skip IOD compliance for this run
  },
  verbosity: 'errors-only',   // suppress warnings and info findings
  sopClassUID: '1.2.840.10008.5.1.4.1.1.2',  // override auto-detection
});

The sopClassUID override is worth calling out. Auto-detection reads tag (0008,0016) from the file. If your pipeline is pre-processing files and that tag hasn't been written yet, you can tell the validator which IOD to check against explicitly.

Working with results

const result = await validate('/path/to/scan.dcm');

// summary counts
console.log(result.summary);  // { errors: 2, warnings: 5, infos: 0 }

// filter by severity
const errors   = result.getFindings('error');
const warnings = result.getFindings('warning');

// serialize
const json = result.toJSON();

CLI usage

The package ships a dicom-validator binary, so you can use it without writing any code:

# single file
npx dicom-validator-ts scan.dcm

# batch
npx dicom-validator-ts *.dcm

# JSON output (useful for piping into jq or a report tool)
npx dicom-validator-ts scan.dcm --format json

# suppress everything except errors
npx dicom-validator-ts scan.dcm --quiet

# override SOP Class
npx dicom-validator-ts scan.dcm --sop-class 1.2.840.10008.5.1.4.1.1.2

Exit codes follow the Unix convention: 0 for clean, 1 for validation failures, 2 for usage errors. That means it drops straight into CI pipelines or pre-upload validation hooks with no extra scripting.

Under the hood: a few things worth knowing

Dictionary data is bundled, not fetched. iods.json, modules.json, and tags.json are static files checked into the package derived from the DICOM standard. A DictionaryLoader singleton lazy-parses them on first use and caches the resulting ASTs — no network call, no startup penalty after the first validation in a process.

Dual ESM + CommonJS output. The build produces dist/index.mjs and dist/index.cjs, so the library works in both modern ESM-only environments and older CommonJS setups without any configuration gymnastics on the caller's side.

The DICOM standard uses SOP Classes to identify what an object is. The SOP Class UID — stored in tag (0008,0016) — is essentially a UUID pointing at a specific IOD definition. There are hundreds of them, covering everything from CT Image Storage (1.2.840.10008.5.1.4.1.1.2) to Digital Mammography X-Ray to Ophthalmic Tomography. The library ships IOD definitions for the commonly encountered classes, and the current test suite uses CT Image Storage as its reference implementation.

Property-based tests catch format edge cases automatically. The VR validators are covered with @fast-check/vitest property tests alongside hand-written unit tests. This matters for VRs like DA and TM, where the valid/invalid boundary is defined by date arithmetic rules rather than a simple regex — and where a generator can find 20000229 (valid, leap year) vs 19000229 (invalid, century not divisible by 400) before a human would think to write that case.

The short version

  1. npm install dicom-validator-ts
  2. Call validate(filePath) — it returns a ValidationResult with passed, findings, and summary.
  3. Use --format json in the CLI to integrate with automated pipelines.
  4. Three independent check categories (VR, VM, IOD) can be toggled per call.
  5. Conditional attributes (Type 1C/2C) are evaluated against a real condition AST, not skipped.
  6. Node.js 18+, MIT license.

A few things worth internalizing

  • Parseability ≠ compliance. A file that opens in a DICOM viewer can still contain hundreds of standard violations. Validation matters most at the point where files enter a system — before they're stored, routed, or processed.
  • The IOD layer is where the real domain knowledge lives. VR and VM checks are mechanical; IOD compliance requires knowing which type of object a file claims to be and what that implies about required content.
  • Conditional attributes are not optional. A naive validator that skips Type 1C/2C rules will silently pass files that clinical systems will reject or mishandle. Evaluating conditions requires reading the dataset itself — which is why most tools don't bother.
  • Dual ESM + CJS still matters. The Node.js ecosystem has not fully converged on ESM. Shipping both formats from a single package avoids forcing consumers to change their module resolution config just to import a utility library.

About Tatsuhiko Arai (新井 竜彦)

Embedded software engineer (Qt, C/C++, Python). Medical imaging (DICOM) contractor. AWS All Certifications Engineer – Japan (2024–2025).

Copyright © 2026 Tatsuhiko Arai. All rights reserved.