SQLAlchemy-JSONAPI¶
Contents:
Quickstart¶
Installation¶
Installation of SQLAlchemy-JSONAPI can be done via pip:
pip install -U sqlalchemy_jsonapi
Attaching to Declarative Base¶
To initialize the serializer, you first have to attach it to an instance of SQLAlchemy’s Declarative Base that is connected to your models:
from sqlalchemy_jsonapi import JSONAPI
class User(Base):
__tablename__ = 'users'
id = Column(UUIDType, primary_key=True)
# ...
class Address(Base):
__tablename__ = 'address'
id = Column(UUIDType, primary_key=True)
user_id = Column(UUIDType, ForeignKey('users.id'))
# ...
serializer = JSONAPI(Base)
Serialization¶
Now that your serializer is initialized, you can quickly and easily serialize your models. Let’s do a simple collection serialization:
@app.route('/api/users')
def users_list():
response = serializer.get_collection(db.session, {}, 'users')
return jsonify(response.data)
The third argument to get_collection where users is specified is the model type. This is auto-generated from the model name, but you can control this using __jsonapi_type_override__.
This is useful when you don’t want hyphenated type names. For example, a model named UserConfig will have a generated type of user-config. You can change this declaratively on the model:
class UserConfig(Base):
__tablename__ = 'userconfig'
__jsonapi_type_override__ = 'userconfig'
Deserialization¶
Deserialization is also quick and easy:
@app.route('/api/users/<user_id>', methods=['PATCH'])
def update_user(user_id):
json_data = request.get_json(force=True)
response = serializer.patch_resource(db.session, json_data, 'users', user_id)
return jsonify(response.data)
If you use Flask, this can be automated and simplified via the included Flask module.
Preparing Your Models¶
Validation¶
SQLAlchemy-JSONAPI makes use of the SQLAlchemy validates decorator:
from sqlalchemy.orm import validates
class User(Base):
email = Column(Unicode(255))
@validates('email')
def validate_email(self, key, email):
""" Ultra-strong email validation. """
assert '@' in email, 'Not an email'
return email
Now raise your hand if you knew SQLAlchemy had that decorator. Well, now you know, and it’s quite useful!
Attribute Descriptors¶
Sometimes, you may need to provide your own getters and setters to attributes:
from sqlalchemy_jsonapi import attr_descriptor, AttributeActions
class User(Base):
id = Column(UUIDType)
# ...
@attr_descriptor(AttributeActions.GET, 'id')
def id_getter(self):
return str(self.id)
@attr_descriptor(AttributeActions.SET, 'id')
def id_setter(self, new_id):
self.id = UUID(new_id)
Note: id is not able to be altered after initial setting in JSON API to keep it consistent.
Relationship Descriptors¶
Relationship’s come in two flavors: to-one and to-many (or tranditional and LDS-flavored if you prefer those terms). To one descriptors have the actions GET and SET:
from sqlalchemy_jsonapi import relationship_descriptor, RelationshipActions
@relationship_descriptor(RelationshipActions.GET, 'significant_other')
def getter(self):
# ...
@relationship_descriptor(RelationshipActions.SET, 'significant_other')
def setter(self, value):
# ...
To-many have GET, APPEND, and DELETE:
@relationship_descriptor(RelationshipActions.GET, 'angry_exes')
def getter(self):
# ...
@relationship_descriptor(RelationshipActions.APPEND, 'angry_exes')
def appender(self):
# ...
@relationship_descriptor(RelationshipActions.DELETE, 'angry_exes')
def remover(self):
# ...
Permission Testing¶
Permissions are a complex challenge in relational databases. While the solution provided right now is extremely simple, it is almost guaranteed to evolve and change drastically as this library gets used more in production. Thus it is advisable that on every major version number increment, you should check this section for changes to permissions.
Anyway, there are currently four permissions that are checked: GET, CREATE, EDIT, and DELETE. Permission tests can be applied module-wide or to specific fields:
@permission_test(Permissions.VIEW)
def can_view(self):
return self.is_published
@permission_test(Permissions.EDIT, 'slug')
def can_edit_slug(self):
return False
Serializer¶
Flask¶
To those who use Flask, setting up SQLAlchemy-JSONAPI can be extremely complex and frustrating. Let’s look at an example:
from sqlalchemy_jsonapi import FlaskJSONAPI
app = Flask(__name__)
db = SQLAlchemy(app)
api = FlaskJSONAPI(app, db)
And after all that work, you should now have a full working API.
Signals¶
As Flask makes use of signals via Blinker, it would be appropriate to make use of them in the Flask module for SQLALchemy-JSONAPI. If a signal receiver returns a value, it can alter the final response.
on_request¶
Triggered before serialization:
@api.on_request.connect
def process_api_request(sender, method, endpoint, data, req_args):
# Handle the request
on_success¶
Triggered after successful serialization:
@api.on_success.connect
def process_api_success(sender, method, endpoint, data, req_args, response):
# Handle the response dictionary
on_error¶
Triggered after failed handling:
@api.on_error.connect
def process_api_error(sender, method, endpoint, data, req_args, error):
# Handle the error
on_response¶
Triggered after rendering of response:
@api.on_response.connect
def process_api_response(sender, method, endpoint, data, req_args, rendered_response):
# Handle the rendered response
Wrapping the Handlers¶
While signals provide some control, sometimes you want to wrap or override the handler for the particular endpoint and method. This can be done through a specialized decorator that allows you to specify in what cases you want the handler wrapped:
@api.wrap_handler(['users'], [Methods.GET], [Endpoints.COLLECTION, Endpoints.RESOURCE])
def log_it(next, *args, **kwargs):
logging.info('In a wrapped handler!')
return next(*args, **kwargs)
Handlers are placed into a list and run in order of placement within the list. That means you can perform several layers of checks and override as needed.