Call.Report
GitHub
GitHub

ffiec-data-connect (Python)

DataNormalizer

Static class that reshapes REST API responses to match the legacy SOAP format — ZIP leading zeros, numeric-ID string coercion, datetime format normalization.

DataNormalizer is the compatibility shim that makes REST responses look like SOAP responses. It’s applied automatically inside the collect_* functions; you only touch it directly if you’re calling RESTAdapter methods yourself.

All methods are @staticmethod — no instance state.

Why this exists

The FFIEC REST API returns typed JSON, which is mostly fine. Three places where it regresses against the SOAP payload format:

  1. ZIP codes lose leading zeros. REST returns 2886 (int) for Warwick, RI. SOAP returned "02886" (str). DataNormalizer coerces to string and pads to five digits.
  2. Numeric identifiers come back as integers. ID_RSSD, FDICCertNumber, OCCChartNumber, OTSDockNumber, PrimaryABARoutNumber all arrive as ints; downstream consumers (databases, joins, string-keyed dicts) expect strings.
  3. SubmissionDateTime format varies. Normalized to MM/dd/yyyy HH:mm:ss AM/PM to match SOAP output.

Without this step, a pipeline migrating from SOAP to REST would silently break schema on three fields. With it, the REST payload is byte-compatible with SOAP for the endpoints that matter.

normalize_response

DataNormalizer.normalize_response(
    data: Any,
    endpoint: str,
    protocol: str = "REST",
) -> Any

Main entry point. endpoint is the SOAP operation name — one of:

  • "RetrievePanelOfReporters"
  • "RetrieveFilersSinceDate"
  • "RetrieveFilersSubmissionDateTime"
  • "RetrieveReportingPeriods"
  • "RetrieveFacsimile" (preserves binary unchanged)
  • "RetrieveUBPRReportingPeriods"
  • "RetrieveUBPRXBRLFacsimile" (preserves binary unchanged)

If protocol != "REST", the function returns data unchanged. If endpoint is unknown, it logs a warning and returns data unchanged — so forward-compatibility is safe, but don’t rely on normalization for an endpoint not listed above.

from ffiec_data_connect import DataNormalizer, create_protocol_adapter, OAuth2Credentials

creds = OAuth2Credentials(username="...", bearer_token="eyJ...")
adapter = create_protocol_adapter(creds)

raw = adapter.retrieve_panel_of_reporters("12/31/2025")
clean = DataNormalizer.normalize_response(raw, "RetrievePanelOfReporters", "REST")
# clean[0]["ID_RSSD"]       == "480228"   (was 480228 int)
# clean[0]["ZIP"]            == "02886"    (was 2886 int)
# clean[0]["FDICCertNumber"] == "17546"    (was 17546 int)

Exceptions from normalization are caught internally and logged; the function returns the original data on failure, to avoid breaking a pipeline over a formatting glitch. If you need strict behavior, validate after the fact with validate_pydantic_compatibility().

TYPE_COERCIONS

Class-level dict mapping each endpoint to its per-field transforms:

DataNormalizer.TYPE_COERCIONS["RetrievePanelOfReporters"]
# {
#   "ID_RSSD":               lambda x: str(x),
#   "FDICCertNumber":        lambda x: str(x) if x is not None else "",
#   "OCCChartNumber":        lambda x: str(x) if x is not None else "",
#   "ZIP":                   lambda x: DataNormalizer._fix_zip_code(x),
#   "HasFiledForReportingPeriod": lambda x: str(x).lower() if x is not None else "false",
#   ...
# }

Two special keys recognized by the dispatcher:

  • "_array_items" — when the response is a list of scalars (e.g. RetrieveFilersSinceDate returns list[int]), the dispatcher maps the coercion across the list.
  • "_preserve_binary": True — signals that the payload is raw XBRL bytes and must pass through unchanged.

You can monkey-patch this dict at runtime if you need to add normalizations for a field the library doesn’t know about yet — but prefer filing an upstream issue.

validate_pydantic_compatibility

DataNormalizer.validate_pydantic_compatibility(data: Any, endpoint: str) -> dict

Runs lightweight post-normalization validation:

  • Numeric-ID fields are strings of digits.
  • ZIP is exactly 5 digits.
  • RSSD lists are all strings.

Returns:

{
    "endpoint": str,
    "compatible": bool,
    "errors": list[str],
    "warnings": list[str],
}

Useful as a canary in a CI or data-ingest check. Does not mutate.

get_normalization_stats

DataNormalizer.get_normalization_stats(original: Any, normalized: Any) -> dict

Diff-counter for debugging. Returns the count of objects changed, the approximate size delta, and the endpoint-level summary. Call it when you need to confirm normalization actually did something on a sample payload.

Internals (not public API, but useful to know)

  • _fix_zip_code(x) — the leading-zero fix. Accepts int | str | None, returns a 5-char string or "".
  • _normalize_datetime(x) — MM/dd/yyyy HH:mm:ss AM/PM coercion.
  • _normalize_date_string(x) — MM/dd/yyyy coercion for period strings.

These are underscore-prefixed; don’t import them directly. They’re documented here only because you’ll see them referenced in TYPE_COERCIONS lambdas.

When normalization runs automatically

Call siteEndpoint normalized
collect_dataRetrieveFacsimile (binary passthrough)
collect_filers_on_reporting_periodRetrievePanelOfReporters
collect_filers_since_dateRetrieveFilersSinceDate
collect_filers_submission_date_timeRetrieveFilersSubmissionDateTime
collect_reporting_periodsRetrieveReportingPeriods
collect_ubpr_reporting_periodsRetrieveUBPRReportingPeriods
collect_ubpr_facsimile_dataRetrieveUBPRXBRLFacsimile (binary passthrough)

If you’re using the collect_* surface, you’re already getting this for free. Invoking DataNormalizer directly is only necessary when you call RESTAdapter.retrieve_* yourself.

See also

navigate · open · Esc close