16

I'm storing JSON down as blob/text in a column using MySQL. Is there a simple way to convert this into a dict using python/SQLAlchemy?

Timmy
  • 12,468
  • 20
  • 77
  • 107

8 Answers8

16

You can very easily create your own type with SQLAlchemy


For SQLAlchemy versions >= 0.7, check out Yogesh's answer below


import jsonpickle
import sqlalchemy.types as types

class JsonType(types.MutableType, types.TypeDecorator):    
    impl = types.Unicode

    def process_bind_param(self, value, engine):
        return unicode(jsonpickle.encode(value))

    def process_result_value(self, value, engine):
        if value:
            return jsonpickle.decode(value)
        else:
            # default can also be a list
            return {}

This can be used when you are defining your tables (example uses elixir):

from elixir import *
class MyTable(Entity):
    using_options(tablename='my_table')
    foo = Field(String, primary_key=True)
    content = Field(JsonType())
    active = Field(Boolean, default=True)

You can also use a different json serialiser to jsonpickle.

Community
  • 1
  • 1
EoghanM
  • 25,161
  • 23
  • 90
  • 123
  • This failed to work for me. Within class MutableType(object): def copy_value raises an exception. def copy_value(self, value): """Unimplemented.""" raise NotImplementedError() – Keith John Hutchison Jun 11 '13 at 07:40
  • I changed the source and it worked but I did not feel comfortable about the maintenance issues this would cause. – Keith John Hutchison Jun 11 '13 at 07:41
  • 1
    Great answer... Also note that PostgreSQL supports a JSON type. This looks promising - it is like your example, but will use the PostgreSQL JSON type if available. [sqlalchemy-utils JSONType](http://sqlalchemy-utils.readthedocs.org/en/latest/_modules/sqlalchemy_utils/types/json.html) – floer32 Jul 15 '14 at 22:48
  • `MutableType` has been replaced by `sqlalchemy.ext.mutable` [extension](http://docs.sqlalchemy.org/en/latest/orm/extensions/mutable.html), can the above class be updated to use it? – Yogesh Mangaj Aug 27 '14 at 09:09
  • @YogeshMangaj if you want to submit a separate answer with ext.mutable I'll refer to it and vote it up. I don't have the means to test out the new version right now. – EoghanM Aug 29 '14 at 17:36
  • @EoghanM sure, I didn't create my own as I do not understand SQLAlchemy all too well, but I found a Gist which I have tested for my usage, it seems to work well [https://gist.github.com/dbarnett/1730610#file-jsonalchemy-py](https://gist.github.com/dbarnett/1730610#file-jsonalchemy-py) – Yogesh Mangaj Aug 29 '14 at 19:04
  • @EoghanM added the answer below http://stackoverflow.com/a/25574866/457447, thanks! – Yogesh Mangaj Aug 29 '14 at 19:21
9

sqlalchemy.types.MutableType has been deprecated (v0.7 onward), the documentation recommends using sqlalchemy.ext.mutable instead.

I found a Git gist by dbarnett that I have tested for my usage. It has worked well so far, for both dictionary and lists.

Pasting below for posterity:

import simplejson
import sqlalchemy
from sqlalchemy import String
from sqlalchemy.ext.mutable import Mutable

class JSONEncodedObj(sqlalchemy.types.TypeDecorator):
    """Represents an immutable structure as a json-encoded string."""

    impl = String

    def process_bind_param(self, value, dialect):
        if value is not None:
            value = simplejson.dumps(value)
        return value

    def process_result_value(self, value, dialect):
        if value is not None:
            value = simplejson.loads(value)
        return value

class MutationObj(Mutable):
    @classmethod
    def coerce(cls, key, value):
        if isinstance(value, dict) and not isinstance(value, MutationDict):
            return MutationDict.coerce(key, value)
        if isinstance(value, list) and not isinstance(value, MutationList):
            return MutationList.coerce(key, value)
        return value

    @classmethod
    def _listen_on_attribute(cls, attribute, coerce, parent_cls):
        key = attribute.key
        if parent_cls is not attribute.class_:
            return

        # rely on "propagate" here
        parent_cls = attribute.class_

        def load(state, *args):
            val = state.dict.get(key, None)
            if coerce:
                val = cls.coerce(key, val)
                state.dict[key] = val
            if isinstance(val, cls):
                val._parents[state.obj()] = key

        def set(target, value, oldvalue, initiator):
            if not isinstance(value, cls):
                value = cls.coerce(key, value)
            if isinstance(value, cls):
                value._parents[target.obj()] = key
            if isinstance(oldvalue, cls):
                oldvalue._parents.pop(target.obj(), None)
            return value

        def pickle(state, state_dict):
            val = state.dict.get(key, None)
            if isinstance(val, cls):
                if 'ext.mutable.values' not in state_dict:
                    state_dict['ext.mutable.values'] = []
                state_dict['ext.mutable.values'].append(val)

        def unpickle(state, state_dict):
            if 'ext.mutable.values' in state_dict:
                for val in state_dict['ext.mutable.values']:
                    val._parents[state.obj()] = key

        sqlalchemy.event.listen(parent_cls, 'load', load, raw=True, propagate=True)
        sqlalchemy.event.listen(parent_cls, 'refresh', load, raw=True, propagate=True)
        sqlalchemy.event.listen(attribute, 'set', set, raw=True, retval=True, propagate=True)
        sqlalchemy.event.listen(parent_cls, 'pickle', pickle, raw=True, propagate=True)
        sqlalchemy.event.listen(parent_cls, 'unpickle', unpickle, raw=True, propagate=True)

class MutationDict(MutationObj, dict):
    @classmethod
    def coerce(cls, key, value):
        """Convert plain dictionary to MutationDict"""
        self = MutationDict((k,MutationObj.coerce(key,v)) for (k,v) in value.items())
        self._key = key
        return self

    def __setitem__(self, key, value):
        dict.__setitem__(self, key, MutationObj.coerce(self._key, value))
        self.changed()

    def __delitem__(self, key):
        dict.__delitem__(self, key)
        self.changed()

class MutationList(MutationObj, list):
    @classmethod
    def coerce(cls, key, value):
        """Convert plain list to MutationList"""
        self = MutationList((MutationObj.coerce(key, v) for v in value))
        self._key = key
        return self

    def __setitem__(self, idx, value):
        list.__setitem__(self, idx, MutationObj.coerce(self._key, value))
        self.changed()

    def __setslice__(self, start, stop, values):
        list.__setslice__(self, start, stop, (MutationObj.coerce(self._key, v) for v in values))
        self.changed()

    def __delitem__(self, idx):
        list.__delitem__(self, idx)
        self.changed()

    def __delslice__(self, start, stop):
        list.__delslice__(self, start, stop)
        self.changed()

    def append(self, value):
        list.append(self, MutationObj.coerce(self._key, value))
        self.changed()

    def insert(self, idx, value):
        list.insert(self, idx, MutationObj.coerce(self._key, value))
        self.changed()

    def extend(self, values):
        list.extend(self, (MutationObj.coerce(self._key, v) for v in values))
        self.changed()

    def pop(self, *args, **kw):
        value = list.pop(self, *args, **kw)
        self.changed()
        return value

    def remove(self, value):
        list.remove(self, value)
        self.changed()

def JSONAlchemy(sqltype):
    """A type to encode/decode JSON on the fly

    sqltype is the string type for the underlying DB column.

    You can use it like:
    Column(JSONAlchemy(Text(600)))
    """
    class _JSONEncodedObj(JSONEncodedObj):
        impl = sqltype
    return MutationObj.as_mutable(_JSONEncodedObj)
Yogesh Mangaj
  • 3,200
  • 6
  • 32
  • 45
9

I think the JSON example from the SQLAlchemy docs is also worth mentioning:

https://docs.sqlalchemy.org/en/13/core/custom_types.html#marshal-json-strings

However, I think it can be improved to be less strict regarding NULL and empty strings:

class JSONEncodedDict(TypeDecorator):
    impl = VARCHAR

    def process_bind_param(self, value, dialect):
        if value is None:
            return None
        return json.dumps(value, use_decimal=True)

    def process_result_value(self, value, dialect):
        if not value:
            return None
        return json.loads(value, use_decimal=True)
snakecharmerb
  • 47,570
  • 11
  • 100
  • 153
ThiefMaster
  • 310,957
  • 84
  • 592
  • 636
  • 1
    NB: This will only work if you treat the value as an immutable. So, you assign the object attribute a full `dict`. If you try to modify just elements of the `dict`, sqlalchemy won't register the changes and they won't be saved on flush. See `sqlalchemy.ext.mutable.Mutable` on how to change that. – Tim Tisdall Apr 30 '15 at 20:46
7

There is a recipe for this in the official documentation:

from sqlalchemy.types import TypeDecorator, VARCHAR
import json

class JSONEncodedDict(TypeDecorator):
    """Represents an immutable structure as a json-encoded string.

    Usage::

        JSONEncodedDict(255)

    """

    impl = VARCHAR

    def process_bind_param(self, value, dialect):
        if value is not None:
            value = json.dumps(value)

        return value

    def process_result_value(self, value, dialect):
        if value is not None:
            value = json.loads(value)
        return value
joelhed
  • 60
  • 6
MazeChaZer
  • 1,228
  • 14
  • 8
6

How about json.loads()?

>>> d= {"foo":1, "bar":[2,3]}
>>> s='{"foo":1, "bar":[2,3]}'
>>> import json
>>> json.loads(s) == d
True
snapshoe
  • 13,454
  • 1
  • 24
  • 28
  • 1
    thanks,is there a way to automatically do it? like similar to a trigger in sqlalchemy. – Timmy Oct 28 '10 at 15:02
2

Based on @snapshoe answer and to answer @Timmy's comment:

You can do it by using properties. Here is an example of a table:

class Providers(Base):
    __tablename__ = "providers"
    id = Column(
        Integer,
        Sequence('providers_id', optional=True),
        primary_key=True
    )
    name = Column(Unicode(40), index=True)
    _config = Column("config", Unicode(2048))

    @property
    def config(self):
        if not self._config:
            return {}
        return json.loads(self._config)

    @config.setter
    def config(self, value):
        self._config = json.dumps(value)

    def set_config(self, field, value):
        config = self.config
        config[field] = value
        self.config = config

    def get_config(self):
        if not self._config:
            return {}
        return json.loads(self._config)

    def unset_config(self, field):
        config = self.get_config()
        if field in config:
            del config[field]
            self.config = config

Now you can use it on a Providers() object:

>>> p = Providers()
>>> p.set_config("foo", "bar")
>>> p.get_config()
{"foo": "bar"}
>>> a.config
{u'foo': u'bar'}

I know this is an old Question maybe even dead, but I hope this could help someone.

Renier
  • 1,523
  • 4
  • 32
  • 60
1

This is what I came up with based on the two answers above.

import json

class JsonType(types.TypeDecorator):    

    impl = types.Unicode

    def process_bind_param(self, value, dialect):
        if value :
            return unicode(json.dumps(value))
        else:
            return {}

    def process_result_value(self, value, dialect):
        if value:
            return json.loads(value)
        else:
            return {}
Keith John Hutchison
  • 4,955
  • 11
  • 46
  • 64
1

As an update to the previous responses, which we've used with success so far. As of MySQL 5.7 and SQLAlchemy 1.1 you can use the native MySQL JSON data type, which gives you better performance and a whole range of operators for free.

It lets you to create virtual secondary indexes on JSON elements too.

But of course you will lock yourself into running your app on MySQL only when moving the logic into the database itself.

Pieter Ennes
  • 2,301
  • 19
  • 21