diff --git a/stdlib/@tests/test_cases/check_json.py b/stdlib/@tests/test_cases/check_json.py new file mode 100644 index 000000000000..15fc78a8ca48 --- /dev/null +++ b/stdlib/@tests/test_cases/check_json.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import json +from decimal import Decimal +from typing import TypedDict + + +class _File: + def write(self, s: str) -> int: ... + + +fp = _File() + + +# By default, json.dumps() will not accept any non JSON-serializable objects. +class CustomClass: ... + + +json.dumps(CustomClass()) # type: ignore +json.dump(CustomClass(), fp) # type: ignore +json.dumps(object()) # type: ignore +json.dump(object(), fp) # type: ignore +json.dumps(Decimal(1)) # type: ignore +json.dump(Decimal(1), fp) # type: ignore + +# Serializable types are supported, included nested JSON. +json.dumps({"a": 34, "b": [1, 2, 3], "c": {"d": "hello", "e": False}}) +json.dump({"a": 34, "b": [1, 2, 3], "c": {"d": "hello", "e": False}}, fp) +json.dumps( + { + "numbers": [1, 2, 3, 4, 5], + "strings": ["hello", "world"], + "booleans": [True, False], + "null": None, + "nested": {"array": [[1, 2], [3, 4.34]], "object": {"x": 1, "y": 2}}, + } +) +json.dump( + { + "numbers": [1, 2, 3, 4, 5], + "strings": ["hello", "world"], + "booleans": [True, False], + "null": None, + "nested": {"array": [[1, 2], [3, 4.34]], "object": {"x": 1, "y": 2}}, + }, + fp, +) +json.dumps(1) +json.dump(1, fp) +json.dumps(1.23) +json.dump(1.23, fp) +json.dumps(True) +json.dump(True, fp) + +# Test explicit nested types that might cause variance issues. +x: dict[str, float | int] = {"a": 1, "b": 2.0} +json.dumps(x) +json.dump(x, fp) + +z: dict[str, dict[str, dict[str, list[int]]]] = {"a": {"b": {"c": [1, 2, 3]}}} +json.dumps(z) +json.dump(z, fp) + + +# Custom types are supported when a custom encoder is provided. +def decimal_encoder(obj: Decimal) -> float: + return float(obj) + + +json.dumps(Decimal(1), default=decimal_encoder) +json.dump(Decimal(1), fp, default=decimal_encoder) + + +# If the custom encoder doesn't return JSON, it will lead a typing error.. +def custom_encoder(obj: Decimal) -> Decimal: + return obj + + +json.dumps(Decimal(1), default=custom_encoder) # type: ignore +json.dump(Decimal(1), fp, default=custom_encoder) # type: ignore + + +class MyTypedDict(TypedDict): + a: str + b: str + + +json.dumps(MyTypedDict(a="hello", b="world")) + + +# We should allow anything for subclasses of json.JSONEncoder. +# Type-checking custom encoders is not practical without generics. +class MyJSONEncoder(json.JSONEncoder): ... + + +json.dumps(Decimal(1), cls=MyJSONEncoder) +json.dump(Decimal(1), fp, cls=MyJSONEncoder) diff --git a/stdlib/json/__init__.pyi b/stdlib/json/__init__.pyi index 63e9718ee151..45b89da8260e 100644 --- a/stdlib/json/__init__.pyi +++ b/stdlib/json/__init__.pyi @@ -1,28 +1,85 @@ from _typeshed import SupportsRead, SupportsWrite -from collections.abc import Callable -from typing import Any +from collections.abc import Callable, Mapping, Sequence +from typing import Any, TypeVar, overload +from typing_extensions import TypeAlias from .decoder import JSONDecodeError as JSONDecodeError, JSONDecoder as JSONDecoder from .encoder import JSONEncoder as JSONEncoder __all__ = ["dump", "dumps", "load", "loads", "JSONDecoder", "JSONDecodeError", "JSONEncoder"] +_T = TypeVar("_T") + +# Mapping[str, object] is used to maintain compatibility with typed dictionaries +# despite it being very loose it's preferrable to using Any. +_JSON: TypeAlias = Mapping[str, object] | Sequence[_JSON] | str | float | bool | None + +@overload def dumps( - obj: Any, + obj: _JSON, *, skipkeys: bool = False, ensure_ascii: bool = True, check_circular: bool = True, allow_nan: bool = True, - cls: type[JSONEncoder] | None = None, + cls: None = None, + indent: None | int | str = None, + separators: tuple[str, str] | None = None, + default: None = None, + sort_keys: bool = False, + **kwds: Any, +) -> str: ... +@overload +def dumps( + obj: _JSON | _T, + *, + skipkeys: bool = False, + ensure_ascii: bool = True, + check_circular: bool = True, + allow_nan: bool = True, + cls: None = None, + indent: None | int | str = None, + separators: tuple[str, str] | None = None, + default: Callable[[_T], _JSON], + sort_keys: bool = False, + **kwds: Any, +) -> str: ... + +# Type-checking subclasses without generics isn't practical. +@overload +def dumps( + obj: object, + *, + skipkeys: bool = False, + ensure_ascii: bool = True, + check_circular: bool = True, + allow_nan: bool = True, + cls: type[JSONEncoder], indent: None | int | str = None, separators: tuple[str, str] | None = None, default: Callable[[Any], Any] | None = None, sort_keys: bool = False, **kwds: Any, ) -> str: ... +@overload def dump( - obj: Any, + obj: _JSON, + fp: SupportsWrite[str], + *, + skipkeys: bool = False, + ensure_ascii: bool = True, + check_circular: bool = True, + allow_nan: bool = True, + cls: None = None, + indent: None | int | str = None, + separators: tuple[str, str] | None = None, + default: None = None, + sort_keys: bool = False, + **kwds: Any, +) -> None: ... +@overload +def dump( + obj: _JSON | _T, fp: SupportsWrite[str], *, skipkeys: bool = False, @@ -32,6 +89,24 @@ def dump( cls: type[JSONEncoder] | None = None, indent: None | int | str = None, separators: tuple[str, str] | None = None, + default: Callable[[_T], _JSON], + sort_keys: bool = False, + **kwds: Any, +) -> None: ... + +# Type-checking subclasses without generics isn't practical. +@overload +def dump( + obj: object, + fp: SupportsWrite[str], + *, + skipkeys: bool = False, + ensure_ascii: bool = True, + check_circular: bool = True, + allow_nan: bool = True, + cls: type[JSONEncoder], + indent: None | int | str = None, + separators: tuple[str, str] | None = None, default: Callable[[Any], Any] | None = None, sort_keys: bool = False, **kwds: Any,