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

> Learn to implement custom clinical ID providers for PII de-identification in OpenMed by subclassing Faker and registering your provider. Generate organization-specific identifiers effectively.

- Repository: [Maziyar Panahi/openmed](https://github.com/maziyarpanahi/openmed)
- Tags: how-to-guide
- Published: 2026-06-10

---

**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`](https://github.com/maziyarpanahi/openmed/blob/main/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`](https://github.com/maziyarpanahi/openmed/blob/main/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`](https://github.com/maziyarpanahi/openmed/blob/main/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`](https://github.com/maziyarpanahi/openmed/blob/main/openmed/core/pii_i18n.py) if you intend to use the built-in validation pipeline.

```python

# 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.

```python

# 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`](https://github.com/maziyarpanahi/openmed/blob/main/openmed/core/anonymizer/engine.py) looks up generators from this registry.

```python

# 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.

```python
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`](https://github.com/maziyarpanahi/openmed/blob/main/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`](https://github.com/maziyarpanahi/openmed/blob/main/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`](https://github.com/maziyarpanahi/openmed/blob/main/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`](https://github.com/maziyarpanahi/openmed/blob/main/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`](https://github.com/maziyarpanahi/openmed/blob/main/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`](https://github.com/maziyarpanahi/openmed/blob/main/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`](https://github.com/maziyarpanahi/openmed/blob/main/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`](https://github.com/maziyarpanahi/openmed/blob/main/openmed/core/anonymizer/providers/clinical_ids.py) for the German Steuer-ID provider.