lodum.json

 1# SPDX-FileCopyrightText: 2025-present Michael R. Bernstein <zopemaven@gmail.com>
 2#
 3# SPDX-License-Identifier: Apache-2.0
 4import json
 5from typing import Any, Dict, Iterator, Type, TypeVar
 6
 7from .core import Loader, BaseDumper, BaseLoader
 8from .exception import DeserializationError
 9from .internal import dump, load, generate_schema, DEFAULT_MAX_SIZE
10
11T = TypeVar("T")
12
13# --- Public API ---
14
15
16def dumps(obj: Any) -> str:
17    """
18    Encodes a Python object to a JSON string.
19
20    Args:
21        obj: The object to encode. Must be lodum-enabled or a supported type.
22
23    Returns:
24        A JSON string representation of the object.
25    """
26    dumper = JsonDumper()
27    dumped_data = dump(obj, dumper)
28    return json.dumps(dumped_data)
29
30
31def loads(cls: Type[T], json_string: str, max_size: int = DEFAULT_MAX_SIZE) -> T:
32    """
33    Decodes a JSON string into a Python object of the specified type.
34
35    Args:
36        cls: The class to instantiate.
37        json_string: The JSON data to decode.
38        max_size: Maximum allowed size of the input string in bytes.
39
40    Returns:
41        An instance of cls populated with the decoded data.
42
43    Raises:
44        DeserializationError: If the input is invalid or exceeds max_size.
45    """
46    if len(json_string) > max_size:
47        raise DeserializationError(
48            f"Input size ({len(json_string)}) exceeds maximum allowed ({max_size})"
49        )
50    data = json.loads(json_string)
51    loader = JsonLoader(data)
52    return load(cls, loader)
53
54
55def schema(cls: Type[Any]) -> Dict[str, Any]:
56    """Generates a JSON Schema for a given lodum-enabled class."""
57    return generate_schema(cls)
58
59
60# --- JSON Dumper Implementation ---
61
62
63class JsonDumper(BaseDumper):
64    def dump_bytes(self, value: bytes) -> Any:
65        import base64
66
67        return base64.b64encode(value).decode("ascii")
68
69
70# --- JSON Loader Implementation ---
71
72
73class JsonLoader(BaseLoader):
74    def load_list(self) -> Iterator["Loader"]:
75        if not isinstance(self._data, list):
76            raise DeserializationError(
77                f"Expected list, got {type(self._data).__name__}"
78            )
79        return (JsonLoader(item) for item in self._data)
80
81    def load_dict(self) -> Iterator[tuple[str, "Loader"]]:
82        if not isinstance(self._data, dict):
83            raise DeserializationError(
84                f"Expected dict, got {type(self._data).__name__}"
85            )
86        return ((k, JsonLoader(v)) for k, v in self._data.items())
87
88    def load_bytes_value(self, value: Any) -> bytes:
89        if not isinstance(value, str):
90            raise DeserializationError(f"Expected str, got {type(value).__name__}")
91        import base64
92
93        try:
94            return base64.b64decode(value)
95        except Exception as e:
96            raise DeserializationError(f"Failed to decode base64: {e}")
def dumps(obj: Any) -> str:
17def dumps(obj: Any) -> str:
18    """
19    Encodes a Python object to a JSON string.
20
21    Args:
22        obj: The object to encode. Must be lodum-enabled or a supported type.
23
24    Returns:
25        A JSON string representation of the object.
26    """
27    dumper = JsonDumper()
28    dumped_data = dump(obj, dumper)
29    return json.dumps(dumped_data)

Encodes a Python object to a JSON string.

Args: obj: The object to encode. Must be lodum-enabled or a supported type.

Returns: A JSON string representation of the object.

