Skip to content

feat: json.dumps, json.dump accuracy improvements #13960

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions stdlib/@tests/test_cases/check_json.py
Original file line number Diff line number Diff line change
@@ -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)
85 changes: 80 additions & 5 deletions stdlib/json/__init__.pyi
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down