cardinal_pythonlib.django.fields.jsonclassfield


Original code copyright (C) 2009-2022 Rudolf Cardinal (rudolf@pobox.com).

This file is part of cardinal_pythonlib.

Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.


Django field class implementing storage of arbitrary Python objects in a database, so long as they are serializable to/from JSON.

e.g.:

import inspect
import json
from typing import Any, Dict, Union

class Thing(object):
    def __init__(self, a: int = 1, b: str = ''):
        self.a = a
        self.b = b
    def __repr__(self) -> str:
        return "<Thing(a={}, b={}) at {}>".format(
            repr(self.a), repr(self.b), hex(id(self)))


MY_JSON_TYPES = {
    'Thing': Thing,
}
TYPE_LABEL = '__type__'

class MyEncoder(json.JSONEncoder):
    def default(self, obj: Any) -> Any:
        typename = type(obj).__name__
        if typename in MY_JSON_TYPES.keys():
            d = obj.__dict__
            d[TYPE_LABEL] = typename
            return d
        return super().default(obj)


class MyDecoder(json.JSONDecoder):  # INADEQUATE for nested things
    def decode(self, s: str) -> Any:
        o = super().decode(s)
        if isinstance(o, dict):
            typename = o.get(TYPE_LABEL, '')
            if typename and typename in MY_JSON_TYPES:
                classtype = MY_JSON_TYPES[typename]
                o.pop(TYPE_LABEL)
                return classtype(**o)
        return o


def my_decoder_hook(d: Dict) -> Any:
    if TYPE_LABEL in d:
        typename = d.get(TYPE_LABEL, '')
        if typename and typename in MY_JSON_TYPES:
            classtype = MY_JSON_TYPES[typename]
            d.pop(TYPE_LABEL)
            return classtype(**d)
    return d


x = Thing(a=5, b="hello")
y = [1, x, 2]

# Encoding:
j = MyEncoder().encode(x)  # OK
j2 = json.dumps(x, cls=MyEncoder)  # OK; same result

k = MyEncoder().encode(y)  # OK
k2 = json.dumps(y, cls=MyEncoder)  # OK; same result

# Decoding
x2 = MyDecoder().decode(j)  # OK, but simple structure
y2 = MyDecoder().decode(k)  # FAILS
y3 = json.JSONDecoder(object_hook=my_decoder_hook).decode(k)  # SUCCEEDS

print(repr(x))
print(repr(x2))
class cardinal_pythonlib.django.fields.jsonclassfield.JsonClassField(*args, db_collation=None, **kwargs)[source]

Django field that serializes Python objects into JSON.

from_db_value(value, expression, connection)[source]

“Called in all circumstances when the data is loaded from the database, including in aggregates and values() calls.”

get_prep_value(value)[source]

Converse of to_python(). Converts Python objects back to query values.

to_python(value)[source]

“Called during deserialization and during the clean() method used from forms…. [s]hould deal gracefully with… (*) an instance of the correct type; (*) a string; (*) None (if the field allows null=True).”

“For to_python(), if anything goes wrong during value conversion, you should raise a ValidationError exception.”