File size: 1,470 Bytes
969891d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
JSON that is actually valid JSON.

Python's `json.dumps` accepts NaN and Infinity by default and emits them as bare
`NaN` / `Infinity` tokens. Those are not valid JSON: the Gemini API rejects the
payload with `400 INVALID_ARGUMENT ... Unexpected token`, and a browser's
`JSON.parse` throws on them too. Because `dumps` does not raise, the problem
slips through any "try dumps, fall back on error" guard.

Non-finite values arrive naturally from analytics — AVG over an empty group,
a ratio with a zero denominator, ST_Area on a degenerate geometry. They are
converted to null, which is what "no value" means in JSON.
"""

from __future__ import annotations

import json
import math
from typing import Any


def json_safe(obj: Any) -> Any:
    """Recursively replace non-finite floats with None so the result is valid JSON."""
    if isinstance(obj, float):
        return None if (math.isnan(obj) or math.isinf(obj)) else obj
    if isinstance(obj, dict):
        return {k: json_safe(v) for k, v in obj.items()}
    if isinstance(obj, (list, tuple)):
        return [json_safe(v) for v in obj]
    return obj


def dumps_safe(obj: Any) -> str:
    """
    Serialize to strictly-valid JSON.

    `allow_nan=False` makes a missed non-finite value raise instead of producing
    a payload that fails later at the API or in the browser; `default=str`
    handles dates and other stragglers.
    """
    return json.dumps(json_safe(obj), allow_nan=False, default=str)