diff --git a/src/power_grid_model_ds/_core/model/grids/serialization/json.py b/src/power_grid_model_ds/_core/model/grids/serialization/json.py index 635e20e7..9998bf17 100644 --- a/src/power_grid_model_ds/_core/model/grids/serialization/json.py +++ b/src/power_grid_model_ds/_core/model/grids/serialization/json.py @@ -7,6 +7,7 @@ import dataclasses import json import logging +import math from pathlib import Path from typing import TYPE_CHECKING, Any @@ -144,7 +145,7 @@ def _restore_grid_values[G: Grid](grid: G, json_data: dict) -> None: def _serialize_array(array: FancyArray) -> list[dict[str, Any]]: - return [{name: record[name].item() for name in array.columns} for record in array] + return [{name: _replace_nan_with_none(record[name].item()) for name in array.columns} for record in array] def _deserialize_array(array_data: list[dict[str, Any]], array_class: type[FancyArray]) -> FancyArray: @@ -182,3 +183,10 @@ def _is_serializable(value: Any, strict: bool, **kwargs) -> bool: _logger.warning(msg) return False return True + + +def _replace_nan_with_none(value: Any) -> Any: + """Replace a NaN value with a JSON-compatible null value.""" + if isinstance(value, float) and math.isnan(value): + return None + return value diff --git a/tests/unit/model/grids/serialization/data/legacy_nan.json b/tests/unit/model/grids/serialization/data/legacy_nan.json new file mode 100644 index 00000000..81f28e15 --- /dev/null +++ b/tests/unit/model/grids/serialization/data/legacy_nan.json @@ -0,0 +1,10 @@ +{ + "data": { + "node": [ + { + "id": 1, + "u_rated": NaN + } + ] + } +} diff --git a/tests/unit/model/grids/serialization/data/legacy_nan.json.license b/tests/unit/model/grids/serialization/data/legacy_nan.json.license new file mode 100644 index 00000000..76010591 --- /dev/null +++ b/tests/unit/model/grids/serialization/data/legacy_nan.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/tests/unit/model/grids/serialization/test_json.py b/tests/unit/model/grids/serialization/test_json.py index 210d9a96..bb66abf1 100644 --- a/tests/unit/model/grids/serialization/test_json.py +++ b/tests/unit/model/grids/serialization/test_json.py @@ -155,6 +155,25 @@ def test_pgm_roundtrip(self, request, grid_fixture: str, tmp_path: Path): assert array_equal_with_nan(original_array, loaded_array), f"Array '{array_name}' does not match" + def test_nan_serializes_as_json_null(self, basic_grid: Grid, tmp_path: Path): + basic_grid.node.u_rated[0] = np.nan + + path = basic_grid.serialize(tmp_path / "grid.json") + with path.open(encoding="utf-8") as file: + file_data = json.load(file) + + string_data = json.loads(basic_grid.serialize(mode="json_string")) + + assert file_data["data"]["node"][0]["u_rated"] is None + assert string_data["data"]["node"][0]["u_rated"] is None + + def test_deserialize_legacy_nan_json(self): + path = Path(__file__).parent / "data" / "legacy_nan.json" + + grid = Grid.deserialize(path) + + assert np.isnan(grid.node.u_rated[0]) + class TestCrossTypeCompatibility: """Test cross-type loading and compatibility"""