diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py index b617cfa7da..9dd13146a9 100644 --- a/pyiceberg/catalog/rest/__init__.py +++ b/pyiceberg/catalog/rest/__init__.py @@ -337,7 +337,12 @@ class ListNamespaceResponse(IcebergBaseModel): class NamespaceResponse(IcebergBaseModel): namespace: Identifier = Field() - properties: Properties = Field() + properties: Properties = Field(default_factory=dict) + + @field_validator("properties", mode="before") + @classmethod + def _coerce_null_properties(cls, v: Any) -> Any: + return v if v is not None else {} class UpdateNamespacePropertiesResponse(IcebergBaseModel): diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index 99d1ef947b..2731a227ce 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -922,6 +922,30 @@ def test_load_namespace_properties_200(rest_mock: Mocker) -> None: assert RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).load_namespace_properties(namespace) == {"prop": "yes"} +def test_load_namespace_properties_200_omitted(rest_mock: Mocker) -> None: + """The REST spec does not require 'properties' in GetNamespaceResponse. When omitted, default to empty dict.""" + namespace = "leden" + rest_mock.get( + f"{TEST_URI}v1/namespaces/{namespace}", + json={"namespace": ["fokko"]}, + status_code=200, + request_headers=TEST_HEADERS, + ) + assert RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).load_namespace_properties(namespace) == {} + + +def test_load_namespace_properties_200_null(rest_mock: Mocker) -> None: + """The REST spec marks 'properties' as nullable. When null, default to empty dict.""" + namespace = "leden" + rest_mock.get( + f"{TEST_URI}v1/namespaces/{namespace}", + json={"namespace": ["fokko"], "properties": None}, + status_code=200, + request_headers=TEST_HEADERS, + ) + assert RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).load_namespace_properties(namespace) == {} + + def test_load_namespace_properties_404(rest_mock: Mocker) -> None: namespace = "leden" rest_mock.get(