File size: 3,626 Bytes
223daa0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""JSON serialisation utilities with Decimal precision preservation.

This module provides safe JSON serialisation for financial data,
ensuring Decimal types are converted to strings (not floats) to
preserve full precision. Converting Decimal to float can cause
monetary errors in large values (e.g., Β£1,000,000,000.47 β†’ Β£1,000,000,000.50).
"""

from typing import Any
from decimal import Decimal
from datetime import datetime, date
import orjson
import logging

logger = logging.getLogger(__name__)


def default_handler(obj: Any) -> Any:
    """Custom default handler for orjson serialisation.

    Handles types that aren't natively JSON-serialisable:
    - Decimal β†’ string (preserves precision for financial data)
    - datetime/date β†’ ISO format string

    Args:
        obj: Object to serialise

    Returns:
        JSON-serialisable representation

    Raises:
        TypeError: If object type is not handled

    Example:
        >>> import orjson
        >>> from decimal import Decimal
        >>> orjson.dumps({"price": Decimal("19.99")}, default=default_handler)
        b'{"price":"19.99"}'
    """
    if isinstance(obj, Decimal):
        # CRITICAL: Convert to string, NOT float
        # Float loses precision in large numbers
        return str(obj)
    elif isinstance(obj, (datetime, date)):
        return obj.isoformat()
    raise TypeError(f"Type {type(obj)} not serialisable")


def dumps(obj: Any) -> bytes:
    """Serialise object to JSON bytes with Decimal support.

    Args:
        obj: Object to serialise

    Returns:
        JSON bytes with Decimal values as strings

    Example:
        >>> from decimal import Decimal
        >>> dumps({"price": Decimal("19.99"), "quantity": 5})
        b'{"price":"19.99","quantity":5}'
    """
    return orjson.dumps(obj, default=default_handler)


def dumps_str(obj: Any) -> str:
    """Serialise object to JSON string with Decimal support.

    Args:
        obj: Object to serialise

    Returns:
        JSON string with Decimal values as strings

    Example:
        >>> from decimal import Decimal
        >>> dumps_str({"price": Decimal("19.99")})
        '{"price":"19.99"}'
    """
    return dumps(obj).decode('utf-8')


def decimal_to_json_safe(obj: Any) -> Any:
    """Recursively convert non-JSON-serialisable objects to safe types.

    This function provides an alternative approach when you need
    Python dict/list structures rather than JSON bytes.

    Converts:
    - Decimal objects β†’ string (preserves full precision)
    - datetime/date objects β†’ ISO format strings

    This preserves precision for financial calculations, which is critical
    as converting Decimal to float can lose pence in large numbers.

    Args:
        obj: Any Python object (dict, list, Decimal, datetime, etc.)

    Returns:
        Same structure with non-serialisable objects converted

    Example:
        >>> from decimal import Decimal
        >>> decimal_to_json_safe({"price": Decimal('19.99')})
        {'price': '19.99'}
        >>> decimal_to_json_safe([Decimal('1.23'), Decimal('4.56')])
        ['1.23', '4.56']
    """
    if isinstance(obj, Decimal):
        # CRITICAL: String preserves precision, float does not
        return str(obj)
    elif isinstance(obj, (datetime, date)):
        return obj.isoformat()
    elif isinstance(obj, dict):
        return {k: decimal_to_json_safe(v) for k, v in obj.items()}
    elif isinstance(obj, list):
        return [decimal_to_json_safe(item) for item in obj]
    elif isinstance(obj, tuple):
        return tuple(decimal_to_json_safe(item) for item in obj)
    return obj