JSON Property

GINO provides additional support to leverage native JSON type in the database as flexible GINO model fields.

Quick Start

from gino import Gino
from sqlalchemy.dialects.postgresql import JSONB

db = Gino()

class User(db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    profile = db.Column(JSONB, nullable=False, server_default="{}")

    age = db.IntegerProperty()
    birthday = db.DateTimeProperty()

The age and birthday are JSON properties stored in the profile column. You may use them the same way as a normal GINO model field:

u = await User.create(name="daisy", age=18)
print(u.name, u.age)  # daisy 18

Note

profile is the default column name for all JSON properties in a model. If you need a different column name for some JSON properties, you’ll need to specify explicitly:

audit_profile = db.Column(JSON, nullable=False, server_default="{}")

access_log = db.ArrayProperty(prop_name="audit_profile")
abnormal_detected = db.BooleanProperty(prop_name="audit_profile")

Using JSON properties in queries is supported:

await User.query.where(User.age > 16).gino.all()

This is simply translated into a native JSON query like this:

SELECT users.id, users.name, users.profile
FROM users
WHERE CAST((users.profile ->> $1) AS INTEGER) > $2;  -- ('age', 16)

Datetime type is very much the same:

from datetime import datetime

await User.query.where(User.birthday > datetime(1990, 1, 1)).gino.all()

And the generated SQL:

SELECT users.id, users.name, users.profile
FROM users
WHERE CAST((users.profile ->> $1) AS TIMESTAMP WITHOUT TIME ZONE) > $2
-- ('birthday', datetime.datetime(1990, 1, 1, 0, 0))

Here’s a list of all the supported JSON properties:

JSON Property

Python type

JSON type

Database Type

StringProperty

str

string

text

IntegerProperty

int

number

int

BooleanProperty

bool

boolean

boolean

DateTimeProperty

datetime

string

text

ObjectProperty

dict

object

JSON

ArrayProperty

list

array

JSON

Hooks

JSON property provides 2 instance-level hooks to customize the data:

class User(db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    profile = db.Column(JSONB, nullable=False, server_default="{}")

    age = db.IntegerProperty()

    @age.before_set
    def age(self, val):
        return val - 1

    @age.after_get
    def age(self, val):
        return val + 1

u = await User.create(name="daisy", age=18)
print(u.name, u.profile, u.age)  # daisy {'age': 17} 18

And 1 class-level hook to customize the SQLAlchemy expression of the property:

class User(db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    profile = db.Column(JSONB, nullable=False, server_default="{}")

    height = db.JSONProperty()

    @height.expression
    def height(cls, exp):
        return exp.cast(db.Float)  # CAST(profile -> 'height' AS FLOAT)

Create Index on JSON Properties

We’ll need to use declared_attr() to wait until the model class is initialized. The rest is very much the same as defining a usual index:

class User(db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    profile = db.Column(JSONB, nullable=False, server_default="{}")

    age = db.IntegerProperty()

    @db.declared_attr
    def age_idx(cls):
        return db.Index("age_idx", cls.age)

This will lead to the SQL below executed if you run db.gino.create_all():

CREATE INDEX age_idx ON users (CAST(profile ->> 'age' AS INTEGER));

Warning

Alembic doesn’t support auto-generating revisions for functional indexes yet. You’ll need to manually edit the revision. Please follow this issue for updates.