"""JSON stored in a TEXT column. MySQL 5.6 has no JSON type - it arrived in 5.7 - and a site is still running 5.6, where `sa.JSON()` fails at migration time with a bare syntax error near the word JSON. Every other JSON-bearing column in this product is already TEXT for that reason: guides' `blocksjson`, GE-Enforce's `manifestjson`. This keeps the Python side identical. A model attribute still holds a dict or a list, still assigns and reads like one; only the storage changes. The alternative - json.dumps and json.loads at seventeen call sites - is the same thing written out seventeen times, with seventeen chances to forget one. Note the SQLite behaviour is deliberately the same as MySQL's. Tests run on SQLite and production does not, so a type that behaved differently across the two would hide exactly the failure this exists to prevent. """ import json from sqlalchemy import Text from sqlalchemy.types import TypeDecorator class JSONText(TypeDecorator): """A dict or list on the Python side, TEXT in the database.""" impl = Text cache_ok = True def process_bind_param(self, value, dialect): if value is None: return None return json.dumps(value, separators=(',', ':')) def process_result_value(self, value, dialect): if value is None or value == '': return None try: return json.loads(value) except (TypeError, ValueError): # A row written before this column was JSON, or hand-edited. Hand # back the raw text rather than raising: a policy that cannot be # parsed should be visible and fixable, not an exception on read. return value