Package: tj-json
tjson— An access utility for typed "JSON-shaped" Python objectsHave you had to deal with deeply nested JSON documents in Python, only to be greeted byKeyError? What about only to run across weird type issues, and mysterious bugs? Have you wished that you could easily get type hinting on values that you pull out of your JSON documents?tjsonis here to help.Installingtjsonhas no dependencies outside the standard Python library. Simply install it with Pip or your favorite dependency manager.pip install tj_jsontjsonis taken on PyPI by a dead project, and I haven't bothered to take over the name.Usagetjson's API  is designed to be intuitive and simple.>>>fromtjson.tjsonimportTJTJis a generic wrapper for JSON values. Simply give it your "JSON-shaped" Python values to get started.Note:tjsonis an access utility, not a parser/deserializer.To parse your JSON, you must still use a parser such as the standard library'sjson.>>>mydata=TJ({'who':'the quick brown fox','what':'jumped','where':['over','the lazy dog'],'timestamp':12345.123})Traversal and accessTo traverseTJ, simply use Python's item accessor[]. All usees of it return a new instance ofTJpointing to the new value. To retrieve the value wrapped byTJ, just check its.value.>>>mydata['where'][1]<tjson.tjson.TJobjectat0x7f02148e2f20>>>>mydata['where'][1].value'the lazy dog'TJobjects also store the path that they followed to reach them, and that can be queried using.path.>>>mydata['where'][1].path'.where[1]'When things go wrong: WarningsWhen using regular Pythondict/liststrucures, trying to access a bad key or index results in exceptions. This makes you need either over-broad or over-nitpicky exception handling, and makes code smell.tjsoninstead relegates problems accessing our JSON documents as warnings, taking advantage of Python's smartwarningsfeature set.When access to the underlying object fails, aTJinstance is created anyway, just wrapping the valueNone. Additionally, an appropriate warning is fired off usingwarnings.warn, and the warnings are collected in the.warningsproperty of theTJ.>>>print(*mydata['who']['entities'][123]['name'].warnings,sep="\n")<stdin>:1:InvalidKeyWarning:Triedtoaccessstrkey'entities'ofnon-objectatpath`.who`Triedtoaccessstrkey'entities'ofnon-objectatpath`.who`Cannotaccessintindex123ofnon-arrayatpath.who.entitiesTriedtoaccessstrkey'name'ofnon-objectatpath`.who.entities[123]`Only the first warning in a chain of warnings is actually sent towarnings.warn. This is to avoid flooding if an error happens early in a complex access chain.If you want the bad accesses to raise warnings instead, you can use thewarningsAPI to enable the specific class:>>>importwarnings>>>fromtjson.errorsimportInvalidKeyWarning>>>warnings.simplefilter('error',InvalidKeyWarning)>>>mydata['who']['entities'][123]['name']Traceback(mostrecentcalllast):File"<stdin>",line1,in<module>File"/home/fsufitch/code/tjson/tjson/tjson.py",line30,in__getitem__returnTJ(None,next_path,_amend_warns(self.warnings,InvalidKeyWarning(f"Tried to access str key{repr(key)}of non-object at path `{self.path}`"),2))File"/home/fsufitch/code/tjson/tjson/tjson.py",line123,in_amend_warnswarn(warning,stacklevel=stacklevel+1)tjson.errors.InvalidKeyWarning:Triedtoaccessstrkey'entities'ofnon-objectatpath`.who`Refer to the standard library'swarningsdocumentation for more usages.Type assertionAnother way to use theTJobjects is to have them check the type of value you are accessing. Consider this case of Bob refusing to play the game properly:>>>high_scores={'Alice':123,'Bob':None,'Charlie':456}>>>alice_score=high_scores['Alice']>>>bob_score=high_scores['Bob']>>>charlie_score=high_scores['Charlie']>>>print(max(alice_score,bob_score,charlie_score))Traceback(mostrecentcalllast):File"<stdin>",line1,in<module>TypeError:'>'notsupportedbetweeninstancesof'NoneType'and'int'TJcan come to the rescue, using the.numberassertion to create a copy of itself that checks that itactuallycontains a number! Like with access problems, any issue results in a warning, and theTJcontaining the default value of the appropriate type (in this case,0).>>>high_scores=TJ({'Alice':123,'Bob':None,'Charlie':456})>>>alice_score=high_scores['Alice'].number.value>>>bob_score=high_scores['Bob'].number.value<stdin>:1:TypeMismatchWarning:Cannotcasttoint|floatatpath`.Bob`>>>charlie_score=high_scores['Charlie'].number.value>>>print(max(alice_score,bob_score,charlie_score))456No more error! But what if you wantedNoneto actually be a valid value? That's cool, you can use.number_or_nullinstead, andNonewill be considered a "technically valid" value for the number.The type assertions supported this way correspond to the different types in JSON, plus their "nullable" versions. They map to the following Python type hints:bool→boolbool_or_null→Optional[bool]string→strstring_or_null→Optional[str]number→int | floatnumber_or_null→Optional[int | float]array→List[...]array_or_null→Optional[List[...]]object→Dict[str, ...]object_or_null→Optional[Dict[str, ...]]Note: JSON does not distinguish between integer and floating point numbers, sotjsondoes not either. If your code cares, you need to handle it separately.Note: The only valid object keys in JSON are strings. Integers are not valid object keys, and need to be wrapped in strings to be used as such.Type hints for your IDEtjsonis built using type hints on all the right places. Your IDE should be able to pick these up in order to provide you a rich type checking experience.For example, using PyLance in Visual Studio Code, I can see that my string operation is potentially unsafe since I might be addingNoneto a string:More...?tjsonhas good test coverage. Check its tests (test_*.py) to see all the features.License: MITLiketjson? Do what you like with it, just give credit.Don't like it? Think it's pointless or trivial? That's probably fair, but it's here anyway.Enjoy, and happy coding!