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
  }
}