def loads(cls: Type[~T], json_string: str, max_size: int = 10485760) -> ~T:
32def loads(cls: Type[T], json_string: str, max_size: int = DEFAULT_MAX_SIZE) -> T:
33    """
34    Decodes a JSON string into a Python object of the specified type.
35
36    Args:
37        cls: The class to instantiate.
38        json_string: The JSON data to decode.
39        max_size: Maximum allowed size of the input string in bytes.
40
41    Returns:
42        An instance of cls populated with the decoded data.
43
44    Raises:
45        DeserializationError: If the input is invalid or exceeds max_size.
46    """
47    if len(json_string) > max_size:
48        raise DeserializationError(
49            f"Input size ({len(json_string)}) exceeds maximum allowed ({max_size})"
50        )
51    data = json.loads(json_string)
52    loader = JsonLoader(data)
53    return load(cls, loader)

Decodes a JSON string into a Python object of the specified type.

Args: cls: The class to instantiate. json_string: The JSON data to decode. max_size: Maximum allowed size of the input string in bytes.

Returns: An instance of cls populated with the decoded data.

Raises: DeserializationError: If the input is invalid or exceeds max_size.

def schema(cls: Type[Any]) -> Dict[str, Any]:
56def schema(cls: Type[Any]) -> Dict[str, Any]:
57    """Generates a JSON Schema for a given lodum-enabled class."""
58    return generate_schema(cls)

Generates a JSON Schema for a given lodum-enabled class.

class JsonDumper(lodum.core.BaseDumper):
64class JsonDumper(BaseDumper):
65    def dump_bytes(self, value: bytes) -> Any:
66        import base64
67
68        return base64.b64encode(value).decode("ascii")

Base implementation of the Dumper protocol to reduce duplication.

def dump_bytes(self, value: bytes) -> Any:
65    def dump_bytes(self, value: bytes) -> Any:
66        import base64
67
68        return base64.b64encode(value).decode("ascii")
class JsonLoader(lodum.core.BaseLoader):
74class JsonLoader(BaseLoader):
75    def load_list(self) -> Iterator["Loader"]:
76        if not isinstance(self._data, list):
77            raise DeserializationError(
78                f"Expected list, got {type(self._data).__name__}"
79            )
80        return (JsonLoader(item) for item in self._data)
81
82    def load_dict(self) -> Iterator[tuple[str, "Loader"]]:
83        if not isinstance(self._data, dict):
84            raise DeserializationError(
85                f"Expected dict, got {type(self._data).__name__}"
86            )
87        return ((k, JsonLoader(v)) for k, v in self._data.items())
88
89    def load_bytes_value(self, value: Any) -> bytes:
90        if not isinstance(value, str):
91            raise DeserializationError(f"Expected str, got {type(value).__name__}")
92        import base64
93
94        try:
95            return base64.b64decode(value)
96        except Exception as e:
97            raise DeserializationError(f"Failed to decode base64: {e}")

Base implementation of the Loader protocol to reduce duplication.

def load_list(self) -> Iterator[lodum.core.Loader]:
75    def load_list(self) -> Iterator["Loader"]:
76        if not isinstance(self._data, list):
77            raise DeserializationError(
78                f"Expected list, got {type(self._data).__name__}"
79            )
80        return (JsonLoader(item) for item in self._data)
def load_dict(self) -> Iterator[tuple[str, lodum.core.Loader]]:
82    def load_dict(self) -> Iterator[tuple[str, "Loader"]]:
83        if not isinstance(self._data, dict):
84            raise DeserializationError(
85                f"Expected dict, got {type(self._data).__name__}"
86            )
87        return ((k, JsonLoader(v)) for k, v in self._data.items())
def load_bytes_value(self, value: Any) -> bytes:
89    def load_bytes_value(self, value: Any) -> bytes:
90        if not isinstance(value, str):
91            raise DeserializationError(f"Expected str, got {type(value).__name__}")
92        import base64
93
94        try:
95            return base64.b64decode(value)
96        except Exception as e:
97            raise DeserializationError(f"Failed to decode base64: {e}")