diff --git a/README.md b/README.md index c033ecf..f2f9856 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,9 @@ If you change the keyword arguments to a class which derives from `Serializable` The format records Python classes and modules, which must remain importable when loading data. Keys starting with two underscores are reserved. Nested Python values require the serialization helpers rather than ordinary JSON encoding. +`to_json` raises `ValueError` for NaN or infinite floats, which standard JSON +can't represent; `from_json` still reads the `NaN` / `Infinity` literals which +older versions wrote. ## Documentation diff --git a/docs/index.md b/docs/index.md index b12ed81..89c1654 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,3 +66,7 @@ and objects implementing the serialization interface. Dictionary keys beginning with two underscores are reserved by the format. `to_dict()` is the object's field mapping; it does not by itself guarantee an ordinary JSON-compatible mapping when those fields contain nested Python objects. + +Floats must be finite: `to_json()` raises `ValueError` for NaN or infinity, +which standard JSON can't represent. `from_json()` still reads the `NaN` and +`Infinity` literals which older versions wrote. diff --git a/pyproject.toml b/pyproject.toml index 8f6053a..cb60156 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,6 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ "typechecks>=0.0.2", - "simplejson>=3.0.0", ] [project.optional-dependencies] diff --git a/serializable/helpers.py b/serializable/helpers.py index 5579417..5e63f18 100644 --- a/serializable/helpers.py +++ b/serializable/helpers.py @@ -17,12 +17,11 @@ from __future__ import annotations +import json from functools import lru_cache from types import BuiltinFunctionType, FunctionType from typing import Any, Union -import simplejson as json # type: ignore[import-untyped] - from .primitive_types import PRIMITIVE_TYPES @@ -310,13 +309,19 @@ def to_json(x: Any) -> str: """ Returns JSON representation of a given Serializable instance or other primitive object. + + Raises ValueError for NaN or infinite floats, which have no standard + JSON representation. """ - return json.dumps(to_serializable_repr(x)) + return json.dumps(to_serializable_repr(x), allow_nan=False) def from_json(json_string: Union[str, bytes]) -> Any: """ Inverse of to_json. Objects are reconstructed bottom-up while parsing, equivalent to (but faster than) from_serializable_repr(json.loads(...)). + + Accepts the NaN and Infinity literals which older versions of + serializable wrote. """ return json.loads(json_string, object_hook=_from_reconstructed_dict) diff --git a/serializable/version.py b/serializable/version.py index 1993484..06549ac 100644 --- a/serializable/version.py +++ b/serializable/version.py @@ -1,4 +1,4 @@ -__version__ = "1.2.1" +__version__ = "1.3.0" def print_version(): diff --git a/tests/test_primitive_values.py b/tests/test_primitive_values.py index f44ce61..99db282 100644 --- a/tests/test_primitive_values.py +++ b/tests/test_primitive_values.py @@ -10,7 +10,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from serializable import from_serializable_repr, to_serializable_repr +import math + +import pytest + +from serializable import from_json, from_serializable_repr, to_json, to_serializable_repr from .common import eq_ @@ -29,3 +33,17 @@ def test_bool(): def test_str(): eq_("waffles", from_serializable_repr(to_serializable_repr("waffles"))) + + +@pytest.mark.parametrize("value", [math.nan, math.inf, -math.inf]) +def test_to_json_rejects_non_finite_floats(value): + # NaN and infinities have no standard JSON representation + with pytest.raises(ValueError, match="Out of range float values"): + to_json([value]) + + +def test_from_json_accepts_non_finite_literals(): + # serializable used to write these (with simplejson < 3.19) + values = from_json("[NaN, Infinity, -Infinity]") + assert math.isnan(values[0]) + eq_(values[1:], [math.inf, -math.inf])