Technical guide to DICOM UID generation

In the landscape of medical informatics, DICOM (Digital Imaging and Communications in Medicine) is the sovereign standard. At its heart lies the need to uniquely identify every entity: a patient, a radiological study, a series of images, or a single instance (SOP Instance). To do this, DICOM UIDs (Unique Identifiers) are used.

Theoretical background: the standard and regulation

A DICOM UID is an identifier based on the ISO 8824 standard (Object Identifiers – OID). A DICOM UID should be globally unique. This means that two images generated in two different hospitals will never have the same UID.

Anatomical structure

A UID consists of two main parts separated by a dot:

				
					UID = [Root] . [Suffix]
				
			
  • Root: the part that identifies the organization or manufacturer that generated the identifier (e.g., 1.2.840.10008 is the basic root of DICOM).
  • Suffix: the variable part that identifies the specific object (study, series, image) within that root.

Technical constraints (DICOM PS3.5)

For a string to be a valid UID, it must follow strict rules:

  • Characters: only numbers (0-9) and dots (.).
  • Lenght: maximum 64 characters.
  • Leading Zeros: each component (the number between dots) cannot start with a zero unless the number is exactly 0:
    • 1.2.840.0.123
    • 1.2.840.05.123 (the 0 before 5 is forbidden)
  • Immutability: once generated and assigned to an image, the UID must never change. If it changes, for the DICOM world that is a different image.

Generation methods

There are two main paths to generate the <Root> portion and the <Suffix> portion.

A. Organizational Root (ISO/ANSI)

An organization can request an official root from a national authority. The root is managed by a central authority, which acts as the disambiguator to ensure that UIDs remain unique on a global scale.

Once obtained, the company is responsible for the suffixes. A typical suffix is structured as follows:

				
					[Root] . [Product_ID] . [SW_Version] . [Level] . [Payload]
				
			

The components in detail:

  • Product/Device ID: a number identifying the specific software or machine (e.g., 101 for your PACS, ` 102 for your Viewer).
  • Software Version: useful for debugging, often the version is converted to an integer, for example 2.4.1 becomes 241.
  • Level: fundamental for separating hierarchy objects:
    • 1 = Study Instance UID
    • 2 = Series Instance UID
    • 3 = SOP Instance UID
  • Payload: typically uses a combination of date/time and a counter:
    • Compact timestamp strategy: uses a single number YYYYMMDDHHMMSSms to avoid illegal leading zeros
    • Counter strategy: adds an incremental number at the end to handle generations in the same millisecond

If the Root is too long, it is advisable to use short hashes instead of complete timestamps to not exceed 64 characters.

B. Root 2.25

If you do not have a registered root, the standard allows the use of the fixed prefix 2.25 The use of root 2.25 is not an arbitrary choice of the DICOM standard, but derives from an international agreement between ISO and ITU-T.
In this case, the suffix usually consists in the decimal conversion of a UUID.

Technical conversion:

  1. Random generation of 16 bytes (like a UUID v4).
  2. Conversion of the string to a single decimal integer.
    • This hexadecimal number is treated as a single 128-bit integer and converted to base 10.
    • The maximum value of a 128-bit integer is $2^{128} – 1$. This means the decimal part will have at most 39 digits.
  1. Final formatting: prepend the prefix 2.25: 2.25.[DecimalNumber].

Example: 2.25.123456789012345678901234567890123456789.

Technical Implementation in TypeScript

				
					import * as crypto from "crypto";

/**
 * DICOM hierarchy levels
 */
export enum DicomLevel {
  STUDY = 1,
  SERIES = 2,
  INSTANCE = 3
}

export interface GeneratorConfig {
  root?: string;
  deviceId?: string;
  softwareVersion?: string;
}

export class DicomUidGenerator {
  private readonly root: string;
  private readonly deviceId: string;
  private readonly swVersion: string;
  private lastTimestamp: string = "";
  private counter: number = 0;

  constructor(config: GeneratorConfig = {}) {
    this.root = config.root || "2.25";
    this.deviceId = (config.deviceId || "1").replace(/\D/g, "");
    this.swVersion = (config.softwareVersion || "1").replace(/\D/g, "");
  }

  /**
   * Method 2.25: Generation via UUID
   */
  public generateFromRandom(): string {
    const buffer = crypto.randomBytes(16);
    const decimalValue = BigInt("0x" + buffer.toString("hex"));
    return this.finalize(`2.25.${decimalValue.toString()}`);
  }

  /**
   * Structured Method: For those who own a registered Root
   */
  public generateStructured(level: DicomLevel): string {
    if (this.root === "2.25") {
      throw new Error("ISO/ANSI Root required for structured generation.");
    }

    const timestamp = this.getDicomTimestamp();
    if (timestamp === this.lastTimestamp) {
      this.counter++;
    } else {
      this.lastTimestamp = timestamp;
      this.counter = 0;
    }

    const components = [
      this.root,
      this.deviceId,
      this.swVersion,
      level,
      timestamp,
      this.counter
    ];
    return this.finalize(components.join("."));
  }

  private getDicomTimestamp(): string {
    const now = new Date();
    return [
      now.getFullYear(),
      (now.getMonth() + 1).toString().padStart(2, "0"),
      now.getDate().toString().padStart(2, "0"),
      now.getHours().toString().padStart(2, "0"),
      now.getMinutes().toString().padStart(2, "0"),
      now.getSeconds().toString().padStart(2, "0"),
      now.getMilliseconds().toString().padStart(3, "0")
    ].join("");
  }

  private finalize(uid: string): string {
    const sanitized = uid
      .split(".")
      .map(part =>
        part.length > 1 && part.startsWith("0") ? part.replace(/^0+/, "") : part
      )
      .join(".");

    if (sanitized.length > 64) {
      throw new Error(`UID Overflow (${sanitized.length} characters).`);
    }

    const dicomRegex = /^(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*$/;
    if (!dicomRegex.test(sanitized)) {
      throw new Error("Invalid DICOM syntax.");
    }

    return sanitized;
  }
}

				
			

Conclusions

Generating DICOM UIDs correctly ensures that medical data travels securely without risks of overwrites or ambiguities. An incorrect UID does not only cause a software bug; it can cause the loss of an examination in the hospital archive or, worse, the overwriting of clinical data.

Which approach to adopt?

There is no universal solution, but a choice dictated by context:

  • the 2.25 method (random) is more robust against collisions and the simplest to scale horizontally
  • the structured method (proprietary root) allows meeting more stringent certification requirements and tracing data, offering advantages in debugging and auditing phases.

Regardless of the method, the rules remain the same: be sure to use numbers and dots, stay within the 64-character limit and remove any leading zeros from segments. Implementing these logics correctly means ensuring that every exam, series, or image remains unique and traceable worldwide.

Latest articles

Stop Scripting Your Setup: Why You Should Try Nix Home Manager

Stop Scripting Your Setup: Why You Should Try Nix Home Manager

Stop Scripting Your Setup: Why You Should Try Nix Home Manager