Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/utils/json.py: 83%
58 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 15:02 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 15:02 +0000
1import base64
2import datetime
3import decimal
4import json
5import pytz
6import typing as t
7from viur.core import db
10class ViURJsonEncoder(json.JSONEncoder):
11 """
12 Adds support for db.Key, db.Entity, datetime, timedelta, bytes, set and Decimal and converts
13 the provided obj into a special dict with JSON-serializable values.
14 """
15 def default(self, obj: t.Any) -> t.Any:
16 if isinstance(obj, bytes):
17 return {".__bytes__": base64.b64encode(obj).decode("ASCII")}
18 elif isinstance(obj, datetime.datetime):
19 return {".__datetime__": obj.astimezone(pytz.UTC).isoformat()}
20 elif isinstance(obj, datetime.timedelta):
21 return {".__timedelta__": obj / datetime.timedelta(microseconds=1)}
22 elif isinstance(obj, decimal.Decimal):
23 # str() keeps the exact value (NumericBone(decimal=True)); float would round it
24 return {".__decimal__": str(obj)}
25 elif isinstance(obj, set):
26 return {".__set__": list(obj)}
27 elif hasattr(obj, "__iter__"): 27 ↛ 30line 27 didn't jump to line 30 because the condition on line 27 was always true
28 return tuple(obj)
29 # cannot be tested in tests...
30 elif isinstance(obj, db.Key):
31 return {".__key__": str(obj)}
33 return super().default(obj)
35 @staticmethod
36 def preprocess(obj: t.Any) -> t.Any:
37 """
38 Needed to preprocess db.Entity as it subclasses dict.
39 There is currently no other way to integrate with JSONEncoder.
40 """
41 if isinstance(obj, db.Entity): 41 ↛ 42line 41 didn't jump to line 42 because the condition on line 41 was never true
42 return {
43 ".__entity__": ViURJsonEncoder.preprocess(dict(obj)),
44 ".__key__": str(obj.key) if obj.key else None
45 }
46 elif isinstance(obj, dict):
47 return {
48 ViURJsonEncoder.preprocess(key): ViURJsonEncoder.preprocess(value) for key, value in obj.items()
49 }
50 elif isinstance(obj, (list, tuple)):
51 return tuple(ViURJsonEncoder.preprocess(value) for value in obj)
53 elif hasattr(obj, "__class__") and obj.__class__.__name__ == "SkeletonInstance": # SkeletonInstance 53 ↛ 54line 53 didn't jump to line 54 because the condition on line 53 was never true
54 return {bone_name: ViURJsonEncoder.preprocess(obj[bone_name]) for bone_name in obj}
56 return obj
59def dumps(obj: t.Any, *, cls: ViURJsonEncoder = ViURJsonEncoder, **kwargs) -> str:
60 """
61 Wrapper for json.dumps() which converts additional ViUR datatypes.
62 """
63 return json.dumps(cls.preprocess(obj), cls=cls, **kwargs)
66def _decode_object_hook(obj: t.Any):
67 """
68 Inverse for _preprocess_json_object, which is an object-hook for json.loads.
69 Check if the object matches a custom ViUR type and recreate it accordingly.
70 """
71 # Membership checks, not truthiness: b"" encodes to "", timedelta(0) to 0.0 and
72 # set() to [] -- all falsy, yet they must round-trip to their type, not to the marker dict.
73 if len(obj) == 1:
74 if ".__bytes__" in obj:
75 return base64.b64decode(obj[".__bytes__"])
76 elif ".__datetime__" in obj:
77 return datetime.datetime.fromisoformat(obj[".__datetime__"])
78 elif ".__timedelta__" in obj:
79 return datetime.timedelta(microseconds=obj[".__timedelta__"])
80 elif ".__decimal__" in obj:
81 return decimal.Decimal(obj[".__decimal__"])
82 elif ".__key__" in obj: 82 ↛ 83line 82 didn't jump to line 83 because the condition on line 82 was never true
83 return db.Key.from_legacy_urlsafe(obj[".__key__"])
84 elif ".__set__" in obj:
85 return set(obj[".__set__"])
87 elif len(obj) == 2 and all(k in obj for k in (".__entity__", ".__key__")): 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true
88 entity = db.Entity(db.Key.from_legacy_urlsafe(obj[".__key__"]) if obj[".__key__"] else None)
89 entity.update(obj[".__entity__"])
90 return entity
92 return obj
95def loads(s: str, *, object_hook=_decode_object_hook, **kwargs) -> t.Any:
96 """
97 Wrapper for json.loads() which recreates additional ViUR datatypes.
98 """
99 return json.loads(s, object_hook=object_hook, **kwargs)