Repository: ome-zarr-models/ome-zarr-models-py

Issue:
`MultiscaleBase.model_dump(exclude_none=True)` fails with version 0.1.7
Hi there,

Based on current `main` (that is, version 0.1.7), if I apply this patch
```diff
diff --git a/tests/v04/test_image.py b/tests/v04/test_image.py
index dd1f5fb..8ce3f74 100644
--- a/tests/v04/test_image.py
+++ b/tests/v04/test_image.py
@@ -247,5 +247,5 @@ def test_no_global_transform() -> None:
         scales=[(4, 4), (8, 8)],
         translations=[(2, 2), (4, 4)],
     )
-    model_dict = new_image.model_dump()
+    model_dict = new_image.model_dump(exclude_none=True)
     assert "coordinateTransformations" not in model_dict["attributes"]["multiscales"][0]
```
then `tests/v04/test_image.py::test_no_global_transform` fails as in the traceback below.

Is this an intended behavior?
If not, it can likely be fixed by replacing `d.pop("coordinateTransformations")` with `d.pop("coordinateTransformations", None)`.

(ref https://github.com/fractal-analytics-platform/ngio/issues/84)

---

```console
$ uv run pytest -s tests/v04/test_image.py::test_no_global_transform
============================================================== test session starts ===============================================================
platform linux -- Python 3.12.10, pytest-8.4.0, pluggy-1.6.0 -- /tmp/ome-zarr-models-py/.venv/bin/python
cachedir: .pytest_cache
rootdir: /tmp/ome-zarr-models-py
configfile: pyproject.toml
plugins: cov-6.1.1
collected 1 item                                                                                                                                 

tests/v04/test_image.py::test_no_global_transform FAILED

==================================================================== FAILURES ====================================================================
____________________________________________________________ test_no_global_transform ____________________________________________________________

self = Multiscale(axes=(Axis(name='x', type='space', unit='km'), Axis(name='y', type='space', unit='km')), datasets=(Dataset(...ation', translation=[4.0, 4.0])))), coordinateTransformations=None, metadata=None, name=None, type=None, version='0.4')
serializer = SerializationCallable(serializer=Multiscale)

    @model_serializer(mode="wrap")
    def _serialize(
        self,
        serializer: SerializerFunctionWrapHandler,
    ) -> dict[str, Any]:
        d: dict[str, Any] = serializer(self)
        if self.coordinateTransformations is None:
>           d.pop("coordinateTransformations")
E           KeyError: 'coordinateTransformations'

src/ome_zarr_models/common/multiscales.py:149: KeyError

The above exception was the direct cause of the following exception:

    def test_no_global_transform() -> None:
        new_image = Image.new(
            array_specs=[
                ArraySpec(shape=(5, 5), chunks=(2, 2), dtype=np.uint8),
                ArraySpec(shape=(3, 3), chunks=(2, 2), dtype=np.uint8),
            ],
            paths=["scale0", "scale1"],
            axes=[
                Axis(name="x", type="space", unit="km"),
                Axis(name="y", type="space", unit="km"),
            ],
            scales=[(4, 4), (8, 8)],
            translations=[(2, 2), (4, 4)],
        )
>       model_dict = new_image.model_dump(exclude_none=True)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/v04/test_image.py:250: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = Image(zarr_version=2, attributes=ImageAttrs(multiscales=[Multiscale(axes=(Axis(name='x', type='space', unit='km'), Axi...=(3, 3), chunks=(2, 2), dtype='|u1', fill_value=0, order='C', filters=None, dimension_separator='/', compressor=None)})

    def model_dump(
        self,
        *,
        mode: Literal['json', 'python'] | str = 'python',
        include: IncEx | None = None,
        exclude: IncEx | None = None,
        context: Any | None = None,
        by_alias: bool | None = None,
        exclude_unset: bool = False,
        exclude_defaults: bool = False,
        exclude_none: bool = False,
        round_trip: bool = False,
        warnings: bool | Literal['none', 'warn', 'error'] = True,
        fallback: Callable[[Any], Any] | None = None,
        serialize_as_any: bool = False,
    ) -> dict[str, Any]:
        """!!! abstract "Usage Documentation"
            [`model_dump`](../concepts/serialization.md#modelmodel_dump)
    
        Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.
    
        Args:
            mode: The mode in which `to_python` should run.
                If mode is 'json', the output will only contain JSON serializable types.
                If mode is 'python', the output may contain non-JSON-serializable Python objects.
            include: A set of fields to include in the output.
            exclude: A set of fields to exclude from the output.
            context: Additional context to pass to the serializer.
            by_alias: Whether to use the field's alias in the dictionary key if defined.
            exclude_unset: Whether to exclude fields that have not been explicitly set.
            exclude_defaults: Whether to exclude fields that are set to their default value.
            exclude_none: Whether to exclude fields that have a value of `None`.
            round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].
            warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,
                "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
            fallback: A function to call when an unknown value is encountered. If not provided,
                a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
            serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
    
        Returns:
            A dictionary representation of the model.
        """
>       return self.__pydantic_serializer__.to_python(
            self,
            mode=mode,
            by_alias=by_alias,
            include=include,
            exclude=exclude,
            context=context,
            exclude_unset=exclude_unset,
            exclude_defaults=exclude_defaults,
            exclude_none=exclude_none,
            round_trip=round_trip,
            warnings=warnings,
            fallback=fallback,
            serialize_as_any=serialize_as_any,
        )
E       pydantic_core._pydantic_core.PydanticSerializationError: Error calling function `_serialize`: KeyError: 'coordinateTransformations'

.venv/lib/python3.12/site-packages/pydantic/main.py:463: PydanticSerializationError
================================================================= tests coverage =================================================================
________________________________________________ coverage: platform linux, python 3.12.10-final-0 ________________________________________________

Coverage XML written to file coverage.xml
============================================================ short test summary info =============================================================
FAILED tests/v04/test_image.py::test_no_global_transform - pydantic_core._pydantic_core.PydanticSerializationError: Error calling function `_serialize`: KeyError: 'coordinateTransformations'
=============================================================== 1 failed in 0.13s ================================================================
```

Discussion:

Fix (patch):
diff --git a/src/ome_zarr_models/common/multiscales.py b/src/ome_zarr_models/common/multiscales.py
index 573d2d5..e6275c5 100644
--- a/src/ome_zarr_models/common/multiscales.py
+++ b/src/ome_zarr_models/common/multiscales.py
@@ -146,7 +146,7 @@ class MultiscaleBase(BaseAttrs):
     ) -> dict[str, Any]:
         d: dict[str, Any] = serializer(self)
         if self.coordinateTransformations is None:
-            d.pop("coordinateTransformations")
+            d.pop("coordinateTransformations", None)
 
         return d
 


Test changes:
diff --git a/tests/v04/test_image.py b/tests/v04/test_image.py
index dd1f5fb..0451a1f 100644
--- a/tests/v04/test_image.py
+++ b/tests/v04/test_image.py
@@ -249,3 +249,5 @@ def test_no_global_transform() -> None:
     )
     model_dict = new_image.model_dump()
     assert "coordinateTransformations" not in model_dict["attributes"]["multiscales"][0]
+
+    new_image.model_dump(exclude_none=True)