From 460058d9701e33ac77b8c7dd24c51e97b1e176b4 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Thu, 9 Jul 2026 11:24:43 +0000 Subject: [PATCH] =?UTF-8?q?Make=20RdSapSchema21=5F0=5F1.built=5Fform=20opt?= =?UTF-8?q?ional=20=E2=80=94=20some=20real=20certs=20omit=20it=20entirely?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some gov-API RdSAP-21.0.1 certs (e.g. 2461-5385-9264-6821-5357, property_id=750232, portfolio 824) omit `built_form` from the response entirely rather than sending a code. `from_dict` hard-fails any dataclass field with no default that's absent from the raw dict, so the whole cert failed to parse. built_form: Optional[int] = None (kw_only, matching the has_fixed_air_conditioning precedent already on this dataclass) — the downstream SAP-cascade consumer (_api_sheltered_sides) already treats a non-int built_form as "no lodging" and falls back to the documented default, so None here is an already-safe degradation, not a new failure mode. Also guards mapper.py's `built_form=str(schema.built_form)` against stringifying None into "None". Co-Authored-By: Claude Sonnet 5 --- datatypes/epc/domain/mapper.py | 2 +- datatypes/epc/schema/rdsap_schema_21_0_1.py | 12 +++++++++--- datatypes/epc/schema/tests/test_schema_loading.py | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 90428bb2e..e3cdde0be 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -2420,7 +2420,7 @@ class EpcPropertyDataMapper: sap_version=schema.sap_version, dwelling_type=schema.dwelling_type, property_type=str(schema.property_type), - built_form=str(schema.built_form), + built_form=None if schema.built_form is None else str(schema.built_form), address_line_1=schema.address_line_1, address_line_2=schema.address_line_2, postcode=schema.postcode, diff --git a/datatypes/epc/schema/rdsap_schema_21_0_1.py b/datatypes/epc/schema/rdsap_schema_21_0_1.py index 4b814d7fb..c0d219568 100644 --- a/datatypes/epc/schema/rdsap_schema_21_0_1.py +++ b/datatypes/epc/schema/rdsap_schema_21_0_1.py @@ -460,7 +460,6 @@ class RdSapSchema21_0_1: postcode: str hot_water: EnergyElement post_town: str - built_form: int door_count: int region_code: int report_type: int @@ -512,9 +511,16 @@ class RdSapSchema21_0_1: renewable_heat_incentive: RenewableHeatIncentive draughtproofed_door_count: int energy_consumption_current: int - # kw_only on just this field: the class isn't kw_only overall, and several - # required fields (calculation_software_version etc.) already follow it. + # kw_only on just these fields: the class isn't kw_only overall, and several + # required fields (calculation_software_version etc.) already follow them. has_fixed_air_conditioning: Optional[str] = field(default=None, kw_only=True) + # Some real-API certs omit `built_form` entirely (e.g. cert + # 2461-5385-9264-6821-5357, property_id=750232, portfolio 824) rather than + # sending a code — `from_dict` otherwise hard-fails the whole cert. The + # mapper/SAP cascade (`_api_sheltered_sides`) already treats a non-int + # built_form as "no lodging" and falls back to the documented default, so + # None here is an already-safe degradation, not a new failure mode. + built_form: Optional[int] = field(default=None, kw_only=True) calculation_software_version: str energy_consumption_potential: int environmental_impact_current: int diff --git a/datatypes/epc/schema/tests/test_schema_loading.py b/datatypes/epc/schema/tests/test_schema_loading.py index 3f2759bf8..ea48b092a 100644 --- a/datatypes/epc/schema/tests/test_schema_loading.py +++ b/datatypes/epc/schema/tests/test_schema_loading.py @@ -400,3 +400,17 @@ class TestRdSapSchema21_0_1AgainstRealApiCert: assert epc.schema_type == "RdSAP-Schema-21.0.1" assert epc.sap_heating is not None assert len(epc.sap_windows) > 0 + + def test_cert_with_no_built_form_key_parses_as_none(self) -> None: + # Arrange — some real gov-API certs omit `built_form` entirely (not + # null: absent). property_id=750232 (portfolio 824) hit exactly this + # on cert 2461-5385-9264-6821-5357 and failed to parse at all. + real_doc = load("21_0_1_real.json") + del real_doc["built_form"] + + # Act + epc = from_dict(RdSapSchema21_0_1, real_doc) + + # Assert — parsing no longer hard-fails; the field degrades to None, + # same as the mapper/SAP-cascade already do for a non-int built_form. + assert epc.built_form is None