How can I safely normalize nested coverages into a flat dictionary while keeping from_dict() and to_dict() idempotent?
I am normalizing property coverage data that can arrive in either a nested or legacy flat format. The goal is to convert both formats into a single coverages structure before creating the model objects.
The normalization works in most cases, but I am concerned about keeping the conversion consistent when the data is deserialized with from_dict() and later serialized again with to_dict().
def expand_premises_coverages(premise: dict) -> list[dict]:
"""Flatten a nested premise (``{...shared, "coverages": [...]}``) into one
row per coverage; return ``[premise]`` unchanged when it has no nested
coverages list, so callers work with both the flat and nested input shapes.
"""
if not isinstance(premise, dict):
return []
coverages = premise.get("coverages")
if not isinstance(coverages, list) or not coverages:
return [premise]
shared = {k: v for k, v in premise.items() if k != "coverages"}
rows = [{**shared, **cov} for cov in coverages if isinstance(cov, dict)]
return rows or [premise]
def normalize_property_coverages(propertydata: dict | None) -> None:
"""Rewrite ``propertydata`` so all coverages live under a single flat
``coverages`` list.
For nested inbound (``premisesN`` with a ``coverages: [...]`` list) each
coverage is emitted as its own row inheriting the parent's identity fields
(premises_number, building_number, street_address, bldg_desc, etc.).
For legacy flat inbound (``premisesN`` already one-subject-per-row) the
rows are promoted 1:1 into ``coverages``. Either way ``propertydata`` ends
up with a canonical ``coverages`` list that every downstream reader (cp7,
gl7, submit_validation, …) can iterate uniformly – no per-reader adapter
needed. Idempotent when ``coverages`` is already a populated list.
"""
if not isinstance(propertydata, dict):
return
existing = propertydata.get("coverages")
if isinstance(existing, list) and existing:
return
if isinstance(existing, dict) and existing:
# Legacy coverageN-keyed dict – flatten to a list.
propertydata["coverages"] = [
row for row in existing.values() if isinstance(row, dict)
]
propertydata.pop("premises", None)
propertydata.pop("premises_information", None)
return
premises = (
propertydata.get("premises")
or propertydata.get("premises_information")
)
if not premises:
return
if isinstance(premises, dict):
entries = [row for row in premises.values() if isinstance(row, dict)]
elif isinstance(premises, list):
entries = [row for row in premises if isinstance(row, dict)]
else:
return
flat: list[dict] = []
for entry in entries:
flat.extend(expand_premises_coverages(entry))
if not flat:
return
propertydata["coverages"] = flat
# Drop the legacy premises group so downstream sees a single source of truth.
propertydata.pop("premises", None)
propertydata.pop("premises_information", None)
@classmethod
def from_dict(cls, data: dict | None) -> PropertyData:
data = data or {}
normalize_property_coverages(data)
coverages_block = (
data.get("coverages")
or data.get("premises")
or data.get("premises_information")
or {}
)
rows: list[PremisesRow] = []
if isinstance(coverages_block, dict):
for _key, row in coverages_block.items():
if isinstance(row, dict):
rows.append(PremisesRow.from_dict(row))
elif isinstance(coverages_block, list):
for row in coverages_block:
if isinstance(row, dict):
rows.append(PremisesRow.from_dict(row))
return cls(
premises=rows,
additional_coverages=dict(
data.get("additionalCoverages")
or data.get("additional_coverages")
or {}
),
raw=dict(data),
)
def to_dict(self) -> dict:
"""Emit the normalized ``coverages`` list and drop legacy premises keys."""
out = dict(self.raw) if self.raw else {}
out["coverages"] = [row.to_dict() for row in self.premises]
out.pop("premises", None)
out.pop("premises_information", None)
if self.additional_coverages:
out["additionalCoverages"] = self.additional_coverages
return out