How to Implement Custom Clinical ID Providers for PII De-identification in OpenMed

You can extend OpenMed's de-identification engine by subclassing Faker's BaseProvider, registering it via register_clinical_provider(), and optionally mapping it to canonical labels in LABEL_GENERATORS to generate organization-specific surrogate identifiers.

OpenMed uses the Faker library to synthesize realistic surrogate values for protected health information (PHI/PII). When standard identifiers like national insurance numbers or medical record numbers do not match your organization's specific formats, you must implement custom clinical ID providers for PII de-identification to ensure the synthetic data passes your validation requirements while maintaining consistency across de-identification runs.

Architecture Overview

The de-identification pipeline in OpenMed separates provider definitions from the anonymization engine. Built-in clinical ID providers reside in openmed/core/anonymizer/providers/clinical_ids.py, which includes generators for Aadhaar numbers, German Steuer-IDs, and medical record numbers (MRNs).

The registration flow follows a three-stage pattern:

  1. Provider Definition: Subclass faker.providers.BaseProvider to expose generation methods
  2. Registration: Call register_clinical_provider() from openmed/core/anonymizer/__init__.py to append the class to the internal _extra_providers list
  3. Label Binding: Map canonical labels (e.g., "CUSTOM_HOSPITAL_ID") to generator functions in LABEL_GENERATORS located in openmed/core/anonymizer/registry.py

Creating a Custom Faker Provider

Create a Python file that subclasses BaseProvider and implements a method returning your synthetic ID format. The method must generate strings that satisfy validators in openmed/core/pii_i18n.py if you intend to use the built-in validation pipeline.


# openmed/custom_providers/hospital_id.py

from faker.providers import BaseProvider
import random

class HospitalIdProvider(BaseProvider):
    """Generates IDs of the form HOSP-XXXX where X is a digit."""
    def hospital_id(self) -> str:
        # 4 random digits, zero-padded

        suffix = f"{random.randint(0, 9999):04d}"
        return f"HOSP-{suffix}"

Registering the Provider Globally

To make your provider available across all new Anonymizer instances, register it at application startup. The register_clinical_provider() function patches the internal registration routine that the engine invokes when building new Faker instances.


# Execute once at application startup

from openmed.core.anonymizer import register_clinical_provider
from openmed.custom_providers.hospital_id import HospitalIdProvider

register_clinical_provider(HospitalIdProvider)

Important: Already instantiated Anonymizer objects will not see the new provider. This design prevents side-effects in long-running processes.

Mapping Custom Labels to Generators

If you define a new canonical label (e.g., "CUSTOM_HOSPITAL_ID"), you must register a generator function in LABEL_GENERATORS. The surrogate generation logic in openmed/core/anonymizer/engine.py looks up generators from this registry.


# openmed/core/anonymizer/registry.py or a plugin module

from openmed.core.anonymizer.registry import LABEL_GENERATORS

def hospital_id_generator(faker, original, *, locale):
    # Faker now has the hospital_id method thanks to registration

    return faker.hospital_id()

# Register under a new canonical label

LABEL_GENERATORS["CUSTOM_HOSPITAL_ID"] = hospital_id_generator

If you are extending an existing label (e.g., "IDENTIFIER"), simply ensure your provider method name matches the expected generator pattern, and the engine will invoke it automatically.

Per-Instance Registration

For scenarios requiring provider isolation, pass the provider class via AnonymizerConfig. This scopes the provider to a single instance without affecting global registration.

from openmed.core.anonymizer import Anonymizer, AnonymizerConfig
from openmed.custom_providers.hospital_id import HospitalIdProvider

cfg = AnonymizerConfig(custom_providers=[HospitalIdProvider])
anon = Anonymizer(config=cfg)

# The provider is available only for this instance

synthetic = anon.surrogate("12345", "CUSTOM_HOSPITAL_ID")

Validation and Determinism Considerations

When implementing custom clinical ID providers for PII de-identification, ensure your solution handles validation and reproducibility:

  • Locale Awareness: Providers attach to each Faker locale instance automatically, allowing locale-specific formatting (e.g., language-dependent prefixes).
  • Determinism: When consistent=True and a seed are provided, the Faker instance seeds after provider registration, guaranteeing reproducible IDs across runs.
  • Validation Loop: For complex IDs requiring check digits (like the German Steuer-ID), import the relevant validator from openmed/core/pii_i18n.py inside your provider and loop until a valid value is found. Always enforce a MAX_TRIES limit to prevent infinite loops.
  • Performance: Trial-and-error generation adds computational overhead; implement caching for expensive validation routines when possible.

Summary

  • Create a Faker provider by subclassing BaseProvider in a new module with methods returning your ID format.
  • Register globally via register_clinical_provider() from openmed/core/anonymizer/__init__.py to populate the internal _extra_providers list.
  • Map new canonical labels to generator functions in LABEL_GENERATORS from openmed/core/anonymizer/registry.py.
  • Use AnonymizerConfig(custom_providers=[...]) for instance-scoped registration without side effects.
  • Validate generated IDs against functions in openmed/core/pii_i18n.py and respect MAX_TRIES limits for complex formats.

Frequently Asked Questions

How do I ensure my custom clinical IDs pass OpenMed's validation?

Import the relevant validator function from openmed/core/pii_i18n.py inside your provider method and implement a retry loop. Generate candidate values until the validator returns True, but always enforce a MAX_TRIES ceiling to prevent infinite loops if the validation rules are too strict.

Can I register multiple custom providers for different locales?

Yes. Register each provider class using register_clinical_provider(). Since providers attach to locale-specific Faker instances, you can implement locale-aware logic inside your methods (e.g., checking self.generator.locale) or create separate provider classes for different regions.

Will custom providers affect existing Anonymizer instances?

No. The registration in openmed/core/anonymizer/__init__.py only affects subsequently created Anonymizer objects. Existing instances maintain their original Faker configuration to prevent side-effects in long-running applications or multi-tenant environments.

How do I handle trial-and-error generation for complex ID formats?

Implement a generation loop inside your provider method that generates candidates, validates them using functions from openmed/core/pii_i18n.py, and returns the first valid result. Set a reasonable MAX_TRIES constant (e.g., 1000) and raise an exception or return a fallback if exceeded, mirroring the pattern used in openmed/core/anonymizer/providers/clinical_ids.py for the German Steuer-ID provider.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →