Example¶
An example of Flask-COMBO-JSONAPI API with ComboJSONAPI looks like this:
from flask import Flask
from marshmallow import pre_load
from combojsonapi.utils import Relationship
from combojsonapi.spec import ApiSpecPlugin
from combojsonapi.permission.permission_system import (
PermissionMixin,
PermissionForGet,
PermissionUser,
)
from combojsonapi.permission import PermissionPlugin
from flask_combo_jsonapi import Api, ResourceDetail, ResourceList
from flask_sqlalchemy import SQLAlchemy
from marshmallow_jsonapi.flask import Schema
from marshmallow_jsonapi import fields
# Create the Flask application
app = Flask(__name__)
app.config["DEBUG"] = True
# Initialize SQLAlchemy
app.config["SQLALCHEMY_ECHO"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////tmp/api-example.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
# swagger / openapi config
app.config["OPENAPI_URL_PREFIX"] = "/api/swagger"
app.config["OPENAPI_VERSION"] = "3.0.0"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/"
app.config["OPENAPI_SWAGGER_UI_VERSION"] = "3.45.0"
# Create models
class Person(db.Model):
class Meta:
required_fields = {
# OPTIONAL BUT RECOMMENDED
# when using sparse fields (`GET /persons?fields[person]=full_name,email`)
# when serialising obj, property `full_name` will use fields `first_name` and `last_name`
# and they will be loaded one by one
# BUT IF YOU USE PERMISSION PLUGIN
# you can fix it by declaring `required_fields`
# in format {"field_name": ["dependant field one", "another dependant field"]}
"full_name": ["first_name", "last_name"],
}
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String)
last_name = db.Column(db.String)
email = db.Column(db.String)
password = db.Column(db.String)
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
class Computer(db.Model):
id = db.Column(db.Integer, primary_key=True)
serial = db.Column(db.String)
person_id = db.Column(db.Integer, db.ForeignKey("person.id"))
person = db.relationship("Person", backref=db.backref("computers"))
db.create_all()
# Create logical data abstraction (same as data storage for this first example)
class PersonSchema(Schema):
class Meta:
type_ = "person"
self_view = "person_detail"
self_view_kwargs = {"id": "<id>"}
self_view_many = "person_list"
id = fields.Integer(as_string=True, dump_only=True)
first_name = fields.String(required=True)
last_name = fields.String(required=True)
full_name = fields.String(required=True, dump_only=True)
email = fields.Email()
computers = Relationship(
nested="ComputerSchema",
schema="ComputerSchema",
self_view="person_detail",
self_view_kwargs={"id": "<id>"},
related_view="computer_list",
many=True,
type_="computer",
)
@pre_load
def remove_id_before_deserializing(self, data, **kwargs):
"""
We don't want to allow editing ID on POST / PATCH
Related issues:
https://github.com/AdCombo/flask-combo-jsonapi/issues/34
https://github.com/miLibris/flask-rest-jsonapi/issues/193
"""
if "id" in data:
del data["id"]
return data
class ComputerSchema(Schema):
class Meta:
type_ = "computer"
self_view = "computer_detail"
self_view_kwargs = {"id": "<id>"}
self_view_many = "computer_list"
id = fields.Integer(as_string=True, dump_only=True)
serial = fields.String(required=True)
owner = Relationship(
nested="PersonSchema",
schema="PersonSchema",
attribute="person",
self_view="computer_detail",
self_view_kwargs={"id": "<id>"},
related_view="person_detail",
related_view_kwargs={"int": "<id>"},
type_="person",
)
@pre_load
def remove_id_before_deserializing(self, data, **kwargs):
"""
We don't want to allow editing ID on POST / PATCH
Related issues:
https://github.com/AdCombo/flask-combo-jsonapi/issues/34
https://github.com/miLibris/flask-rest-jsonapi/issues/193
"""
if "id" in data:
del data["id"]
return data
# create Permissions
class BaseAllowAllFieldsPermission(PermissionMixin):
ALL_FIELDS = []
def get(
self, *args, many=True, user_permission: PermissionUser = None, **kwargs
) -> PermissionForGet:
"""Setting all available columns"""
self.permission_for_get.allow_columns = (self.ALL_FIELDS, 10)
return self.permission_for_get
class PersonsPermission(BaseAllowAllFieldsPermission):
ALL_FIELDS = [
"id",
"first_name",
"last_name",
"full_name",
"email",
"computers",
]
class ComputersPermission(BaseAllowAllFieldsPermission):
ALL_FIELDS = [
"id",
"serial",
"person",
"owner",
]
# Create resource managers
class PersonList(ResourceList):
schema = PersonSchema
data_layer = {
"session": db.session,
"model": Person,
"permission_get": [PersonsPermission],
}
class PersonDetail(ResourceDetail):
schema = PersonSchema
data_layer = {
"session": db.session,
"model": Person,
"permission_get": [PersonsPermission],
}
class ComputerList(ResourceList):
schema = ComputerSchema
data_layer = {
"session": db.session,
"model": Computer,
"permission_get": [ComputersPermission],
}
class ComputerDetail(ResourceDetail):
schema = ComputerSchema
data_layer = {
"session": db.session,
"model": Computer,
"permission_get": [ComputersPermission],
}
api_spec_plugin = ApiSpecPlugin(
app=app,
# Declaring tags list with their descriptions, so API gets organized into groups.
# This is optional: when there are no tags,
# api will be grouped automatically by type schemas names (type_)
tags={
"Person": "Person API",
"Computer": "Computer API",
},
)
# Create endpoints
api = Api(
app,
plugins=[
api_spec_plugin,
PermissionPlugin(strict=False),
],
)
api.route(PersonList, "person_list", "/persons", tag="Person")
api.route(PersonDetail, "person_detail", "/persons/<int:id>", tag="Person")
api.route(ComputerList, "computer_list", "/computers", tag="Computer")
api.route(ComputerDetail, "computer_detail", "/computers/<int:id>", tag="Computer")
if __name__ == "__main__":
# Start application
app.run(debug=True)
Warning
In this example Flask-SQLAlchemy is used, so you’ll need to install it before running this example.
$ pip install flask_sqlalchemy
Save this file as api.py and run it using your Python interpreter. Note that we’ve enabled Flask debugging mode to provide code reloading and better error messages.
$ python api.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader
Warning
Debug mode should never be used in a production environment!
Classical CRUD operations¶
Create object¶
Request:
POST /persons HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "person",
"attributes": {
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@example.com"
}
}
}
Response:
HTTP/1.1 201 Created
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"email": "john.smith@example.com",
"first_name": "John",
"last_name": "Smith"
},
"id": "1",
"links": {
"self": "/persons/1"
},
"type": "person"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/persons/1"
}
}
Get object¶
Request:
GET /persons/1 HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"email": "john.smith@example.com",
"first_name": "John",
"full_name": "John Smith",
"last_name": "Smith"
},
"id": "1",
"links": {
"self": "/persons/1"
},
"type": "person"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/persons/1"
}
}
Get objects¶
Request:
GET /persons HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"email": "john.smith@example.com",
"first_name": "John",
"full_name": "John Smith",
"last_name": "Smith"
},
"id": "1",
"links": {
"self": "/persons/1"
},
"type": "person"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost:5000/persons"
},
"meta": {
"count": 1
}
}
Sparse fields¶
Note that we tell SQLAlchemy that full_name requires first_name and first_name fields.
If we don’t declare this dependency in model’s Meta.required_fields,
when serialising each model will fire two queries to DB to get these fields.
It happens because PermissionPlugin removes not requested fields from
the SQL query for the sake of performance. And Meta.required_fields
will tell PermissionPlugin which fields have to be loaded
even if not requested directly.
Request:
GET /persons?fields[person]=full_name,email HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"email": "john.smith@example.com",
"full_name": "John Smith"
},
"id": "1",
"links": {
"self": "/persons/1"
},
"type": "person"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost:5000/persons?fields%5Bperson%5D=full_name%2Cemail"
},
"meta": {
"count": 1
}
}
Relationships¶
Create computer¶
Request:
POST /computers HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "computer",
"attributes": {
"serial": "Amstrad"
}
}
}
Response:
HTTP/1.1 201 Created
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"serial": "Amstrad"
},
"id": "1",
"links": {
"self": "/computers/1"
},
"type": "computer"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/computers/1"
}
}
Update person’s attributes and relationships in one request¶
Request:
PATCH /persons/1?include=computers HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "person",
"id": "1",
"attributes": {
"email": "john@example.com"
},
"relationships": {
"computers": {
"data": [
{
"type": "computer",
"id": "1"
}
]
}
}
}
}
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"email": "john@example.com",
"first_name": "John",
"full_name": "John Smith",
"last_name": "Smith"
},
"id": "1",
"links": {
"self": "/persons/1"
},
"relationships": {
"computers": {
"data": [
{
"id": "1",
"type": "computer"
}
]
}
},
"type": "person"
},
"included": [
{
"attributes": {
"serial": "Amstrad"
},
"id": "1",
"links": {
"self": "/computers/1"
},
"type": "computer"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/persons/1"
}
}
Get computers with owners (person relationship) and select fields at the same time¶
Request:
GET /computers?include=owner&fields[person]=full_name,email HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"serial": "Amstrad"
},
"id": "1",
"links": {
"self": "/computers/1"
},
"relationships": {
"owner": {
"data": {
"id": "1",
"type": "person"
}
}
},
"type": "computer"
}
],
"included": [
{
"attributes": {
"email": "john@example.com",
"full_name": "John Smith"
},
"id": "1",
"links": {
"self": "/persons/1"
},
"type": "person"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost:5000/computers?include=owner&fields%5Bperson%5D=email%2Cfull_name"
},
"meta": {
"count": 1
}
}