FastAPI-JSONAPI

PyPI Python Version GitHub Codecov

FastAPI-JSONAPI is an extension for FastAPI that adds support for quickly building REST APIs with huge flexibility around the JSON:API 1.0 specification. It is designed to fit the complexity of real life environments so FastAPI-JSONAPI helps you to create a logical abstraction of your data called “resource”. It can interface any kind of ORMs or data storage through the concept of data layers.

Main concepts

Architecture
* JSON:API 1.0 specification: this is a very popular specification for client-server interactions through a JSON-based REST API. It helps you work in a team because it is very precise and sharable. Thanks to this specification your API can offer a lot of features such as a strong structure of request and response, filtering, pagination, sparse fieldsets, including related objects, great error formatting, etc.

* Logical data abstraction: you usually need to expose resources to clients that don’t fit your data table architecture. For example sometimes you don’t want to expose all attributes of a table, compute additional attributes or create a resource that uses data from multiple data storages.

* Data layer: the data layer is a CRUD interface between your resource manager and your data. Thanks to this you can use any data storage or ORM. There is an already full-featured data layer that uses the SQLAlchemy ORM but you can create and use your own custom data layer to use data from your data storage. You can even create a data layer that uses multiple data storage systems and ORMs, send notifications or perform custom actions during CRUD operations.

Features

FastAPI-JSONAPI has many features:

  • Relationship management - in developing

  • Powerful filtering

  • Include related objects - in developing

  • Sparse fieldsets - in developing

  • Pagination

  • Sorting

  • Permission management - in developing

  • OAuth support - in developing

User’s Guide

This part of the documentation will show you how to get started using FastAPI-JSONAPI with FastAPI.

Installation

Install FastAPI-JSONAPI with pip

pip install FastAPI-JSONAPI

The development version can be downloaded from its page at GitHub.

git clone https://github.com/mts-ai/FastAPI-JSONAPI.git
cd fastapi-jsonapi
poetry install poetry install --all-extras

Note

If you don’t have virtualenv please install it

$ pip install virtualenv

If you don’t have poetry please install it

$ pip install poetry

A minimal API

import sys
from pathlib import Path
from typing import Any, ClassVar, Dict

import uvicorn
from fastapi import APIRouter, Depends, FastAPI
from sqlalchemy import Column, Integer, Text
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from fastapi_jsonapi import RoutersJSONAPI, init
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBaseGeneric, ListViewBaseGeneric
from fastapi_jsonapi.schema_base import BaseModel
from fastapi_jsonapi.views.utils import HTTPMethod, HTTPMethodConfig
from fastapi_jsonapi.views.view_base import ViewBase

CURRENT_FILE = Path(__file__).resolve()
CURRENT_DIR = CURRENT_FILE.parent
PROJECT_DIR = CURRENT_DIR.parent.parent
DB_URL = f"sqlite+aiosqlite:///{CURRENT_DIR}/db.sqlite3"
sys.path.append(str(PROJECT_DIR))

Base = declarative_base()


class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(Text, nullable=True)


class UserAttributesBaseSchema(BaseModel):
    name: str

    class Config:
        orm_mode = True


class UserSchema(UserAttributesBaseSchema):
    """User base schema."""


def async_session() -> sessionmaker:
    engine = create_async_engine(url=make_url(DB_URL))
    _async_session = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
    return _async_session


class Connector:
    @classmethod
    async def get_session(cls):
        """
        Get session as dependency

        :return:
        """
        sess = async_session()
        async with sess() as db_session:  # type: AsyncSession
            yield db_session
            await db_session.rollback()


async def sqlalchemy_init() -> None:
    engine = create_async_engine(url=make_url(DB_URL))
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)


class SessionDependency(BaseModel):
    session: AsyncSession = Depends(Connector.get_session)

    class Config:
        arbitrary_types_allowed = True


def session_dependency_handler(view: ViewBase, dto: SessionDependency) -> Dict[str, Any]:
    return {
        "session": dto.session,
    }


class UserDetailView(DetailViewBaseGeneric):
    method_dependencies: ClassVar = {
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=session_dependency_handler,
        ),
    }


class UserListView(ListViewBaseGeneric):
    method_dependencies: ClassVar = {
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=session_dependency_handler,
        ),
    }


def add_routes(app: FastAPI):
    tags = [
        {
            "name": "User",
            "description": "",
        },
    ]

    router: APIRouter = APIRouter()
    RoutersJSONAPI(
        router=router,
        path="/users",
        tags=["User"],
        class_detail=UserDetailView,
        class_list=UserListView,
        schema=UserSchema,
        model=User,
        resource_type="user",
    )

    app.include_router(router, prefix="")
    return tags


def create_app() -> FastAPI:
    """
    Create app factory.

    :return: app
    """
    app = FastAPI(
        title="FastAPI and SQLAlchemy",
        debug=True,
        openapi_url="/openapi.json",
        docs_url="/docs",
    )
    add_routes(app)
    app.on_event("startup")(sqlalchemy_init)
    init(app)
    return app


app = create_app()

if __name__ == "__main__":
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8080,
    )

This example provides the following API structure:

URL

method

endpoint

Usage

/users

GET

user_list

Get a collection of users

/users

POST

user_list

Create a user

/users

DELETE

user_list

Delete users

/users/{user_id}

GET

user_detail

Get user details

/users/{user_id}

PATCH

user_detail

Update a user

/users/{user_id}

DELETE

user_detail

Delete a user

Request:

POST /users HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "user",
    "attributes": {
        "name": "John"
    }
  }
}

Response:

HTTP/1.1 201 Created
Content-Type: application/vnd.api+json

{
  "data": {
    "attributes": {
      "name": "John"
    },
    "id": "1",
    "links": {
      "self": "/users/1"
    },
    "type": "user"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/users/1"
  }
}

Request:

GET /users/1 HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "attributes": {
      "name": "John"
    },
    "id": "1",
    "links": {
      "self": "/users/1"
    },
    "type": "user"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/users/1"
  }
}

Request:

GET /users HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "name": "John"
      },
      "id": "1",
      "links": {
        "self": "/users/1"
      },
      "type": "user"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/users"
  },
  "meta": {
    "count": 1
  }
}

Request:

PATCH /users/1 HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": {
    "id": 1,
    "type": "user",
    "attributes": {
        "name": "Sam"
    }
  }
}

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "attributes": {
      "name": "Sam"
    },
    "id": "1",
    "links": {
      "self": "/users/1"
    },
    "type": "user"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/users/1"
  }
}

Request:

DELETE /users/1 HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "jsonapi": {
    "version": "1.0"
  },
  "meta": {
    "message": "Object successfully deleted"
  }
}

Filtering API example

from typing import Any, Union

from pydantic.fields import Field, ModelField
from sqlalchemy.orm import InstrumentedAttribute
from sqlalchemy.sql.elements import BinaryExpression, BooleanClauseList

from fastapi_jsonapi.schema_base import BaseModel


def jsonb_contains_sql_filter(
    schema_field: ModelField,
    model_column: InstrumentedAttribute,
    value: dict[Any, Any],
    operator: str,
) -> Union[BinaryExpression, BooleanClauseList]:
    """
    Any SQLA (or Tortoise) magic here

    :param schema_field:
    :param model_column:
    :param value: any dict
    :param operator: value 'jsonb_contains'
    :return: one sqla filter expression
    """
    return model_column.op("@>")(value)


class PictureSchema(BaseModel):
    """
    Now you can use `jsonb_contains` sql filter for this resource
    """

    name: str
    meta: dict[Any, Any] = Field(
        default_factory=dict,
        description="Any additional info in JSON format.",
        example={"location": "Moscow", "spam": "eggs"},
        _jsonb_contains_sql_filter_=jsonb_contains_sql_filter,
    )

Filter by jsonb contains

[
  {
    "name": "words",
    "op": "jsonb_contains",
    "val": {"location": "Moscow", "spam": "eggs"}
  }
]

Request:

GET /photos?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22jsonb_contains%22%2C%22val%22%3A%7B%22location%22%3A%22Moscow%22%2C%22spam%22%3A%22eggs%22%7D%7D%5D%0A HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "name": "pic-qwerty",
        "words": {
          "location": "Moscow",
          "spam": "eggs",
          "foo": "bar",
          "qwe": "abc"
        }
      },
      "id": "7",
      "type": "photo"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "meta": {
    "count": 1
  }
}

Other examples

# pseudo-code

class User:
    name: str = ...
    words: list[str] = ...

Filter by word

[
  {
    "name": "words",
    "op": "in",
    "val": "spam"
  }
]

Request:

GET /users?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%22spam%22%7D%5D HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "name": "Sam",
        "words": [
          "spam",
          "eggs",
          "green-apple"
        ]
      },
      "id": "2",
      "links": {
        "self": "/users/2"
      },
      "type": "user"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/users?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%22spam%22%7D%5D"
  },
  "meta": {
    "count": 1
  }
}

Filter by words

[
  {
    "name": "words",
    "op": "in",
    "val": ["bar", "eggs"]
  }
]

Request:

GET /users?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%5B%22bar%22%2C%22eggs%22%5D%7D%5D HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "name": "John",
        "words": [
          "foo",
          "bar",
          "green-grass"
        ]
      },
      "id": "1",
      "links": {
        "self": "/users/1"
      },
      "type": "user"
    },
    {
      "attributes": {
        "name": "Sam",
        "words": [
          "spam",
          "eggs",
          "green-apple"
        ]
      },
      "id": "2",
      "links": {
        "self": "/users/2"
      },
      "type": "user"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/users?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%5B%22bar%22%2C%22eggs%22%5D%7D%5D"
  },
  "meta": {
    "count": 2
  }
}

Quickstart

It’s time to write your first advanced REST API. This guide assumes you have a working understanding of FastAPI, and that you have already installed both FastAPI and FastAPI-JSONAPI. If not, then follow the steps in the Installation section.

In this section you will learn basic usage of FastAPI-JSONAPI around a small tutorial that uses the SQLAlchemy data layer. This tutorial shows you an example of a user and their computers.

Advanced example

An example of FastAPI-JSONAPI API looks like this:

"""Route creator"""

from typing import (
    Any,
    Dict,
    List,
)

from fastapi import (
    APIRouter,
    FastAPI,
)

from examples.api_for_sqlalchemy.models import (
    Child,
    Computer,
    Parent,
    ParentToChildAssociation,
    Post,
    User,
    UserBio,
)
from fastapi_jsonapi import RoutersJSONAPI
from fastapi_jsonapi.atomic import AtomicOperations

from .api.views_base import DetailViewBase, ListViewBase
from .models.schemas import (
    ChildInSchema,
    ChildPatchSchema,
    ChildSchema,
    ComputerInSchema,
    ComputerPatchSchema,
    ComputerSchema,
    ParentInSchema,
    ParentPatchSchema,
    ParentSchema,
    ParentToChildAssociationSchema,
    PostInSchema,
    PostPatchSchema,
    PostSchema,
    UserBioInSchema,
    UserBioPatchSchema,
    UserBioSchema,
    UserInSchema,
    UserPatchSchema,
    UserSchema,
)


def add_routes(app: FastAPI) -> List[Dict[str, Any]]:
    tags = [
        {
            "name": "User",
            "description": "Users API",
        },
        {
            "name": "Post",
            "description": "Posts API",
        },
    ]

    router: APIRouter = APIRouter()
    RoutersJSONAPI(
        router=router,
        path="/users",
        tags=["User"],
        class_detail=DetailViewBase,
        class_list=ListViewBase,
        model=User,
        schema=UserSchema,
        resource_type="user",
        schema_in_patch=UserPatchSchema,
        schema_in_post=UserInSchema,
    )

    RoutersJSONAPI(
        router=router,
        path="/posts",
        tags=["Post"],
        class_detail=DetailViewBase,
        class_list=ListViewBase,
        model=Post,
        schema=PostSchema,
        resource_type="post",
        schema_in_patch=PostPatchSchema,
        schema_in_post=PostInSchema,
    )

    RoutersJSONAPI(
        router=router,
        path="/user-bio",
        tags=["Bio"],
        class_detail=DetailViewBase,
        class_list=ListViewBase,
        model=UserBio,
        schema=UserBioSchema,
        resource_type="user_bio",
        schema_in_patch=UserBioPatchSchema,
        schema_in_post=UserBioInSchema,
    )

    RoutersJSONAPI(
        router=router,
        path="/parents",
        tags=["Parent"],
        class_detail=DetailViewBase,
        class_list=ListViewBase,
        model=Parent,
        schema=ParentSchema,
        resource_type="parent",
        schema_in_patch=ParentPatchSchema,
        schema_in_post=ParentInSchema,
    )

    RoutersJSONAPI(
        router=router,
        path="/children",
        tags=["Child"],
        class_detail=DetailViewBase,
        class_list=ListViewBase,
        model=Child,
        schema=ChildSchema,
        resource_type="child",
        schema_in_patch=ChildPatchSchema,
        schema_in_post=ChildInSchema,
    )

    RoutersJSONAPI(
        router=router,
        path="/parent-to-child-association",
        tags=["Parent To Child Association"],
        class_detail=DetailViewBase,
        class_list=ListViewBase,
        schema=ParentToChildAssociationSchema,
        resource_type="parent-to-child-association",
        model=ParentToChildAssociation,
    )

    RoutersJSONAPI(
        router=router,
        path="/computers",
        tags=["Computer"],
        class_detail=DetailViewBase,
        class_list=ListViewBase,
        model=Computer,
        schema=ComputerSchema,
        resource_type="computer",
        schema_in_patch=ComputerPatchSchema,
        schema_in_post=ComputerInSchema,
    )

    atomic = AtomicOperations()

    app.include_router(router, prefix="")
    app.include_router(atomic.router, prefix="")
    return tags

This example provides the following API:

url

method

endpoint

action

/users

GET

user_list

Retrieve a collection of users

/users

POST

user_list

Create a user

/users/<int:id>

GET

user_detail

Retrieve details of a user

/users/<int:id>

PATCH

user_detail

Update a user

/users/<int:id>

DELETE

user_detail

Delete a user

in developing

url

method

endpoint

action

/users/<int:id>/group

GET

computer_list

Retrieve a collection computers related to a user

/users/<int:id>/group

POST

computer_list

Create a computer related to a user

/users/<int:id>/relationships/group

GET

user_computers

Retrieve relationships between a user and computers

/users/<int:id>/relationships/computers

POST

user_computers

Create relationships between a user and computers

/users/<int:id>/relationships/computers

PATCH

user_computers

Update relationships between a user and computers

/users/<int:id>/relationships/computers

DELETE

user_computers

Delete relationships between a user and computers

/computers

GET

computer_list

Retrieve a collection of computers

/computers

POST

computer_list

Create a computer

/computers/<int:id>

GET

computer_detail

Retrieve details of a computer

/computers/<int:id>

PATCH

computer_detail

Update a computer

/computers/<int:id>

DELETE

computer_detail

Delete a computer

/computers/<int:id>/owner

GET

user_detail

Retrieve details of the owner of a computer

/computers/<int:id>/owner

PATCH

user_detail

Update the owner of a computer

/computers/<int:id>/owner

DELETE

user_detail

Delete the owner of a computer

/computers/<int:id>/relationships/owner

GET

user_computers

Retrieve relationships between a user and computers

/computers/<int:id>/relationships/owner

POST

user_computers

Create relationships between a user and computers

/computers/<int:id>/relationships/owner

PATCH

user_computers

Update relationships between a user and computers

/computers/<int:id>/relationships/owner

DELETE

user_computers

Delete relationships between a user and computers

Save this file as api.py and run it using your Python interpreter. Note that we’ve enabled messages.

$ python api.py
 * Running on http://127.0.0.1:8082/
 * Restarting with reloader

Warning

Debug mode should never be used in a production environment!

Classical CRUD operations

Create object

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"
    },
    "relationships": {
      "owner": {
        "links": {
          "related": "/computers/1/owner",
          "self": "/computers/1/relationships/owner"
        }
      }
    },
    "type": "computer"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/computers/1"
  }
}
List objects

Request:

GET /computers 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": {
          "links": {
            "related": "/computers/1/owner",
            "self": "/computers/1/relationships/owner"
          }
        }
      },
      "type": "computer"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/computers"
  },
  "meta": {
    "count": 1
  }
}
Update object

Request:

PATCH /computers/1 HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "computer",
    "id": "1",
    "attributes": {
      "serial": "New Amstrad"
    }
  }
}

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "attributes": {
      "serial": "New Amstrad"
    },
    "id": "1",
    "links": {
      "self": "/computers/1"
    },
    "relationships": {
      "owner": {
        "links": {
          "related": "/computers/1/owner",
          "self": "/computers/1/relationships/owner"
        }
      }
    },
    "type": "computer"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/computers/1"
  }
}
Delete object

Request:

DELETE /computers/1 HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "jsonapi": {
    "version": "1.0"
  },
  "meta": {
    "message": "Object successfully deleted"
  }
}

Relationships

Note

Now let’s use relationships tools. First, create 3 computers named “Halo”, “Nestor” and “Commodore”. We assume that Halo has id=2, Nestor id=3 and Commodore id=4.

Update object and his relationships

Now John sell his Halo (id=2) and buys a new computer named Nestor (id=3). So we want to link this new computer to John. John have also made a mistake in his email so let’s update these 2 things in the same time.

Request:

PATCH /users/1?include=computers HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "user",
    "id": "1",
    "attributes": {
      "email": "john@example.com"
    },
    "relationships": {
      "computers": {
        "data": [
          {
            "type": "computer",
            "id": "3"
          }
        ]
      }
    }
  }
}

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "attributes": {
      "display_name": "JOHN <john@example.com>",
      "name": "John"
    },
    "id": "1",
    "links": {
      "self": "/users/1"
    },
    "relationships": {
      "computers": {
        "data": [
          {
            "id": "3",
            "type": "computer"
          }
        ],
        "links": {
          "related": "/users/1/computers",
          "self": "/users/1/relationships/computers"
        }
      }
    },
    "type": "user"
  },
  "included": [
    {
      "attributes": {
        "serial": "Nestor"
      },
      "id": "3",
      "links": {
        "self": "/computers/3"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/3/owner",
            "self": "/computers/3/relationships/owner"
          }
        }
      },
      "type": "computer"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/users/1"
  }
}
Create relationship

Now John buys a new computer named Commodore (id=4) so let’s link it to John.

Request:

POST /users/1/relationships/computers HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "type": "computer",
      "id": "4"
    }
  ]
}

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "jsonapi": {
    "version": "1.0"
  },
  "meta": {
    "message": "Relationship successfully created"
  }
}
Check user’s computers without loading actual user

Request:

GET /users/1/computers HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "serial": "Nestor"
      },
      "id": "3",
      "links": {
        "self": "/computers/3"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/3/owner",
            "self": "/computers/3/relationships/owner"
          }
        }
      },
      "type": "computer"
    },
    {
      "attributes": {
        "serial": "Commodore"
      },
      "id": "4",
      "links": {
        "self": "/computers/4"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/4/owner",
            "self": "/computers/4/relationships/owner"
          }
        }
      },
      "type": "computer"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "first": "http://localhost:5000/computers",
    "last": "http://localhost:5000/computers?page%5Bnumber%5D=2",
    "next": "http://localhost:5000/computers?page%5Bnumber%5D=2",
    "self": "http://localhost:5000/computers"
  },
  "meta": {
    "count": 2
  }
}
Delete relationship

Now John sells his old Nestor computer, so let’s unlink it from John.

Request:

DELETE /users/1/relationships/computers HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "type": "computer",
      "id": "3"
    }
  ]
}

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "jsonapi": {
    "version": "1.0"
  },
  "meta": {
    "message": "Relationship successfully updated"
  }
}

If you want to see more examples visit JSON API 1.0 specification

Limit API methods

Sometimes you won’t need all the CRUD methods. For example, you want to create only GET, POST and GET LIST methods, so user can’t update or delete any items.

Set methods on Routers registration:

RoutersJSONAPI(
    router=router,
    path="/users",
    tags=["User"],
    class_detail=UserDetailView,
    class_list=UserListView,
    schema=UserSchema,
    model=User,
    resource_type="user",
    methods=[
        RoutersJSONAPI.Methods.GET_LIST,
        RoutersJSONAPI.Methods.POST,
        RoutersJSONAPI.Methods.GET,
    ],
)

This will limit generated views to:

URL

method

endpoint

Usage

/users

GET

user_list

Get a collection of users

/users

POST

user_list

Create a user

/users/{user_id}

GET

user_detail

Get user details

Full code example (should run “as is”):

import sys
from pathlib import Path
from typing import Any, ClassVar, Dict

import uvicorn
from fastapi import APIRouter, Depends, FastAPI
from sqlalchemy import Column, Integer, Text
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from fastapi_jsonapi import RoutersJSONAPI, init
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBaseGeneric, ListViewBaseGeneric
from fastapi_jsonapi.schema_base import BaseModel
from fastapi_jsonapi.views.utils import HTTPMethod, HTTPMethodConfig
from fastapi_jsonapi.views.view_base import ViewBase

CURRENT_FILE = Path(__file__).resolve()
CURRENT_DIR = CURRENT_FILE.parent
PROJECT_DIR = CURRENT_DIR.parent.parent
DB_URL = f"sqlite+aiosqlite:///{CURRENT_DIR}/db.sqlite3"
sys.path.append(str(PROJECT_DIR))

Base = declarative_base()


class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(Text, nullable=True)


class UserAttributesBaseSchema(BaseModel):
    name: str

    class Config:
        orm_mode = True


class UserSchema(UserAttributesBaseSchema):
    """User base schema."""


def async_session() -> sessionmaker:
    engine = create_async_engine(url=make_url(DB_URL))
    _async_session = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
    return _async_session


class Connector:
    @classmethod
    async def get_session(cls):
        """
        Get session as dependency

        :return:
        """
        sess = async_session()
        async with sess() as db_session:  # type: AsyncSession
            yield db_session
            await db_session.rollback()


async def sqlalchemy_init() -> None:
    engine = create_async_engine(url=make_url(DB_URL))
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)


class SessionDependency(BaseModel):
    session: AsyncSession = Depends(Connector.get_session)

    class Config:
        arbitrary_types_allowed = True


def session_dependency_handler(view: ViewBase, dto: SessionDependency) -> Dict[str, Any]:
    return {
        "session": dto.session,
    }


class UserDetailView(DetailViewBaseGeneric):
    method_dependencies: ClassVar = {
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=session_dependency_handler,
        ),
    }


class UserListView(ListViewBaseGeneric):
    method_dependencies: ClassVar = {
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=session_dependency_handler,
        ),
    }


def add_routes(app: FastAPI):
    tags = [
        {
            "name": "User",
            "description": "",
        },
    ]

    router: APIRouter = APIRouter()
    RoutersJSONAPI(
        router=router,
        path="/users",
        tags=["User"],
        class_detail=UserDetailView,
        class_list=UserListView,
        schema=UserSchema,
        model=User,
        resource_type="user",
        methods=[
            RoutersJSONAPI.Methods.GET_LIST,
            RoutersJSONAPI.Methods.POST,
            RoutersJSONAPI.Methods.GET,
        ],
    )

    app.include_router(router, prefix="")
    return tags


def create_app() -> FastAPI:
    """
    Create app factory.

    :return: app
    """
    app = FastAPI(
        title="FastAPI app with limited methods",
        debug=True,
        openapi_url="/openapi.json",
        docs_url="/docs",
    )
    add_routes(app)
    app.on_event("startup")(sqlalchemy_init)
    init(app)
    return app


app = create_app()

if __name__ == "__main__":
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8080,
    )

Routing

Example:

from fastapi import APIRouter, FastAPI

from examples.api_for_sqlalchemy.models import User
from examples.api_for_sqlalchemy.models.schemas import (
    UserInSchema,
    UserPatchSchema,
    UserSchema,
)
from fastapi_jsonapi import RoutersJSONAPI
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBase, ListViewBase


def add_routes(app: FastAPI):
    tags = [
        {
            "name": "User",
            "description": "Users API",
        },
    ]

    router: APIRouter = APIRouter()
    RoutersJSONAPI(
        router=router,
        path="/users",
        tags=["User"],
        class_detail=DetailViewBase,
        class_list=ListViewBase,
        model=User,
        schema=UserSchema,
        resource_type="user",
        schema_in_patch=UserPatchSchema,
        schema_in_post=UserInSchema,
    )

    app.include_router(router, prefix="")
    return tags


app = FastAPI()
add_routes(app)

Atomic Operations

Atomic Operations allows to perform multiple “operations” in a linear and atomic manner. Operations are a serialized form of the mutations allowed in the base JSON:API specification.

Clients can send an array of operations in a single request. This extension guarantees that those operations will be processed in order and will either completely succeed or fail together.

What can I do?

Atomic operations extension supports these three actions:

  • add - create a new object

  • update - update any existing object

  • remove - delete any existing object

You can send one or more atomic operations in one request.

If anything fails in one of the operations, everything will be rolled back.

Note

Only SQLAlchemy data layer supports atomic operations right now. Feel free to send PRs to add support for other data layers.

Configuration

You need to include atomic router:

from fastapi import FastAPI
from fastapi_jsonapi.atomic import AtomicOperations


def add_routes(app: FastAPI):
    atomic = AtomicOperations()
    app.include_router(atomic.router)

Default path for atomic operations is /operations

There’s a way to customize url path, you can also pass your custom APIRouter:

from fastapi import APIRouter
from fastapi_jsonapi.atomic import AtomicOperations

my_router = APIRouter(prefix="/qwerty", tags=["Atomic Operations"])

AtomicOperations(
    # you can pass custom url path
    url_path="/atomic",
    # also you can pass your custom router
    router=my_router,
)

Create some objects

Create two objects, they are not linked anyhow:

Request:

POST /operations HTTP/1.1
Content-Type: application/vnd.api+json

{
  "atomic:operations": [
    {
      "op": "add",
      "data": {
        "type": "computer",
        "attributes": {
          "name": "Commodore"
        }
      }
    },
    {
      "op": "add",
      "data": {
        "type": "user",
        "attributes": {
          "first_name": "Kate",
          "last_name": "Grey"
        }
      }
    }
  ]
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "atomic:results": [
    {
      "data": {
        "attributes": {
          "name": "Commodore"
        },
        "id": "4",
        "type": "computer"
      },
      "meta": null
    },
    {
      "data": {
        "attributes": {
          "age": null,
          "email": null,
          "first_name": "Kate",
          "last_name": "Grey",
          "status": "active"
        },
        "id": "5",
        "type": "user"
      },
      "meta": null
    }
  ]
}

Update object

Update details

Atomic operations array has to contain at least one operation. Body in each atomic action has to be as in other regular requests. For example, update any existing object:

POST /operations HTTP/1.1
Content-Type: application/vnd.api+json

{
  "atomic:operations": [
    {
      "op": "update",
      "data": {
        "id": "5",
        "type": "user",
        "attributes": {
          "last_name": "White",
          "email": "kate@example.com"
        }
      }
    }
  ]
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "atomic:results": [
    {
      "data": {
        "attributes": {
          "age": null,
          "email": "kate@example.com",
          "first_name": "Kate",
          "last_name": "White",
          "status": "active"
        },
        "id": "5",
        "type": "user"
      },
      "meta": null
    }
  ]
}
Update details and relationships

Warning

There may be issues when updating to-many relationships. This feature is not fully-tested yet.

Update already any existing computer and link it to any existing user:

Request:

POST /operations HTTP/1.1
Content-Type: application/vnd.api+json

{
  "atomic:operations": [
    {
      "op": "update",
      "data": {
        "id": "4",
        "type": "computer",
        "attributes": {
          "name": "Commodore PET"
        },
        "relationships": {
          "user": {
            "data": {
              "id": "5",
              "type": "user"
            }
          }
        }
      }
    }
  ]
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "atomic:results": [
    {
      "data": {
        "attributes": {
          "name": "Commodore PET"
        },
        "id": "4",
        "type": "computer"
      },
      "meta": null
    }
  ]
}

You can check that details and relationships are updated by fetching the object and related objects:

Request:

GET /computers/4?include=user HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": {
    "attributes": {
      "name": "Commodore PET"
    },
    "id": "4",
    "relationships": {
      "user": {
        "data": {
          "id": "5",
          "type": "user"
        }
      }
    },
    "type": "computer"
  },
  "included": [
    {
      "attributes": {
        "age": null,
        "email": "kate@example.com",
        "first_name": "Kate",
        "last_name": "White",
        "status": "active"
      },
      "id": "5",
      "type": "user"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "meta": null
}

Remove object

Operations include remove object action

You can mix any actions, for example you can create, update, remove at the same time:

Request:

POST /operations HTTP/1.1
Content-Type: application/vnd.api+json

{
  "atomic:operations": [
    {
      "op": "add",
      "data": {
        "type": "computer",
        "attributes": {
          "name": "Liza"
        },
        "relationships": {
          "user": {
            "data": {
              "id": "1",
              "type": "user"
            }
          }
        }
      }
    },
    {
      "op": "update",
      "data": {
        "id": "2",
        "type": "user_bio",
        "attributes": {
          "birth_city": "Saint Petersburg",
          "favourite_movies": "\"The Good, the Bad and the Ugly\", \"Once Upon a Time in America\""
        }
      }
    },
    {
      "op": "remove",
      "ref": {
        "id": "2",
        "type": "child"
      }
    }
  ]
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "atomic:results": [
    {
      "data": {
        "attributes": {
          "name": "Liza"
        },
        "id": "5",
        "type": "computer"
      },
      "meta": null
    },
    {
      "data": {
        "attributes": {
          "birth_city": "Saint Petersburg",
          "favourite_movies": "\"The Good, the Bad and the Ugly\", \"Once Upon a Time in America\"",
          "keys_to_ids_list": null,
        },
        "id": "2",
        "type": "user_bio"
      },
      "meta": null
    },
    {
      "data": null,
      "meta": null
    }
  ]
}
All operations remove objects

If all actions are to delete objects, empty response will be returned:

Request:

POST /operations HTTP/1.1
Content-Type: application/vnd.api+json

{
  "atomic:operations": [
    {
      "op": "remove",
      "ref": {
        "id": "6",
        "type": "computer"
      }
    },
    {
      "op": "remove",
      "ref": {
        "id": "7",
        "type": "computer"
      }
    }
  ]
}

Response:

HTTP/1.1 204 No Content

Local identifier

Sometimes you need to create an object, create another object and link it to the first one:

Create user and create bio for this user:

Request:

POST /operations HTTP/1.1
Content-Type: application/vnd.api+json

{
   "atomic:operations":[
      {
         "op":"add",
         "data":{
            "lid":"some-local-id",
            "type":"user",
            "attributes":{
               "first_name":"Bob",
               "last_name":"Pink"
            }
         }
      },
      {
         "op":"add",
         "data":{
            "type":"user_bio",
            "attributes":{
               "birth_city":"Moscow",
               "favourite_movies":"Jaws, Alien"
            },
            "relationships":{
               "user":{
                  "data":{
                     "lid":"some-local-id",
                     "type":"user"
                  }
               }
            }
         }
      }
   ]
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "atomic:results": [
    {
      "data": {
        "attributes": {
          "age": null,
          "email": null,
          "first_name": "Bob",
          "last_name": "Pink",
          "status": "active"
        },
        "id": "7",
        "type": "user"
      },
      "meta": null
    },
    {
      "data": {
        "attributes": {
          "birth_city": "Moscow",
          "favourite_movies": "Jaws, Alien"
        },
        "id": "2",
        "type": "user_bio"
      },
      "meta": null
    }
  ]
}

Many to many with local identifier

If you have many-to-many association (examples with many-to-many), atomic operations should look like this:

Request:

POST /operations HTTP/1.1
Content-Type: application/vnd.api+json

{
   "atomic:operations":[
      {
         "op":"add",
         "data":{
            "lid":"new-parent",
            "type":"parent",
            "attributes":{
               "name":"David Newton"
            }
         }
      },
      {
         "op":"add",
         "data":{
            "lid":"new-child",
            "type":"child",
            "attributes":{
               "name":"James Owen"
            }
         }
      },
      {
         "op":"add",
         "data":{
            "type":"parent-to-child-association",
            "attributes":{
               "extra_data":"Lay piece happy box."
            },
            "relationships":{
               "parent":{
                  "data":{
                     "lid":"new-parent",
                     "type":"parent"
                  }
               },
               "child":{
                  "data":{
                     "lid":"new-child",
                     "type":"child"
                  }
               }
            }
         }
      }
   ]
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "atomic:results": [
    {
      "data": {
        "attributes": {
          "name": "David Newton"
        },
        "id": "1",
        "type": "parent"
      },
      "meta": null
    },
    {
      "data": {
        "attributes": {
          "name": "James Owen"
        },
        "id": "1",
        "type": "child"
      },
      "meta": null
    },
    {
      "data": {
        "attributes": {
          "extra_data": "Lay piece happy box."
        },
        "id": "1",
        "type": "parent-to-child-association"
      },
      "meta": null
    }
  ]
}

Check that objects and relationships were created. Pass includes in the url path, like this /parent-to-child-association/1?include=parent,child

Request:

GET /parent-to-child-association/1?include=parent%2Cchild HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": {
    "attributes": {
      "extra_data": "Lay piece happy box."
    },
    "id": "1",
    "relationships": {
      "child": {
        "data": {
          "id": "1",
          "type": "child"
        }
      },
      "parent": {
        "data": {
          "id": "1",
          "type": "parent"
        }
      }
    },
    "type": "parent-to-child-association"
  },
  "included": [
    {
      "attributes": {
        "name": "James Owen"
      },
      "id": "1",
      "type": "child"
    },
    {
      "attributes": {
        "name": "David Newton"
      },
      "id": "1",
      "type": "parent"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "meta": null
}

Errors

If any action on the operations list fails, everything will be rolled back and an error will be returned. Example:

Request:

POST /operations HTTP/1.1
Content-Type: application/vnd.api+json

{
  "atomic:operations": [
    {
      "op": "add",
      "data": {
        "type": "computer",
        "attributes": {
          "name": "Commodore"
        }
      }
    },
    {
      "op": "update",
      "data": {
        "type": "user_bio",
        "attributes": {
          "favourite_movies": "Saw"
        }
      }
    }
  ]
}

Response:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "detail": {
    "data": {
      "attributes": {
        "favourite_movies": "Saw"
      },
      "id": null,
      "lid": null,
      "relationships": null,
      "type": "user_bio"
    },
    "errors": [
      {
        "loc": [
          "data",
          "attributes",
          "birth_city"
        ],
        "msg": "field required",
        "type": "value_error.missing"
      },
      {
        "loc": [
          "data",
          "id"
        ],
        "msg": "none is not an allowed value",
        "type": "type_error.none.not_allowed"
      }
    ],
    "message": "Validation error on operation update",
    "ref": null
  }
}

Since update action requires id field and user-bio update schema requires birth_city field, the app rollbacks all actions and computer is not saved in DB (and user-bio is not updated).

Error is not in JSON:API style yet, PRs are welcome.

Notes

Note

See examples for SQLAlchemy in the repo, all examples are based on it.

Note

Atomic Operations provide current_atomic_operation context variable. Usage example can be found in tests test_current_atomic_operation.

Warning

Field “href” is not supported yet. Resource can be referenced only by the “type” field.

Relationships resources are not implemented yet, so updating relationships directly through atomic operations is not supported too (see skipped tests).

Includes in the response body are not supported (and not planned, until you PR it)

View Dependencies

As you already know, in the process of its work, FastAPI-JSONAPI interacts between application layers. Sometimes there are things that are necessary to process requests but are only computable at runtime. In order for ResourceManager and DataLayer to use such things, there is a mechanism called method_dependencies.

The most common cases of such things are database session and access handling. The example below demonstrates some simple implementation of these ideas using sqlalchemy.

Example:

from __future__ import annotations

from typing import ClassVar, Dict

from fastapi import Depends, Header
from pydantic import BaseModel
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker
from typing_extensions import Annotated

from fastapi_jsonapi.exceptions import Forbidden
from fastapi_jsonapi.misc.sqla.generics.base import (
    DetailViewBaseGeneric,
    ListViewBaseGeneric,
)
from fastapi_jsonapi.views.utils import (
    HTTPMethod,
    HTTPMethodConfig,
)
from fastapi_jsonapi.views.view_base import ViewBase


def get_async_sessionmaker() -> sessionmaker:
    _async_session = sessionmaker(
        bind=create_async_engine(
            url=make_url(
                f"sqlite+aiosqlite:///tmp/db.sqlite3",
            )
        ),
        class_=AsyncSession,
        expire_on_commit=False,
    )
    return _async_session


async def async_session_dependency():
    """
    Get session as dependency

    :return:
    """
    session_maker = get_async_sessionmaker()
    async with session_maker() as db_session:  # type: AsyncSession
        yield db_session
        await db_session.rollback()


class SessionDependency(BaseModel):
    session: AsyncSession = Depends(async_session_dependency)

    class Config:
        arbitrary_types_allowed = True


async def common_handler(view: ViewBase, dto: SessionDependency) -> dict:
    return {"session": dto.session}


async def check_that_user_is_admin(x_auth: Annotated[str, Header()]):
    if x_auth != "admin":
        raise Forbidden(detail="Only admin user have permissions to this endpoint")


class AdminOnlyPermission(BaseModel):
    is_admin: bool | None = Depends(check_that_user_is_admin)


class DetailView(DetailViewBaseGeneric):
    method_dependencies: ClassVar[Dict[HTTPMethod, HTTPMethodConfig]] = {
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=common_handler,
        ),
    }


class ListView(ListViewBaseGeneric):
    method_dependencies: ClassVar[Dict[HTTPMethod, HTTPMethodConfig]] = {
        HTTPMethod.GET: HTTPMethodConfig(dependencies=AdminOnlyPermission),
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=common_handler,
        ),
    }

In this example, the focus should be on the HTTPMethod and HTTPMethodConfig entities. By setting the method_dependencies attribute, you can set FastAPI dependencies for endpoints, as well as manage the creation of additional kwargs needed to initialize the DataLayer.

Dependencies can be any Pydantic model containing Depends as default values. It’s really the same as if you defined the dependency session for the endpoint as:

from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession

app = FastAPI()


@app.get("/items")
def get_items(session: AsyncSession = Depends(async_session_dependency)):
    pass

Dependencies do not have to be used to generate DataLayer keys and can be used for any purpose, as is the case with the check_that_user_is_admin function, which is used to check permissions. In case the header “X-AUTH” is not equal to “admin”, the Forbidden response will be returned.

In this case, if you do not set the “X-AUTH” header, it will work like this

Request:

GET /users HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 403 Forbidden
Content-Type: application/vnd.api+json

{
   "errors": [
      {
         "detail": "Only admin user have permissions to this endpoint",
         "status_code": 403,
         "title": "Forbidden"
      }
   ]
}

and when “X-AUTH” is set, it will work like this

Request:

GET /users HTTP/1.1
Content-Type: application/vnd.api+json
X-AUTH: admin

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "name": "John"
      },
      "id": "1",
      "links": {
        "self": "/users/1"
      },
      "type": "user"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/users"
  },
  "meta": {
    "count": 1
  }
}

Handlers

As noted above, dependencies can be used to create a kwargs for a DataLayer. To do this, you need to define prepare_data_layer_kwargs in HTTPMethodConfig. This is a callable object which can be synchronous or asynchronous.

Its signature should look like this

async def my_handler(view: ViewBase, dto: BaseModel) -> Dict[str, Any]:
    pass

or this

async def my_handler(view: ViewBase) -> Dict[str, Any]:
    pass

In the case of dto, it is an instance of the class corresponds to what is in HTTPMethodConfig.dependencies and should only be present in the function signature if dependencies is not None.

The HTTPMethodConfig.ALL method has special behavior. When declared, its dependencies will be passed to each endpoint regardless of the existence of other configs.

Explaining with a specific example, in the case when HTTPMethod.ALL is declared and it has dependencies, and also a method such as HTTPMethod.GET also has dependencies, the signature for the HTTPMethod.GET handler will be a union of dependencies

Example:

from typing import ClassVar

from fastapi import Depends
from pydantic import BaseModel

from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBaseGeneric
from fastapi_jsonapi.views.utils import HTTPMethod, HTTPMethodConfig
from fastapi_jsonapi.views.view_base import ViewBase


def one():
    return 1


def two():
    return 2


class CommonDependency(BaseModel):
    key_1: int = Depends(one)


class GetDependency(BaseModel):
    key_2: int = Depends(two)


class DependencyMix(CommonDependency, GetDependency):
    pass


def common_handler(view: ViewBase, dto: CommonDependency) -> dict:
    return {"key_1": dto.key_1}


def get_handler(view: ViewBase, dto: DependencyMix):
    return {"key_2": dto.key_2}


class DetailView(DetailViewBaseGeneric):
    method_dependencies: ClassVar = {
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=CommonDependency,
            prepare_data_layer_kwargs=common_handler,
        ),
        HTTPMethod.GET: HTTPMethodConfig(
            dependencies=GetDependency,
            prepare_data_layer_kwargs=get_handler,
        ),
    }

In this case DataLayer.__init__ will get {"key_1": 1, "key_2": 2} as kwargs.

You can take advantage of this knowledge and do something with the key_1 value, because before entering the DataLayer, the results of both handlers are defined as:

dl_kwargs = common_handler(view, dto)
dl_kwargs.update(get_handler(view, dto))

You can override the value of key_1 in the handler

def get_handler(view: ViewBase, dto: DependencyMix):
    return {"key_1": 42, "key_2": dto.key_2}

or just overriding the dependency

def handler(view, dto):
    return 42

class GetDependency(BaseModel):
    key_1: int = Depends(handler)
    key_2: int = Depends(two)

In both cases DataLayer.__init__ will get {"key_1": 42, "key_2": 2} as kwargs

Filtering

FastAPI-JSONAPI has a very flexible filtering system. The filtering system is directly attached to the data layer used by the ResourceList manager. These examples show the filtering interface for SQLAlchemy’s data layer but you can use the same interface for your custom data layer’s filtering implementation as well. The only requirement is that you have to use the “filter” query string parameter to filter according to the JSON:API 1.0 specification.

Note

Examples are not urlencoded for a better readability

SQLAlchemy

The filtering system of SQLAlchemy’s data layer has exactly the same interface as the one used in Flask-Restless. So this is a first example:

GET /users?filter=[{"name":"first_name","op":"eq","val":"John"}] HTTP/1.1
Accept: application/vnd.api+json

In this example we want to retrieve user records for people named John. So we can see that the filtering interface completely fits that of SQLAlchemy: a list a filter information.

name:

the name of the field you want to filter on

op:

the operation you want to use (all SQLAlchemy operations are available)

val:

the value that you want to compare. You can replace this by “field” if you want to compare against the value of another field

Example with field:

GET /users?filter=[{"name":"first_name","op":"eq","field":"birth_date"}] HTTP/1.1
Accept: application/vnd.api+json

In this example, we want to retrieve people whose name is equal to their birth_date. This example is absurd, it’s just here to explain the syntax of this kind of filter.

If you want to filter through relationships you can do that:

[
  {
    "name": "group",
    "op": "any",
    "val": {
      "name": "name",
      "op": "ilike",
      "val": "%admin%"
    }
  }
]
GET [{"name":"group","op":"any","val":{"name":"name","op":"ilike","val":"%admin%"}}] HTTP/1.1
Accept: application/vnd.api+json

Note

When you filter on relationships use the “any” operator for “to many” relationships and the “has” operator for “to one” relationships.

There is a shortcut to achieve the same filtering:

GET /users?filter=[{"name":"group.name","op":"ilike","val":"%admin%"}] HTTP/1.1
Accept: application/vnd.api+json

You can also use boolean combination of operations:

[
  {
    "name":"group.name",
    "op":"ilike",
    "val":"%admin%"
  },
  {
    "or": [
      {
        "not": {
          "name": "first_name",
          "op": "eq",
          "val": "John"
        }
      },
      {
        "and": [
          {
            "name": "first_name",
            "op": "like",
            "val": "%Jim%"
          },
          {
            "name": "date_create",
            "op": "gt",
            "val": "1990-01-01"
          }
        ]
      }
    ]
  }
]
GET /users?filter=[{"name":"group.name","op":"ilike","val":"%admin%"},{"or":[{"not":{"name":"first_name","op":"eq","val":"John"}},{"and":[{"name":"first_name","op":"like","val":"%Jim%"},{"name":"date_create","op":"gt","val":"1990-01-01"}]}]}] HTTP/1.1
Accept: application/vnd.api+json

Filtering records by a field that is null

GET /users?filter=[{"name":"name","op":"is_","val":null}] HTTP/1.1
Accept: application/vnd.api+json

Filtering records by a field that is not null

GET /users?filter=[{"name":"name","op":"isnot","val":null}] HTTP/1.1
Accept: application/vnd.api+json

Common available operators:

  • any: used to filter on “to many” relationships

  • between: used to filter a field between two values

  • endswith: checks if field ends with a string

  • eq: checks if field is equal to something

  • ge: checks if field is greater than or equal to something

  • gt: checks if field is greater than something

  • has: used to filter on “to one” relationships

  • ilike: checks if field contains a string (case insensitive)

  • in_: checks if field is in a list of values

  • is_: checks if field is a value

  • isnot: checks if field is not a value

  • like: checks if field contains a string

  • le: checks if field is less than or equal to something

  • lt: checks if field is less than something

  • match: checks if field matches against a string or pattern

  • ne: checks if field is not equal to something

  • notilike: checks if field does not contain a string (case insensitive)

  • notin_: checks if field is not in a list of values

  • notlike: checks if field does not contain a string

  • startswith: checks if field starts with a string

Note

Available operators depend on the field type in your model

Simple filters

Simple filters add support for a simplified form of filters and support only the eq operator. Each simple filter is transformed into a full filter and appended to the list of filters.

For example

GET /users?filter[first_name]=John HTTP/1.1
Accept: application/vnd.api+json

equals:

GET /users?filter=[{"name":"first_name","op":"eq","val":"John"}] HTTP/1.1
Accept: application/vnd.api+json

You can also use more than one simple filter in a request:

GET /users?filter[first_name]=John&filter[gender]=male HTTP/1.1
Accept: application/vnd.api+json

which is equal to:

[
   {
      "name":"first_name",
      "op":"eq",
      "val":"John"
   },
   {
      "name":"gender",
      "op":"eq",
      "val":"male"
   }
]
GET /users?filter=[{"name":"first_name","op":"eq","val":"John"},{"name":"gender","op":"eq","val":"male"}] HTTP/1.1

You can also use relationship attribute in a request:

GET /users?filter[group_id]=1 HTTP/1.1
Accept: application/vnd.api+json

which is equal to:

GET /users?filter=[{"name":"group.id","op":"eq","val":"1"}] HTTP/1.1

Custom SQL filtering

Sometimes you need custom filtering that’s not supported natively. You can define new filtering rules as in this example:

Prepare pydantic schema which is used in RoutersJSONAPI as schema

schemas/picture.py:

from typing import Any, Union

from pydantic.fields import Field, ModelField
from sqlalchemy.orm import InstrumentedAttribute
from sqlalchemy.sql.elements import BinaryExpression, BooleanClauseList

from fastapi_jsonapi.schema_base import BaseModel


def jsonb_contains_sql_filter(
    schema_field: ModelField,
    model_column: InstrumentedAttribute,
    value: dict[Any, Any],
    operator: str,
) -> Union[BinaryExpression, BooleanClauseList]:
    """
    Any SQLA (or Tortoise) magic here

    :param schema_field:
    :param model_column:
    :param value: any dict
    :param operator: value 'jsonb_contains'
    :return: one sqla filter expression
    """
    return model_column.op("@>")(value)


class PictureSchema(BaseModel):
    """
    Now you can use `jsonb_contains` sql filter for this resource
    """

    name: str
    meta: dict[Any, Any] = Field(
        default_factory=dict,
        description="Any additional info in JSON format.",
        example={"location": "Moscow", "spam": "eggs"},
        _jsonb_contains_sql_filter_=jsonb_contains_sql_filter,
    )

Declare models as usual, create routes as usual.

Search for objects

Note

Note that url has to be quoted. It’s unquoted only for an example

Request:

GET /pictures?filter=[{"name":"picture.meta","op":"jsonb_contains","val":{"location":"Moscow"}}] HTTP/1.1
Accept: application/vnd.api+json

Filter value has to be a valid JSON:

[
   {
      "name":"picture.meta",
      "op":"jsonb_contains",
      "val":{
         "location":"Moscow"
      }
   }
]

Client generated id

According to the specification JSON:API doc it is possible to create an id on the client and pass it to the server. Let’s define the id type as a UUID.

Request:

POST /users HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "user",
    "attributes": {
        "name": "John"
    },
    "id": "867ab602-d9f7-44d0-8d3a-d2ae6d96b3a7"
  }
}

Response:

HTTP/1.1 201 Created
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "user",
    "id": "867ab602-d9f7-44d0-8d3a-d2ae6d96b3a7",
    "attributes": {
      "name": "John"
    },
    "links": {
      "self": "/users/867ab602-d9f7-44d0-8d3a-d2ae6d96b3a7"
    },
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/users/867ab602-d9f7-44d0-8d3a-d2ae6d96b3a7"
  }
}

In order to do this you need to define an id with the Field keyword client_can_set_id in the schema or schema_in_post.

Example:

import sys
from pathlib import Path
from typing import ClassVar

import uvicorn
from fastapi import APIRouter, Depends, FastAPI
from fastapi_jsonapi.schema_base import Field, BaseModel as PydanticBaseModel
from sqlalchemy import Column, Integer, Text
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from fastapi_jsonapi import RoutersJSONAPI, init
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBaseGeneric, ListViewBaseGeneric
from fastapi_jsonapi.views.utils import HTTPMethod, HTTPMethodConfig
from fastapi_jsonapi.views.view_base import ViewBase

CURRENT_FILE = Path(__file__).resolve()
CURRENT_DIR = CURRENT_FILE.parent
PROJECT_DIR = CURRENT_DIR.parent.parent
DB_URL = f"sqlite+aiosqlite:///{CURRENT_DIR.absolute()}/db.sqlite3"
sys.path.append(str(PROJECT_DIR))

Base = declarative_base()


class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, autoincrement=False)
    name = Column(Text, nullable=True)


class BaseModel(PydanticBaseModel):
    class Config:
        orm_mode = True


class UserAttributesBaseSchema(BaseModel):
    name: str


class UserSchema(UserAttributesBaseSchema):
    """User base schema."""


class UserPatchSchema(UserAttributesBaseSchema):
    """User PATCH schema."""


class UserInSchema(UserAttributesBaseSchema):
    """User input schema."""

    id: int = Field(client_can_set_id=True)


async def get_session():
    sess = sessionmaker(
        bind=create_async_engine(url=make_url(DB_URL)),
        class_=AsyncSession,
        expire_on_commit=False,
    )
    async with sess() as db_session:  # type: AsyncSession
        yield db_session
        await db_session.rollback()


async def sqlalchemy_init() -> None:
    engine = create_async_engine(url=make_url(DB_URL))
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)


class SessionDependency(BaseModel):
    session: AsyncSession = Depends(get_session)

    class Config:
        arbitrary_types_allowed = True


def session_dependency_handler(view: ViewBase, dto: SessionDependency) -> dict:
    return {"session": dto.session}


class UserDetailView(DetailViewBaseGeneric):
    method_dependencies: ClassVar = {
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=session_dependency_handler,
        )
    }


class UserListView(ListViewBaseGeneric):
    method_dependencies: ClassVar = {
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=session_dependency_handler,
        )
    }


def add_routes(app: FastAPI):
    tags = [
        {
            "name": "User",
            "description": "",
        },
    ]

    router: APIRouter = APIRouter()
    RoutersJSONAPI(
        router=router,
        path="/users",
        tags=["User"],
        class_detail=UserDetailView,
        class_list=UserListView,
        schema=UserSchema,
        resource_type="user",
        schema_in_patch=UserPatchSchema,
        schema_in_post=UserInSchema,
        model=User,
    )

    app.include_router(router, prefix="")
    return tags


def create_app() -> FastAPI:
    """
    Create app factory.

    :return: app
    """
    app = FastAPI(
        title="FastAPI and SQLAlchemy",
        debug=True,
        openapi_url="/openapi.json",
        docs_url="/docs",
    )
    add_routes(app)
    app.on_event("startup")(sqlalchemy_init)
    init(app)
    return app


app = create_app()

if __name__ == "__main__":
    current_file_name = CURRENT_FILE.name.replace(CURRENT_FILE.suffix, "")
    uvicorn.run(
        f"{current_file_name}:app",
        host="0.0.0.0",
        port=8084,
        reload=True,
        app_dir=str(CURRENT_DIR),
    )

In case the key client_can_set_id is not set, the id field will be ignored in post requests.

In fact, the library deviates slightly from the specification and allows you to use any type, not just UUID. Just define the one you need in the Pydantic model to do it.

Logical data abstraction

The first thing to do in FastAPI-JSONAPI is to create a logical data abstraction. This part of the API describes schemas of resources exposed by the API that are not an exact mapping of the data architecture. Pydantic is a very popular serialization/deserialization library that offers a lot of features to abstract your data architecture. Moreover there is another library called pydantic that fits the JSON:API 1.0 specification and provides FastAPI integration.

Example:

In this example, let’s assume that we have two legacy models, User and Computer, and we want to create an abstraction on top of them.

from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)
    birth_date = Column(String)
    password = Column(String)


class Computer(Base):
    computer_id = Column(Integer, primary_key=True)
    serial = Column(String)
    user_id = Column(Integer, ForeignKey('user.id'))
    user = relationship('User', backref=backref('computers'))

Now let’s create the logical abstraction to illustrate this concept.

from pydantic import (
    BaseModel,
    Field,
)
from typing import List
from datetime import datetime


class UserSchema(BaseModel):
    class Config:
        orm_mode = True

    id: int
    name: str
    email: str
    birth_date: datetime
    computers: List['ComputerSchema']


class ComputerSchema(BaseModel):
    class Config:
        orm_mode = True

    id: int
    serial: str
    owner: UserSchema

You can see several differences between models and schemas exposed by the API.

First, take a look at the User compared to UserSchema:

  • We can see that User has an attribute named “password” and we don’t want to expose it through the api so it is not set in UserSchema

  • UserSchema has an attribute named “display_name” that is the result of concatenation of name and email

  • In the “computers” Relationship() defined on UserSchema we have set the id_field to “computer_id” as that is the primary key on the Computer(db.model). Without setting id_field the relationship looks for a field called “id”.

Second, take a look at the Computer compared to ComputerSchema:

  • The attribute computer_id is exposed as id for consistency of the api

  • The user relationship between Computer and User is exposed in ComputerSchema as owner because it is more explicit

As a result you can see that you can expose your data in a very flexible way to create the API of your choice on top of your data architecture.

Data layer

The data layer is a CRUD interface between resource manager and data. It is a very flexible system to use any ORM or data storage. You can even create a data layer that uses multiple ORMs and data storage to manage your own objects. The data layer implements a CRUD interface for objects and relationships. It also manages pagination, filtering and sorting.

FastAPI-JSONAPI has a full-featured data layer that uses the popular ORM SQLAlchemy.

To configure the data layer you have to set its required parameters in the resource manager.

Example:

from fastapi import FastAPI

from fastapi_jsonapi import RoutersJSONAPI
from fastapi_jsonapi.data_layers.base import BaseDataLayer
from fastapi_jsonapi.data_layers.sqla_orm import SqlalchemyDataLayer
from fastapi_jsonapi.views.detail_view import DetailViewBase
from fastapi_jsonapi.views.list_view import ListViewBase


class MyCustomDataLayer(BaseDataLayer):
    """Overload abstract methods here"""

    ...


class MyCustomSqlaDataLayer(SqlalchemyDataLayer):
    """Overload any methods here"""

    async def before_delete_objects(self, objects: list, view_kwargs: dict):
        raise Exception("not allowed to delete objects")


class UserDetailView(DetailViewBase):
    data_layer_cls = MyCustomDataLayer


class UserListView(ListViewBase):
    data_layer_cls = MyCustomSqlaDataLayer


app = FastAPI()
RoutersJSONAPI(
    app,
    # ...
    class_detail=UserDetailView,
    class_list=UserListView,
    # ...
)

Define relationships

As noted in quickstart, objects can accept a relationships. In order to make it technically possible to create, update, and modify relationships, you must declare a RelationShipInfo when creating a schema.

As an example, let’s say you have a user model, their biography, and the computers they own. The user and biographies are connected by To-One relationship, the user and computers are connected by To-Many relationship

Models:

from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship

from examples.api_for_sqlalchemy.extensions.sqlalchemy import Base
from examples.api_for_sqlalchemy.utils.sqlalchemy.base_model_mixin import BaseModelMixin


class User(Base, BaseModelMixin):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, autoincrement=True)
    name: str = Column(String)

    posts = relationship("Post", back_populates="user", uselist=True)
    bio = relationship("UserBio", back_populates="user", uselist=False)
    computers = relationship("Computer", back_populates="user", uselist=True)


class Computer(Base, BaseModelMixin):
    __tablename__ = "computers"

    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String, nullable=False)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
    user = relationship("User", back_populates="computers")


class UserBio(Base, BaseModelMixin):
    __tablename__ = "user_bio"
    id = Column(Integer, primary_key=True, autoincrement=True)
    birth_city: str = Column(String, nullable=False, default="", server_default="")
    favourite_movies: str = Column(String, nullable=False, default="", server_default="")
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True)
    user = relationship("User", back_populates="bio", uselist=False)

Schemas:

from typing import Optional

from pydantic import BaseModel as PydanticBaseModel

from fastapi_jsonapi.schema_base import Field, RelationshipInfo


class BaseModel(PydanticBaseModel):
    class Config:
        orm_mode = True


class UserBaseSchema(BaseModel):
    id: int
    name: str
    bio: Optional["UserBioSchema"] = Field(
        relationship=RelationshipInfo(
            resource_type="user_bio",
        ),
    )
    computers: Optional["ComputerSchema"] = Field(
        relationship=RelationshipInfo(
            resource_type="computer",
            many=True,
        ),
    )


class UserSchema(BaseModel):
    id: int
    name: str


class UserBioBaseSchema(BaseModel):
    birth_city: str
    favourite_movies: str
    keys_to_ids_list: dict[str, list[int]] = None

    user: "UserSchema" = Field(
        relationship=RelationshipInfo(
            resource_type="user",
        ),
    )


class ComputerBaseSchema(BaseModel):
    id: int
    name: str
    user: Optional["UserSchema"] = Field(
        relationship=RelationshipInfo(
            resource_type="user",
        ),
    )

Configuration

You have access to 5 configuration keys:

  • PAGE_SIZE: the number of items in a page (default is 30)

  • MAX_PAGE_SIZE: the maximum page size. If you specify a page size greater than this value you will receive a 400 Bad Request response.

  • MAX_INCLUDE_DEPTH: the maximum length of an include through schema relationships

  • ALLOW_DISABLE_PAGINATION: if you want to disallow to disable pagination you can set this configuration key to False

  • CATCH_EXCEPTIONS: if you want fastapi_jsonapi to catch all exceptions and return them as JsonApiException (default is True)

Sparse fieldsets

You can restrict the fields returned by your API using the query string parameter called “fields”. It is very useful for performance purposes because fields not returned are not resolved by the API. You can use the “fields” parameter on any kind of route (classical CRUD route or relationships route) and any kind of HTTP methods as long as the method returns data.

Note

Examples are not URL encoded for better readability

The syntax of the fields parameter is

?fields[<resource_type>]=<list of fields to return>

Example:

GET /users?fields[user]=display_name HTTP/1.1
Accept: application/vnd.api+json

In this example user’s display_name is the only field returned by the API. No relationship links are returned so the response is very fast because the API doesn’t have to do any expensive computation of relationship links.

You can manage returned fields for the entire response even for included objects

Example:

If you don’t want to compute relationship links for included computers of a user you can do something like this

GET /users/1?include=computers&fields[computer]=serial HTTP/1.1
Accept: application/vnd.api+json

And of course you can combine both like this:

Example:

GET /users/1?include=computers&fields[computer]=serial&fields[user]=name,computers HTTP/1.1
Accept: application/vnd.api+json

Warning

If you want to use both “fields” and “include”, don’t forget to specify the name of the relationship in “fields”; if you don’t, the include wont work.

Pagination

When you use the default implementation of the get method on a ResourceList your results will be paginated by default. Default pagination size is 30 but you can manage it from querystring parameter named “page”.

Note

Examples are not URL encoded for a better readability

Size

You can control page size like this:

GET /users?page[size]=10 HTTP/1.1
Accept: application/vnd.api+json

Number

You can control page number like this:

GET /users?page[number]=2 HTTP/1.1
Accept: application/vnd.api+json

Size + Number

Of course, you can control both like this:

GET /users?page[size]=10&page[number]=2 HTTP/1.1
Accept: application/vnd.api+json

Disable pagination

You can disable pagination by setting size to 0

GET /users?page[size]=0 HTTP/1.1
Accept: application/vnd.api+json

Sorting

You can sort results using the query string parameter named “sort”

Note

Examples are not URL encoded for better readability

Example:

GET /users?sort=name HTTP/1.1
Accept: application/vnd.api+json

Multiple sort

You can sort on multiple fields like this:

GET /users?sort=name,birth_date HTTP/1.1
Accept: application/vnd.api+json

Descending sort

You can in descending order using a minus symbol, “-”, like this:

GET /users?sort=-name HTTP/1.1
Accept: application/vnd.api+json

Multiple sort + Descending sort

Of course, you can combine both like this:

GET /users?sort=-name,birth_date HTTP/1.1
Accept: application/vnd.api+json

Errors

The JSON:API 1.0 specification recommends to return errors like this:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/vnd.api+json

{
  "errors": [
    {
      "status": "422",
      "source": {
        "pointer":"/data/attributes/first-name"
      },
      "title":  "Invalid Attribute",
      "detail": "First name must contain at least three characters."
    }
  ],
  "jsonapi": {
    "version": "1.0"
  }
}

The “source” field gives information about the error if it is located in data provided or in a query string parameter.

The previous example shows an error located in data provided. The following example shows error in the query string parameter “include”:

HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json

{
  "errors": [
    {
      "status": "400",
      "source": {
        "parameter": "include"
      },
      "title":  "BadRequest",
      "detail": "Include parameter is invalid"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  }
}

FastAPI-JSONAPI provides two kinds of helpers for displaying errors:

* the exceptions module: you can import a lot of exceptions from this module that helps you to raise exceptions that will be well-formatted according to the JSON:API 1.0 specification

When you create custom code for your API I recommend using exceptions from the FastAPI-JSONAPI’s exceptions module to raise errors because HTTPException-based exceptions are caught and rendered according to the JSON:API 1.0 specification.

You can raise an exception in any point yor app. ResourceManager, DataLayer, etc. All of the exceptions defined by FastAPI-JSONAPI will handled by the root handler fastapi_jsonapi.exceptions.handlers.base_exception_handler and we’ll see pretty JSON:API spec output

Example:

from fastapi_jsonapi.exceptions import BadRequest
from fastapi_jsonapi.schema import BaseJSONAPIDataInSchema, JSONAPIResultDetailSchema
from fastapi_jsonapi.views.list_view import ListViewBase


class CustomErrorView(ListViewBase):
    def post_resource_list_result(
        self,
        data_create: BaseJSONAPIDataInSchema,
        **extra_view_deps,
    ) -> JSONAPIResultDetailSchema:
        try:
            # any logic here
            pass
        except Exception:
            raise BadRequest(detail="My custom err")

Request:

POST /computers HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "computer",
    "attributes": {
      "name": "John"
    }
  }
}

Response:

HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json

{
   "errors":[
      {
         "detail":"My custom err",
         "source":{"pointer":""},
         "status_code":400,
         "title":"Bad Request"
      }
   ]
}

Permission

in developing

OAuth

in developing

Package fastapi_jsonapi index

fastapi_jsonapi.data_layers.fields.enum module

Base enum module.

class fastapi_jsonapi.data_layers.fields.enum.Enum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

Bases: MixinEnum

Base enum class.

All used non-integer enumerations must inherit from this class.

class fastapi_jsonapi.data_layers.fields.enum.IntEnum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

Bases: MixinIntEnum

Base IntEnum class.

All used integer enumerations must inherit from this class.

fastapi_jsonapi.data_layers.fields.mixins module

Enum mixin module.

class fastapi_jsonapi.data_layers.fields.mixins.MixinEnum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

Bases: Enum

Extension over enum class from standard library.

classmethod inverse()

Return all inverted items sequence.

classmethod keys()

Get all field keys from Enum.

classmethod names()

Get all field names.

classmethod value_to_enum(value)

Convert value to enum.

classmethod values()

Get all values from Enum.

class fastapi_jsonapi.data_layers.fields.mixins.MixinIntEnum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

Bases: IntEnum

Здесь пришлось дублировать код, чтобы обеспечить совместимость с FastAPI и Pydantic.

Основная проблема - данные либы определяют валидаторы для стандартной библиотеки enum, используя вызов issubclass. И для стандартного IntEnum есть отдельная ветка issubclass(IntEnum), в которой происходят специальные преобразования, например, аргументы из запроса конвертируются в тип int. Поэтому OurEnum(int, Enum) не срабатывает по условию issubclass(obj, IntEnum) и выбираются неверные валидаторы и конверторы. А код ниже пришлось задублировать, так как у стандартного Enum есть метакласс, который разрешает только такую цепочку наследования: NewEnum(клас_тип, миксин_без_типа_1, …, миксин_без_типа_n, Enum) По этому правилу нельзя построить наследование, добавляющее миксин без типа к стандартному IntEnum: NewEnum(our_mixin, IntEnum), так как IntEnum = (int, Enum) Поэтому пока остается такое решение до каких-либо исправлений со стороны разработчиков либы, либо появления более гениальных идей

classmethod inverse()

Return all inverted items sequence.

classmethod keys()

Get all field keys from Enum.

classmethod names()

Get all field names.

classmethod value_to_enum(value)

Convert value to enum.

classmethod values()

Get all values from Enum.

fastapi_jsonapi.data_layers.filtering.sqlalchemy module

Helper to create sqlalchemy filters according to filter querystring parameter

class fastapi_jsonapi.data_layers.filtering.sqlalchemy.RelationshipFilteringInfo(*, target_schema: Type[TypeSchema], model: Type[TypeModel], aliased_model: AliasedClass, join_column: InstrumentedAttribute)

Bases: BaseModel

class Config

Bases: object

arbitrary_types_allowed = True
aliased_model: AliasedClass
join_column: InstrumentedAttribute
model: Type[TypeModel]
target_schema: Type[TypeSchema]
fastapi_jsonapi.data_layers.filtering.sqlalchemy.build_filter_expression(schema_field: ModelField, model_column: InstrumentedAttribute, operator: str, value: Any) BinaryExpression

Builds sqlalchemy filter expression, like YourModel.some_field == value

Custom sqlalchemy filtering logic can be created in a schemas field for any operator To implement a new filtering logic (override existing or create a new one) create a method inside a field following this pattern: _<your_op_name>_sql_filter_

Parameters:
  • schema_field – schemas field instance

  • model_column – sqlalchemy column instance

  • operator – your operator, for example: “eq”, “in”, “ilike_str_array”, …

  • value – filtering value

fastapi_jsonapi.data_layers.filtering.sqlalchemy.build_filter_expressions(filter_item: Dict, target_schema: Type[TypeSchema], target_model: Type[TypeModel], relationships_info: Dict[str, RelationshipFilteringInfo]) BinaryExpression | BooleanClauseList

Return sqla expressions.

Builds sqlalchemy expression which can be use in where condition: query(Model).where(build_filter_expressions(…))

fastapi_jsonapi.data_layers.filtering.sqlalchemy.build_terminal_node_filter_expressions(filter_item: Dict, target_schema: Type[TypeSchema], target_model: Type[TypeModel], relationships_info: Dict[str, RelationshipFilteringInfo])
fastapi_jsonapi.data_layers.filtering.sqlalchemy.cast_iterable_with_pydantic(types: List[Type], values: List, schema_field: ModelField) Tuple[List, List[str]]
fastapi_jsonapi.data_layers.filtering.sqlalchemy.cast_value_with_pydantic(types: List[Type], value: Any, schema_field: ModelField) Tuple[Any | None, List[str]]
fastapi_jsonapi.data_layers.filtering.sqlalchemy.cast_value_with_scheme(field_types: List[Type], value: Any) Tuple[Any, List[str]]
fastapi_jsonapi.data_layers.filtering.sqlalchemy.check_can_be_none(fields: list[ModelField]) bool

Return True if None is possible value for target field

fastapi_jsonapi.data_layers.filtering.sqlalchemy.create_filters_and_joins(filter_info: list, model: Type[TypeModel], schema: Type[TypeSchema])
fastapi_jsonapi.data_layers.filtering.sqlalchemy.gather_relationship_paths(filter_item: dict | list) Set[str]

Extracts relationship paths from query filter

fastapi_jsonapi.data_layers.filtering.sqlalchemy.gather_relationships(entrypoint_model: Type[TypeModel], schema: Type[TypeSchema], relationship_paths: Set[str]) dict[str, RelationshipFilteringInfo]
fastapi_jsonapi.data_layers.filtering.sqlalchemy.gather_relationships_info(model: Type[TypeModel], schema: Type[TypeSchema], relationship_path: List[str], collected_info: dict[str, RelationshipFilteringInfo], target_relationship_idx: int = 0, prev_aliased_model: Any | None = None) dict[str, RelationshipFilteringInfo]
fastapi_jsonapi.data_layers.filtering.sqlalchemy.get_custom_filter_expression_callable(schema_field, operator: str) Callable
fastapi_jsonapi.data_layers.filtering.sqlalchemy.get_model_column(model: Type[TypeModel], schema: Type[TypeSchema], field_name: str) InstrumentedAttribute
fastapi_jsonapi.data_layers.filtering.sqlalchemy.get_operator(model_column: InstrumentedAttribute, operator_name: str) str

Get the function operator from his name

Return callable:

a callable to make operation on a column

fastapi_jsonapi.data_layers.filtering.sqlalchemy.is_relationship_filter(name: str) bool
fastapi_jsonapi.data_layers.filtering.sqlalchemy.is_terminal_node(filter_item: dict) bool

If node shape is:

{

“name: …, “op: …, “val: …,

}

fastapi_jsonapi.data_layers.filtering.sqlalchemy.prepare_relationships_info(model: Type[TypeModel], schema: Type[TypeSchema], filter_info: list)
fastapi_jsonapi.data_layers.filtering.sqlalchemy.separate_types(types: List[Type]) Tuple[List[Type], List[Type]]

Separates the types into two kinds.

The first are those for which there are already validators defined by pydantic - str, int, datetime and some other built-in types. The second are all other types for which the arbitrary_types_allowed config is applied when defining the pydantic model

fastapi_jsonapi.data_layers.filtering.sqlalchemy.validator_requires_model_field(validator: Callable) bool

Check if validator accepts the field param

Parameters:

validator

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation module

Previously used: ‘__’

class fastapi_jsonapi.data_layers.filtering.tortoise_operation.ProcessTypeOperationFieldName(*args, **kwargs)

Bases: Protocol

fastapi_jsonapi.data_layers.filtering.tortoise_operation.add_suffix(field_name: str, suffix: str, sep: str = '__') str

joins str

Parameters:
  • field_name

  • suffix

  • sep

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.prepare_field_name_for_filtering(field_name: str, type_op: str) str

Prepare fields for use in ORM.

Parameters:
  • field_name – name of the field by which the filtering will be performed.

  • type_op – operation type.

Returns:

prepared name field.

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_any(field_name: str, type_op: str) str

used to filter on to many relationships

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_between(field_name: str, type_op: str) str

used to filter a field between two values

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_contains(field_name: str, type_op: str) str

field contains specified substring

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_endswith(field_name: str, type_op: str) str

check if field ends with a string

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_eq(field_name: str, type_op: str) str

check if field is equal to something

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_ge(field_name: str, type_op: str) str

check if field is greater than or equal to something

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_gt(field_name: str, type_op: str) str

check if field is greater than to something

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_has(field_name: str, type_op: str) str

used to filter on to one relationship

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_icontains(field_name: str, type_op: str) str

case insensitive contains

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_iendswith(field_name: str, type_op: str) str

check if field ends with a string

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_iequals(field_name: str, type_op: str) str

case insensitive equals

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_ilike(field_name: str, type_op: str) str

case insensitive contains

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_in_(field_name: str, type_op: str) str

check if field is in a list of values

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_is_(field_name: str, type_op: str) str

check if field is null. wtf

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_isnot(field_name: str, type_op: str) str

check if field is not null. wtf

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_istartswith(field_name: str, type_op: str) str

check if field starts with a string (case insensitive)

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_le(field_name: str, type_op: str) str

check if field is less than or equal to something

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_like(field_name: str, type_op: str) str

field contains specified substring

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_lt(field_name: str, type_op: str) str

check if field is less than to something

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_match(field_name: str, type_op: str) str

check if field match against a string or pattern

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_ne(field_name: str, type_op: str) str

check if field is not equal to something

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_notilike(field_name: str, type_op: str) str

check if field does not contains a string (case insensitive)

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_notin_(field_name: str, type_op: str) str

check if field is not in a list of values

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_notlike(field_name: str, type_op: str) str

check if field does not contains a string

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_operation.type_op_startswith(field_name: str, type_op: str) str

check if field starts with value

Parameters:
  • field_name

  • type_op

Returns:

fastapi_jsonapi.data_layers.filtering.tortoise_orm module

Tortoise filters creator.

class fastapi_jsonapi.data_layers.filtering.tortoise_orm.FilterTortoiseORM(model: TypeModel)

Bases: object

create_query(filter_q: tuple | Q) Q

Tortoise filter creation.

filter_converter(schema: Type[BaseModel], filters: List[Dict[str, str | int | float | dict | list | None]]) List

Make a list with filters, which can be used in the tortoise filter.

Parameters:
  • schema – schemas schema of object.

  • filters – list of JSON API filters.

Returns:

list of filters, prepared for use in tortoise model.

Raises:

InvalidFilters – if the filter was created with an error.

async json_api_filter(query, schema: Type[BaseModel], query_params: QueryStringManager) QuerySet

Make queries with filtering from request.

orm_and_or(op: DBORMOperandType, filters: list) None | QuerySet | Dict[str, QuerySet | List[QuerySet]]

Filter for query to ORM.

val_to_query(val: Any) Any

Value to query.

validate(filter_q: None | Q | Dict[str, Q | List[Q]]) Q | None

Tortoise filter validation.

Parameters:

filter_q – dict with filter body.

Returns:

validated filter.

Raises:

QueryError – if the field in the filter does not match the field in tortoise.

fastapi_jsonapi.data_layers.filtering.tortoise_orm.prepare_filter_pair(field: Type[ModelField], field_name: str, type_op: str, value: Any) Tuple

Prepare filter.

fastapi_jsonapi.data_layers.sorting.sqlalchemy module

Helper to create sqlalchemy sortings according to filter querystring parameter

class fastapi_jsonapi.data_layers.sorting.sqlalchemy.Node(model: Type[TypeModel], sort_: dict, schema: Type[TypeSchema])

Bases: object

Helper to recursively create sorts with sqlalchemy according to sort querystring parameter

property column: InstrumentedAttribute

Get the column object.

Returns:

the column to filter on

classmethod create_sort(schema_field: ModelField, model_column, order: str)

Create sqlalchemy sort.

Params schema_field:

Params model_column:

column sqlalchemy

Params order:

desc | asc (or custom)

Returns:

property name: str

Return the name of the node or raise a BadRequest exception

Return str:

the name of the sort to sort on

property related_model: DeclarativeMeta

Get the related model of a relationship field.

Returns:

the related model.

property related_schema: Type[TypeSchema]

Get the related schema of a relationship field.

Returns:

the related schema

resolve() Tuple[BinaryExpression, List[List[Any]]]

Create sort for a particular node of the sort tree.

fastapi_jsonapi.data_layers.sorting.sqlalchemy.create_sorts(model: Type[TypeModel], filter_info: list | dict, schema: Type[TypeSchema])

Apply filters from filters information to base query.

Params model:

the model of the node.

Params filter_info:

current node filter information.

Params schema:

the resource.

fastapi_jsonapi.data_layers.sorting.tortoise_orm module

class fastapi_jsonapi.data_layers.sorting.tortoise_orm.SortTortoiseORM

Bases: object

classmethod sort(query: QuerySet, query_params_sorting: List[Dict[str, str]], default_sort: str = '') QuerySet

Реализация динамической сортировки для query.

Parameters:
  • query – запрос

  • query_params_sorting – параметры от клиента

  • default_sort – дефолтная сортировка, например “-id” или sort=-id,created_at

fastapi_jsonapi.data_layers.base module

The base class of a data layer.

If you want to create your own data layer you must inherit from this base class

class fastapi_jsonapi.data_layers.base.BaseDataLayer(request: Request, schema: Type[TypeSchema], model: Type[TypeModel], url_id_field: str, id_name_field: str | None = None, disable_collection_count: bool = False, default_collection_count: int = -1, type_: str = '', **kwargs)

Bases: object

Base class of a data layer

async after_create_object(obj, data, view_kwargs)

Provide additional data after object creation

Parameters:
  • obj – an object from data layer

  • data – the data validated by schemas

  • view_kwargs – kwargs from the resource view

async after_create_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs)

Make work after to create a relationship

Parameters:
  • obj – an object from data layer

  • updated (bool) – True if object was updated else False

  • json_data – the request params

  • relationship_field (str) – the model attribute used for relationship

  • related_id_field (str) – the identifier field of the related model

  • view_kwargs – kwargs from the resource view

Return boolean:

True if relationship have changed else False

async after_delete_object(obj: TypeModel, view_kwargs)

Make work after delete object

Parameters:
  • obj – an object from data layer

  • view_kwargs – kwargs from the resource view

async after_delete_objects(objects: List[TypeModel], view_kwargs: dict)

Any action after deleting objects.

Parameters:
  • objects – an object from data layer.

  • view_kwargs – kwargs from the resource view.

async after_delete_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs)

Make work after to delete a relationship

Parameters:
  • obj – an object from data layer

  • updated (bool) – True if object was updated else False

  • json_data – the request params

  • relationship_field (str) – the model attribute used for relationship

  • related_id_field (str) – the identifier field of the related model

  • view_kwargs – kwargs from the resource view

async after_get_collection(collection, qs, view_kwargs)

Make work after to retrieve a collection of objects

Parameters:
  • collection (iterable) – the collection of objects

  • qs – a querystring manager to retrieve information from url

  • view_kwargs – kwargs from the resource view

async after_get_object(obj, view_kwargs)

Make work after to retrieve an object

Parameters:
  • obj – an object from data layer

  • view_kwargs – kwargs from the resource view

async after_get_relationship(obj, related_objects, relationship_field, related_type_, related_id_field, view_kwargs)

Make work after to get information about a relationship

Parameters:
  • obj – an object from data layer

  • related_objects (iterable) – related objects of the object

  • relationship_field (str) – the model attribute used for relationship

  • related_type (str) – the related resource type

  • related_id_field (str) – the identifier field of the related model

  • view_kwargs – kwargs from the resource view

Return tuple:

the object and related object(s)

async after_update_object(obj: TypeModel, data, view_kwargs)

Make work after update object

Parameters:
  • obj – an object from data layer

  • data – the data validated by schemas

  • view_kwargs – kwargs from the resource view

async after_update_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs)

Make work after to update a relationship

Parameters:
  • obj – an object from data layer

  • updated (bool) – True if object was updated else False

  • json_data – the request params

  • relationship_field (str) – the model attribute used for relationship

  • related_id_field (str) – the identifier field of the related model

  • view_kwargs – kwargs from the resource view

Return boolean:

True if relationship have changed else False

async atomic_end(success: bool = True)
async atomic_start(previous_dl: BaseDataLayer | None = None)
async before_create_object(data, view_kwargs)

Provide additional data before object creation

Parameters:
  • data – the data validated by schemas

  • view_kwargs – kwargs from the resource view

async before_create_relationship(json_data, relationship_field, related_id_field, view_kwargs)

Make work before to create a relationship

Parameters:
  • json_data – the request params

  • relationship_field (str) – the model attribute used for relationship

  • related_id_field (str) – the identifier field of the related model

  • view_kwargs – kwargs from the resource view

Return boolean:

True if relationship have changed else False

async before_delete_object(obj: TypeModel, view_kwargs)

Make checks before delete object

Parameters:
  • obj – an object from data layer

  • view_kwargs – kwargs from the resource view

async before_delete_objects(objects: List[TypeModel], view_kwargs: dict)

Make checks before deleting objects.

Parameters:
  • objects – an object from data layer.

  • view_kwargs – kwargs from the resource view.

async before_delete_relationship(json_data, relationship_field, related_id_field, view_kwargs)

Make work before to delete a relationship

Parameters:
  • json_data – the request params

  • relationship_field (str) – the model attribute used for relationship

  • related_id_field (str) – the identifier field of the related model

  • view_kwargs – kwargs from the resource view

async before_get_collection(qs, view_kwargs)

Make work before to retrieve a collection of objects

Parameters:
  • qs – a querystring manager to retrieve information from url

  • view_kwargs – kwargs from the resource view

async before_get_object(view_kwargs)

Make work before to retrieve an object

Parameters:

view_kwargs – kwargs from the resource view

async before_get_relationship(relationship_field, related_type_, related_id_field, view_kwargs)

Make work before to get information about a relationship

Parameters:
  • relationship_field (str) – the model attribute used for relationship

  • related_type (str) – the related resource type

  • related_id_field (str) – the identifier field of the related model

  • view_kwargs – kwargs from the resource view

Return tuple:

the object and related object(s)

async before_update_object(obj, data, view_kwargs)

Make checks or provide additional data before update object

Parameters:
  • obj – an object from data layer

  • data – the data validated by schemas

  • view_kwargs – kwargs from the resource view

async before_update_relationship(json_data, relationship_field, related_id_field, view_kwargs)

Make work before to update a relationship

Parameters:
  • json_data – the request params

  • relationship_field (str) – the model attribute used for relationship

  • related_id_field (str) – the identifier field of the related model

  • view_kwargs – kwargs from the resource view

Return boolean:

True if relationship have changed else False

async create_object(data_create: BaseJSONAPIItemInSchema, view_kwargs: dict) TypeModel

Create an object

Parameters:
  • data_create – validated data

  • view_kwargs – kwargs from the resource view

Return DeclarativeMeta:

an object

async create_relationship(json_data, relationship_field, related_id_field, view_kwargs)

Create a relationship

Parameters:
  • json_data – the request params

  • relationship_field (str) – the model attribute used for relationship

  • related_id_field (str) – the identifier field of the related model

  • view_kwargs – kwargs from the resource view

Return boolean:

True if relationship have changed else False

async delete_object(obj, view_kwargs)

Delete an item through the data layer

Parameters:
  • obj (DeclarativeMeta) – an object

  • view_kwargs – kwargs from the resource view

async delete_objects(objects: List[TypeModel], view_kwargs)
async delete_relationship(json_data, relationship_field, related_id_field, view_kwargs)

Delete a relationship

Parameters:
  • json_data – the request params

  • relationship_field (str) – the model attribute used for relationship

  • related_id_field (str) – the identifier field of the related model

  • view_kwargs – kwargs from the resource view

async get_collection(qs: QueryStringManager, view_kwargs: dict | None = None) Tuple[int, list]

Retrieve a collection of objects

Parameters:
  • qs – a querystring manager to retrieve information from url

  • view_kwargs – kwargs from the resource view

Return tuple:

the number of object and the list of objects

async get_object(view_kwargs: dict, qs: QueryStringManager | None = None) TypeModel

Retrieve an object

Parameters:
  • view_kwargs – kwargs from the resource view

  • qs

Return DeclarativeMeta:

an object

get_object_id(obj: TypeModel)
get_object_id_field()
get_object_id_field_name()

compound key may cause errors

Returns:

Prepare query for the related model

Parameters:

related_model – Related ORM model class (not instance)

Returns:

Get related object.

Parameters:
  • related_model – Related ORM model class (not instance)

  • related_id_field – id field of the related model (usually it’s id)

  • id_value – related object id value

Returns:

an ORM object

Prepare query to get related object

Parameters:
  • related_model

  • related_id_field

  • id_value

Returns:

Get related objects list.

Parameters:
  • related_model – Related ORM model class (not instance)

  • related_id_field – id field of the related model (usually it’s id)

  • ids – related object id values list

Returns:

a list of ORM objects

Prepare query to get related objects list

Parameters:
  • related_model

  • related_id_field

  • ids

Returns:

async get_relationship(relationship_field, related_type_, related_id_field, view_kwargs)

Get information about a relationship

Parameters:
  • relationship_field (str) – the model attribute used for relationship

  • related_type (str) – the related resource type

  • related_id_field (str) – the identifier field of the related model

  • view_kwargs – kwargs from the resource view

Return tuple:

the object and related object(s)

query(view_kwargs)

Construct the base query to retrieve wanted data

Parameters:

view_kwargs – kwargs from the resource view

async update_object(obj, data_update: BaseJSONAPIItemInSchema, view_kwargs: dict)

Update an object

Parameters:
  • obj – an object

  • data_update – the data validated by schemas

  • view_kwargs – kwargs from the resource view

Return boolean:

True if object have changed else False

async update_relationship(json_data, relationship_field, related_id_field, view_kwargs)

Update a relationship

Parameters:
  • json_data – the request params

  • relationship_field (str) – the model attribute used for relationship

  • related_id_field (str) – the identifier field of the related model

  • view_kwargs – kwargs from the resource view

Return boolean:

True if relationship have changed else False

fastapi_jsonapi.data_typing module

fastapi_jsonapi.data_layers.orm module

ORM types enums.

class fastapi_jsonapi.data_layers.orm.DBORMOperandType(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

Bases: str, Enum

and_ = 'and'
not_ = 'not'
or_ = 'or'

fastapi_jsonapi.data_layers.shared module

fastapi_jsonapi.data_layers.shared.create_filters_or_sorts(model: Type[TypeModel], filter_or_sort_info: list | dict, class_node: Type[NodeSQLAlchemy], schema: Type[TypeSchema]) Tuple

Apply filters / sorts from filters / sorts information to base query

Parameters:
  • model – the model of the node

  • filter_or_sort_info – current node filter_or_sort information

  • class_node

  • schema – the resource

fastapi_jsonapi.data_layers.sqla_orm module

This module is a CRUD interface between resource managers and the sqlalchemy ORM

class fastapi_jsonapi.data_layers.sqla_orm.SqlalchemyDataLayer(schema: Type[TypeSchema], model: Type[TypeModel], session: AsyncSession, disable_collection_count: bool = False, default_collection_count: int = -1, id_name_field: str | None = None, url_id_field: str = 'id', eagerload_includes: bool = True, query: Select | None = None, auto_convert_id_to_column_type: bool = True, **kwargs: Any)

Bases: BaseDataLayer

Sqlalchemy data layer

async after_create_object(obj: TypeModel, model_kwargs: dict, view_kwargs: dict)

Provide additional data after object creation.

Parameters:
  • obj – an object from data layer.

  • model_kwargs – the data validated by pydantic.

  • view_kwargs – kwargs from the resource view.

async after_create_relationship(obj: Any, updated: bool, json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Make work after to create a relationship.

Parameters:
  • obj – an object from data layer.

  • updated – True if object was updated else False.

  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Return boolean:

True if relationship have changed else False.

async after_delete_object(obj: TypeModel, view_kwargs: dict)

Make work after delete object.

Parameters:
  • obj – an object from data layer.

  • view_kwargs – kwargs from the resource view.

async after_delete_objects(objects: List[TypeModel], view_kwargs: dict)

Any actions after deleting objects.

Parameters:
  • objects – an object from data layer.

  • view_kwargs – kwargs from the resource view.

async after_delete_relationship(obj: Any, updated: bool, json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Make work after to delete a relationship.

Parameters:
  • obj – an object from data layer.

  • updated – True if object was updated else False.

  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

async after_get_collection(collection: Iterable, qs: QueryStringManager, view_kwargs: dict)

Make work after to retrieve a collection of objects.

Parameters:
  • collection – the collection of objects.

  • qs – a querystring manager to retrieve information from url.

  • view_kwargs – kwargs from the resource view.

async after_get_object(obj: Any, view_kwargs: dict)

Make work after to retrieve an object.

Parameters:
  • obj – an object from data layer.

  • view_kwargs – kwargs from the resource view.

async after_get_relationship(obj: Any, related_objects: Iterable, relationship_field: str, related_type_: str, related_id_field: str, view_kwargs: dict)

Make work after to get information about a relationship.

Parameters:
  • obj – an object from data layer.

  • related_objects – related objects of the object.

  • relationship_field – the model attribute used for relationship.

  • related_type – the related resource type.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Return tuple:

the object and related object(s).

async after_update_object(obj: Any, model_kwargs: dict, view_kwargs: dict)

Make work after update object.

Parameters:
  • obj – an object from data layer.

  • model_kwargs – the data validated by schemas.

  • view_kwargs – kwargs from the resource view.

async after_update_relationship(obj: Any, updated: bool, json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Make work after to update a relationship.

Parameters:
  • obj – an object from data layer.

  • updated – True if object was updated else False.

  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Return boolean:

True if relationship have changed else False.

async apply_relationships(obj: TypeModel, data_create: BaseJSONAPIItemInSchema, action_trigger: Literal['create', 'update']) None

Handles relationships passed in request

Parameters:
  • obj

  • data_create

  • action_trigger – indicates which one operation triggered relationships applying

Returns:

async atomic_end(success: bool = True)
async atomic_start(previous_dl: SqlalchemyDataLayer | None = None)
async before_create_object(model_kwargs: dict, view_kwargs: dict)

Provide additional data before object creation.

Parameters:
  • model_kwargs – the data validated by pydantic.

  • view_kwargs – kwargs from the resource view.

async before_create_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Make work before to create a relationship.

Parameters:
  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Return boolean:

True if relationship have changed else False.

async before_delete_object(obj: TypeModel, view_kwargs: dict)

Make checks before delete object.

Parameters:
  • obj – an object from data layer.

  • view_kwargs – kwargs from the resource view.

async before_delete_objects(objects: List[TypeModel], view_kwargs: dict)

Make checks before deleting objects.

Parameters:
  • objects – an object from data layer.

  • view_kwargs – kwargs from the resource view.

async before_delete_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Make work before to delete a relationship.

Parameters:
  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

async before_get_collection(qs: QueryStringManager, view_kwargs: dict)

Make work before to retrieve a collection of objects.

Parameters:
  • qs – a querystring manager to retrieve information from url.

  • view_kwargs – kwargs from the resource view.

async before_get_object(view_kwargs: dict)

Make work before to retrieve an object.

Parameters:

view_kwargs – kwargs from the resource view.

async before_get_relationship(relationship_field: str, related_type_: str, related_id_field: str, view_kwargs: dict)

Make work before to get information about a relationship.

Parameters:
  • relationship_field (str) – the model attribute used for relationship.

  • related_type (str) – the related resource type.

  • related_id_field (str) – the identifier field of the related model.

  • view_kwargs (dict) – kwargs from the resource view.

Return tuple:

the object and related object(s).

async before_update_object(obj: Any, model_kwargs: dict, view_kwargs: dict)

Make checks or provide additional data before update object.

Parameters:
  • obj – an object from data layer.

  • model_kwargs – the data validated by schemas.

  • view_kwargs – kwargs from the resource view.

async before_update_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Make work before to update a relationship.

Parameters:
  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Return boolean:

True if relationship have changed else False.

async check_object_has_relationship_or_raise(obj: TypeModel, relation_name: str)

Checks that there is relationship with relation_name in obj

Parameters:
  • obj

  • relation_name

async create_object(data_create: BaseJSONAPIItemInSchema, view_kwargs: dict) TypeModel

Create an object through sqlalchemy.

Parameters:
  • data_create – the data validated by pydantic.

  • view_kwargs – kwargs from the resource view.

Returns:

async create_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict) bool

Create a relationship.

Parameters:
  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Returns:

True if relationship have changed else False.

async delete_object(obj: TypeModel, view_kwargs: dict)

Delete an object through sqlalchemy.

Parameters:
  • obj – an item from sqlalchemy.

  • view_kwargs – kwargs from the resource view.

async delete_objects(objects: List[TypeModel], view_kwargs: dict)
async delete_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Delete a relationship.

Parameters:
  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

eagerload_includes(query: Select, qs: QueryStringManager) Select

Use eagerload feature of sqlalchemy to optimize data retrieval for include querystring parameter.

Parameters:
  • query – sqlalchemy queryset.

  • qs – a querystring manager to retrieve information from url.

Returns:

the query with includes eagerloaded.

filter_query(query: Select, filter_info: list | None) Select

Filter query according to jsonapi 1.0.

Parameters:
  • query – sqlalchemy query to sort.

  • filter_info – filter information.

Returns:

the sorted query.

async get_collection(qs: QueryStringManager, view_kwargs: dict | None = None) Tuple[int, list]

Retrieve a collection of objects through sqlalchemy.

Parameters:
  • qs – a querystring manager to retrieve information from url.

  • view_kwargs – kwargs from the resource view.

Returns:

the number of object and the list of objects.

async get_collection_count(query: Select, qs: QueryStringManager, view_kwargs: dict) int

Returns number of elements for this collection

Parameters:
  • query – SQLAlchemy query

  • qs – QueryString

  • view_kwargs – view kwargs

Returns:

async get_object(view_kwargs: dict, qs: QueryStringManager | None = None) TypeModel

Retrieve an object through sqlalchemy.

Parameters:
  • view_kwargs – kwargs from the resource view

  • qs

Return DeclarativeMeta:

an object from sqlalchemy

get_object_id_field_name()

compound key may cause errors

Returns:

Retrieves object or objects to link from database

Parameters:
  • related_model

  • relationship_info

  • relationship_in

Prepare sql query (statement) to fetch related model

Parameters:

related_model

Returns:

Get related object.

Parameters:
  • related_model – SQLA ORM model class

  • related_id_field – id field of the related model (usually it’s id)

  • id_value – related object id value

Returns:

a related SQLA ORM object

Prepare query to get related object

Parameters:
  • related_model

  • related_id_field

  • id_value

Returns:

Fetch related objects (many)

Parameters:
  • related_model

  • related_id_field

  • ids

Returns:

Prepare query to get related objects list

Parameters:
  • related_model

  • related_id_field

  • ids

Returns:

async get_relationship(relationship_field: str, related_type_: str, related_id_field: str, view_kwargs: dict) Tuple[Any, Any]

Get a relationship.

Parameters:
  • relationship_field – the model attribute used for relationship.

  • related_type – the related resource type.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Returns:

the object and related object(s).

Links target object with relationship object or objects

Parameters:
  • obj

  • relation_name

  • related_data

  • action_trigger – indicates which one operation triggered relationships applying

paginate_query(query: Select, paginate_info: PaginationQueryStringManager) Select

Paginate query according to jsonapi 1.0.

Parameters:
  • query – sqlalchemy queryset.

  • paginate_info – pagination information.

Returns:

the paginated query

prepare_id_value(col: InstrumentedAttribute, value: Any) Any

Convert value to the required python type.

Type is declared on the SQLA column.

Parameters:
  • col

  • value

Returns:

query(view_kwargs: dict) Select

Construct the base query to retrieve wanted data.

Parameters:

view_kwargs – kwargs from the resource view

retrieve_object_query(view_kwargs: dict, filter_field: InstrumentedAttribute, filter_value: Any) Select

Build query to retrieve object.

Parameters:
  • view_kwargs – kwargs from the resource view

  • filter_field – the field to filter on

  • filter_value – the value to filter with

Return sqlalchemy query:

a query from sqlalchemy

async save()
sort_query(query: Select, sort_info: list) Select

Sort query according to jsonapi 1.0.

Parameters:
  • query – sqlalchemy query to sort.

  • sort_info – sort information.

Returns:

the sorted query.

async update_object(obj: TypeModel, data_update: BaseJSONAPIItemInSchema, view_kwargs: dict) bool

Update an object through sqlalchemy.

Parameters:
  • obj – an object from sqlalchemy.

  • data_update – the data validated by pydantic.

  • view_kwargs – kwargs from the resource view.

Returns:

True if object have changed else False.

async update_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict) bool

Update a relationship

Parameters:
  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Returns:

True if relationship have changed else False.

fastapi_jsonapi.data_layers.tortoise_orm module

This module is a CRUD interface between resource managers and the Tortoise ORM

class fastapi_jsonapi.data_layers.tortoise_orm.TortoiseDataLayer(schema: Type[TypeSchema], model: Type[TypeModel], disable_collection_count: bool = False, default_collection_count: int = -1, id_name_field: str | None = None, url_id_field: str = 'id', query: QuerySet | None = None, **kwargs: Any)

Bases: BaseDataLayer

Tortoise data layer

async after_create_object(obj: Any, data: dict, view_kwargs: dict)

Provide additional data after object creation.

Parameters:
  • obj – an object from data layer.

  • data – the data validated by pydantic.

  • view_kwargs – kwargs from the resource view.

async after_create_relationship(obj: Any, updated: bool, json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Make work after to create a relationship.

Parameters:
  • obj – an object from data layer.

  • updated – True if object was updated else False.

  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Return boolean:

True if relationship have changed else False.

async after_delete_object(obj: Any, view_kwargs: dict)

Make work after delete object.

Parameters:
  • obj – an object from data layer.

  • view_kwargs – kwargs from the resource view.

async after_delete_relationship(obj: Any, updated: bool, json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Make work after to delete a relationship.

Parameters:
  • obj – an object from data layer.

  • updated – True if object was updated else False.

  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

async after_get_collection(collection: Iterable, qs: QueryStringManager, view_kwargs: dict) Iterable

Make work after to retrieve a collection of objects.

Parameters:
  • collection – the collection of objects.

  • qs – a querystring manager to retrieve information from url.

  • view_kwargs – kwargs from the resource view.

async after_get_object(obj: Any, view_kwargs: dict)

Make work after to retrieve an object.

Parameters:
  • obj – an object from data layer.

  • view_kwargs – kwargs from the resource view.

async after_get_relationship(obj: Any, related_objects: Iterable, relationship_field: str, related_type_: str, related_id_field: str, view_kwargs: dict)

Make work after to get information about a relationship.

Parameters:
  • obj – an object from data layer.

  • related_objects – related objects of the object.

  • relationship_field – the model attribute used for relationship.

  • related_type – the related resource type.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Return tuple:

the object and related object(s).

async after_update_object(obj: Any, data: dict, view_kwargs: dict)

Make work after update object.

Parameters:
  • obj – an object from data layer.

  • data – the data validated by schemas.

  • view_kwargs – kwargs from the resource view.

async after_update_relationship(obj: Any, updated: bool, json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Make work after to update a relationship.

Parameters:
  • obj – an object from data layer.

  • updated – True if object was updated else False.

  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Return boolean:

True if relationship have changed else False.

async before_create_object(data: dict, view_kwargs: dict)

Provide additional data before object creation.

Parameters:
  • data – the data validated by pydantic.

  • view_kwargs – kwargs from the resource view.

async before_create_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Make work before to create a relationship.

Parameters:
  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Return boolean:

True if relationship have changed else False.

async before_delete_object(obj: Any, view_kwargs: dict)

Make checks before delete object.

Parameters:
  • obj – an object from data layer.

  • view_kwargs – kwargs from the resource view.

async before_delete_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Make work before to delete a relationship.

Parameters:
  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

async before_get_collection(qs: QueryStringManager, view_kwargs: dict)

Make work before to retrieve a collection of objects.

Parameters:
  • qs – a querystring manager to retrieve information from url.

  • view_kwargs – kwargs from the resource view.

async before_get_object(view_kwargs: dict)

Make work before to retrieve an object.

Parameters:

view_kwargs – kwargs from the resource view.

async before_get_relationship(relationship_field: str, related_type_: str, related_id_field: str, view_kwargs: dict)

Make work before to get information about a relationship.

Parameters:
  • relationship_field (str) – the model attribute used for relationship.

  • related_type (str) – the related resource type.

  • related_id_field (str) – the identifier field of the related model.

  • view_kwargs (dict) – kwargs from the resource view.

Return tuple:

the object and related object(s).

async before_update_object(obj: Any, data: dict, view_kwargs: dict)

Make checks or provide additional data before update object.

Parameters:
  • obj – an object from data layer.

  • data – the data validated by schemas.

  • view_kwargs – kwargs from the resource view.

async before_update_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Make work before to update a relationship.

Parameters:
  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Return boolean:

True if relationship have changed else False.

async create_object(data_create: BaseJSONAPIItemInSchema, view_kwargs: dict) TypeModel

Create an object

Parameters:
  • data_create – validated data

  • view_kwargs – kwargs from the resource view

Return DeclarativeMeta:

an object

async create_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict) bool

Create a relationship.

Parameters:
  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Returns:

True if relationship have changed else False.

async delete_object(obj: TypeModel, view_kwargs: dict)

Delete an object through Tortoise.

Parameters:
  • obj – an item from Tortoise.

  • view_kwargs – kwargs from the resource view.

async delete_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict)

Delete a relationship.

Parameters:
  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

eagerload_includes(query: QuerySet, qs: QueryStringManager) QuerySet

Use eagerload feature of Tortoise to optimize data retrieval for include querystring parameter.

Parameters:
  • query – Tortoise queryset.

  • qs – a querystring manager to retrieve information from url.

Returns:

the query with includes eagerloaded.

async get_collection(qs: QueryStringManager, view_kwargs: dict | None = None) Tuple[int, list]

Retrieve a collection of objects through Tortoise.

Parameters:
  • qs – a querystring manager to retrieve information from url.

  • view_kwargs – kwargs from the resource view.

Returns:

the number of object and the list of objects.

async get_collection_count(query: QuerySet) int

Prepare query to fetch collection

Parameters:
  • query – Tortoise query

  • qs – QueryString

  • view_kwargs – view kwargs

Returns:

async get_object(view_kwargs: dict, qs: QueryStringManager | None = None) TypeModel

Retrieve an object

Parameters:
  • view_kwargs – kwargs from the resource view

  • qs

Return DeclarativeMeta:

an object

Get related object.

Parameters:
  • related_model – Tortoise model

  • related_id_field – the identifier field of the related model

  • id_value – related object id value

Returns:

a related object

async get_relationship(relationship_field: str, related_type_: str, related_id_field: str, view_kwargs: dict) Tuple[Any, Any]

Get a relationship.

Parameters:
  • relationship_field – the model attribute used for relationship.

  • related_type – the related resource type.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Returns:

the object and related object(s).

paginate_query(query: QuerySet, paginate_info: PaginationQueryStringManager) QuerySet

Paginate query according to jsonapi 1.0.

Parameters:
  • query – Tortoise queryset.

  • paginate_info – pagination information.

Returns:

the paginated query

query(view_kwargs: dict) QuerySet

Construct the base query to retrieve wanted data.

Parameters:

view_kwargs – kwargs from the resource view

retrieve_object_query(view_kwargs: dict, filter_field: Any, filter_value: Any) QuerySet

Build query to retrieve object.

Parameters:
  • view_kwargs – kwargs from the resource view

  • filter_field – the field to filter on

  • filter_value – the value to filter with

Return Tortoise query:

a query from Tortoise

async update_object(obj: TypeModel, data_update: BaseJSONAPIItemInSchema, view_kwargs: dict) bool

Update an object through Tortoise.

Parameters:
  • obj – an object from Tortoise.

  • data – the data validated by schemas.

  • view_kwargs – kwargs from the resource view.

Returns:

True if object have changed else False.

async update_relationship(json_data: dict, relationship_field: str, related_id_field: str, view_kwargs: dict) bool

Update a relationship

Parameters:
  • json_data – the request params.

  • relationship_field – the model attribute used for relationship.

  • related_id_field – the identifier field of the related model.

  • view_kwargs – kwargs from the resource view.

Returns:

True if relationship have changed else False.

fastapi_jsonapi.api module

JSON API router class.

class fastapi_jsonapi.api.RoutersJSONAPI(router: APIRouter, path: str | List[str], tags: List[str], class_list: Type[ListViewBase], class_detail: Type[DetailViewBase], model: Type[TypeModel], schema: Type[BaseModel], resource_type: str, schema_in_post: Type[BaseModel] | None = None, schema_in_patch: Type[BaseModel] | None = None, pagination_default_size: int | None = 25, pagination_default_number: int | None = 1, pagination_default_offset: int | None = None, pagination_default_limit: int | None = None, methods: Iterable[str] = (), max_cache_size: int = 0)

Bases: object

API Router interface for JSON API endpoints in web-services.

DEFAULT_METHODS = ('ViewMethods.GET_LIST', 'ViewMethods.POST', 'ViewMethods.DELETE_LIST', 'ViewMethods.GET', 'ViewMethods.DELETE', 'ViewMethods.PATCH')
Methods

alias of ViewMethods

all_jsonapi_routers: ClassVar[Dict[str, RoutersJSONAPI]] = {}
get_endpoint_name(action: Literal['get', 'create', 'update', 'delete'], kind: Literal['list', 'detail'])

Generate view name

:param action :param kind: list / detail :return:

async handle_view_dependencies(request: Request, view_cls: Type[ViewBase], method: HTTPMethod) Dict[str, Any]

Combines all dependencies (prepared) and returns them as list

Consider method config is already prepared for generic views Reuse the same config for atomic operations

Parameters:
  • request

  • view_cls

  • method

Returns:

prepare_dependencies_handler_signature(custom_handler: Callable[[...], Any], method_config: HTTPMethodConfig) Signature
class fastapi_jsonapi.api.ViewMethods(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

Bases: str, Enum

DELETE = '5'
DELETE_LIST = '3'
GET = '4'
GET_LIST = '1'
PATCH = '6'
POST = '2'

fastapi_jsonapi.jsonapi_typing module

JSON API types.

fastapi_jsonapi.querystring module

Helper to deal with querystring parameters according to jsonapi specification.

class fastapi_jsonapi.querystring.HeadersQueryStringManager(*, host: str | None = None, connection: str | None = None, accept: str | None = None, referer: str | None = None, **extra_data: Any)

Bases: BaseModel

Header query string manager.

Contains info about request headers.

accept: str | None
accept_encoding: str | None
accept_language: str | None
connection: str | None
host: str | None
referer: str | None
user_agent: str | None
class fastapi_jsonapi.querystring.PaginationQueryStringManager(*, offset: int | None = None, size: int | None = 25, number: int = 1, limit: int | None = None)

Bases: BaseModel

Pagination query string manager.

Contains info about offsets, sizes, number and limits of query with pagination.

limit: int | None
number: int
offset: int | None
size: int | None
class fastapi_jsonapi.querystring.QueryStringManager(request: Request)

Bases: object

Querystring parser according to jsonapi reference.

property fields: Dict[str, List[str]]

Return fields wanted by client.

Returns:

a dict of sparse fieldsets information

Return value will be a dict containing all fields by resource, for example:

{
    "user": ['name', 'email'],
}
Raises:

InvalidField – if result field not in schema.

property filters: List[dict]

Return filters from query string.

Returns:

filter information

Raises:

InvalidFilters – if filter loading from json has failed.

get_sorts(schema: Type[TypeSchema]) List[Dict[str, str]]

Return fields to sort by including sort name for SQLAlchemy and row sort parameter for other ORMs.

Returns:

a list of sorting information

Example of return value:

[
    {'field': 'created_at', 'order': 'desc'},
]
Raises:

InvalidSort – if sort field wrong.

property include: List[str]

Return fields to include.

Returns:

a list of include information.

Raises:

InvalidInclude – if nesting is more than MAX_INCLUDE_DEPTH.

managed_keys = ('filter', 'page', 'fields', 'sort', 'include', 'q')
property pagination: PaginationQueryStringManager

Return all page parameters as a dict.

Returns:

a dict of pagination information.

To allow multiples strategies, all parameters starting with page will be included. e.g:

{
    "number": '25',
    "size": '150',
}

Example with number strategy:

query_string = {‘page[number]’: ‘25’, ‘page[size]’: ‘10’} parsed_query.pagination {‘number’: ‘25’, ‘size’: ‘10’}

Raises:

BadRequest – if the client is not allowed to disable pagination.

property querystring: Dict[str, str]

Return original querystring but containing only managed keys.

Returns:

dict of managed querystring parameter

fastapi_jsonapi.schema module

Base JSON:API schemas.

Pydantic (for FastAPI).

class fastapi_jsonapi.schema.BaseJSONAPIDataInSchema(*, data: BaseJSONAPIItemInSchema)

Bases: BaseModel

data: BaseJSONAPIItemInSchema
class fastapi_jsonapi.schema.BaseJSONAPIItemInSchema(*, type: str, attributes: TypeSchema, relationships: TypeSchema | None = None, id: str | None = None)

Bases: BaseJSONAPIItemSchema

Schema for post/patch method

TODO POST: optionally accept custom id for object https://jsonapi.org/format/#crud-creating-client-ids TODO PATCH: accept object id (maybe create a new separate schema)

attributes: TypeSchema
id: str | None
relationships: TypeSchema | None
class fastapi_jsonapi.schema.BaseJSONAPIItemSchema(*, type: str, attributes: dict)

Bases: BaseModel

Base JSON:API item schema.

attributes: dict
type: str
class fastapi_jsonapi.schema.BaseJSONAPIObjectSchema(*, type: str, attributes: dict, id: str)

Bases: BaseJSONAPIItemSchema

Base JSON:API object schema.

id: str
class fastapi_jsonapi.schema.BaseJSONAPIRelationshipDataToManySchema(*, data: List[BaseJSONAPIRelationshipSchema])

Bases: BaseModel

data: List[BaseJSONAPIRelationshipSchema]
class fastapi_jsonapi.schema.BaseJSONAPIRelationshipDataToOneSchema(*, data: BaseJSONAPIRelationshipSchema)

Bases: BaseModel

data: BaseJSONAPIRelationshipSchema
class fastapi_jsonapi.schema.BaseJSONAPIRelationshipSchema(*, id: str, type: str)

Bases: BaseModel

class Config

Bases: BaseConfig

extra = 'forbid'
id: str
type: str
class fastapi_jsonapi.schema.BaseJSONAPIResultSchema(*, meta: JSONAPIResultListMetaSchema | None = None, jsonapi: JSONAPIDocumentObjectSchema = JSONAPIDocumentObjectSchema(version='1.0'))

Bases: BaseModel

JSON:API Required fields schema

jsonapi: JSONAPIDocumentObjectSchema
meta: JSONAPIResultListMetaSchema | None
class fastapi_jsonapi.schema.JSONAPIDocumentObjectSchema(*, version: str = '1.0')

Bases: BaseModel

JSON:API Document Object Schema.

https://jsonapi.org/format/#document-jsonapi-object

version: str
class fastapi_jsonapi.schema.JSONAPIObjectSchema(*, type: str, attributes: dict, id: str)

Bases: BaseJSONAPIObjectSchema

JSON:API base object schema.

class fastapi_jsonapi.schema.JSONAPIResultDetailSchema(*, meta: JSONAPIResultListMetaSchema | None = None, jsonapi: JSONAPIDocumentObjectSchema = JSONAPIDocumentObjectSchema(version='1.0'), data: JSONAPIObjectSchema)

Bases: BaseJSONAPIResultSchema

JSON:API base detail schema.

data: JSONAPIObjectSchema
class fastapi_jsonapi.schema.JSONAPIResultListMetaSchema(*, count: int | None = None, totalPages: int | None = None)

Bases: BaseModel

JSON:API list meta schema.

class Config

Bases: object

allow_population_by_field_name = True
count: int | None
total_pages: int | None
class fastapi_jsonapi.schema.JSONAPIResultListSchema(*, meta: JSONAPIResultListMetaSchema | None = None, jsonapi: JSONAPIDocumentObjectSchema = JSONAPIDocumentObjectSchema(version='1.0'), data: Sequence[JSONAPIObjectSchema])

Bases: BaseJSONAPIResultSchema

JSON:API list base result schema.

data: Sequence[JSONAPIObjectSchema]
exception fastapi_jsonapi.schema.JSONAPISchemaIntrospectionError

Bases: Exception

fastapi_jsonapi.schema.get_model_field(schema: Type[TypeSchema], field: str) str

Get the model field of a schema field.

# todo: use alias (custom names)?

For example:

class Computer(sqla_base):

user = relationship(User)

class ComputerSchema(pydantic_base):

owner = Field(alias=”user”, relationship=…)

Parameters:
  • schema – a pydantic schema

  • field – the name of the schema field

Returns:

the name of the field in the model

Raises:

Exception – if the schema from parameter has no attribute for parameter.

Retrieve the related schema of a relationship field.

Params schema:

the schema to retrieve le relationship field from

Params field:

the relationship field

Returns:

the related schema

fastapi_jsonapi.schema.get_relationships(schema: Type[TypeSchema], model_field: bool = False) List[str]

Return relationship fields of a schema.

Parameters:
  • schema – a schemas schema

  • model_field – list of relationship fields of a schema

fastapi_jsonapi.schema.get_schema_from_type(resource_type: str, app: FastAPI) Type[BaseModel]

Retrieve a schema from the registry by his type.

Parameters:
  • resource_type – the type of the resource.

  • app – FastAPI app instance.

Return Schema:

the schema class.

Raises:

Exception – if the schema not found for this resource type.

fastapi_jsonapi.signature module

Functions for extracting and updating signatures.

fastapi_jsonapi.signature.create_additional_query_params(schema: Type[BaseModel] | None) tuple[list[Parameter], list[Parameter]]
fastapi_jsonapi.signature.create_filter_parameter(name: str, field: ModelField) Parameter

fastapi_jsonapi.splitter module

Splitter for filters, sorts and includes.

Changelog

2.8.0

Performance improvements
  • added simple cache to SchemaBuilder by @CosmoV in #87

Authors

2.7.0

Refactoring and relationships update fixes
Authors

2.6.0

Fix JOINS by relationships
Authors

2.5.1

Fix custom sql filtering, bring back backward compatibility
  • Fix custom sql filtering support: bring back backward compatibility by @mahenzon in #74

  • Read version from file by @mahenzon in #74

Authors

2.5.0

Fix relationships filtering, refactor alchemy helpers
Authors

2.4.2

Separate helper methods for relationships query
  • fix run validator: sometimes it requires model field by @mahenzon in #70

Authors

2.4.1

Separate helper methods for relationships query
  • remove resource manager example since no resource manager exists by @mahenzon in #66

  • create separate methods for building query for fetching related objects by @mahenzon in #67

  • update ruff linter by @mahenzon in #69

Authors

2.4.0

Relationship loading, filtering improvements, fixes
Authors

2.3.2

Duplicated entities in response fix
  • fix duplicates in list response #48

Authors

2.3.1

Pydantic validators inheritance fix
  • fix schema validators passthrough #45

  • fix doc build

Authors

2.3.0

Current Atomic Operation context var
  • create context var for current atomic operation #46

  • create example and coverage for universal dependency both for generic views and atomic operations

  • tests refactoring

Authors

2.2.2

Atomic Operation dependency resolution fixes
  • fixed Atomic Operation dependency resolution #43

Authors

2.2.1

OpenAPI generation fixes
  • fixed openapi generation for custom id type #40

Authors

2.2.0

Support for pydantic validators
  • Pydantic validators are applied to generated schemas now

Authors

2.1.0

Atomic Operations
  • Atomic Operations (see example, JSON:API doc)

  • Create view now accepts BaseJSONAPIItemInSchema as update view does

Authors

2.0.0

Generic views, process relationships

Note

Backward-incompatible changes

  • Automatically create all CRUD views based on schemas (see example)

  • Allow to pass Client-Generated IDs (see example, JSON:API doc)

  • Process relationships on create / update (see example, JSON:API doc)

  • Accept pydantic model with any dependencies on it (see example)

  • handle exceptions (return errors, JSON:API doc)

  • refactor data layers

  • tests coverage

Authors

1.1.0

Generic views
  • Create generic view classes #28

@CosmoV

1.0.0

Backward-incompatible changes, improvements, bug fixes
  • Includes (see example with many-to-many) - any level of includes is now supported (tested with 4);

  • View Classes generics (Detail View and List View);

  • View Classes now use instance-level methods (breaking change, previously classmethods were used);

  • Pydantic schemas now have to be inherited from custom BaseModel methods (breaking change, previously all schemas were supported). It uses custom registry class, so we can collect and resolve all schemas. Maybe there’s some workaround to collect all known schemas;

  • Improved interactive docs, request and response examples now have more info, more schemas appear in docs;

  • Reworked schemas resolving and building;

  • Fixed filtering (schemas resolving fix);

  • Create custom sql filters example;

  • Add linters: black, ruff;

  • Add pre-commit;

  • Add autotests with pytest;

  • Add poetry, configure dependencies groups;

  • Add GitHub Action with linting and testing;

  • Upgrade examples;

  • Update docs.

@mahenzon

0.2.1

Enhancements and bug fixes
  • Fix setup.py for docs in PYPI - @znbiz

0.2.0

Enhancements and bug fixes
  • Rename from fastapi_rest_jsonapi import… to from fastapi_jsonapi import … - @znbiz

  • Add documentation - @znbiz

A minimal API

import sys
from pathlib import Path
from typing import Any, ClassVar, Dict

import uvicorn
from fastapi import APIRouter, Depends, FastAPI
from sqlalchemy import Column, Integer, Text
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from fastapi_jsonapi import RoutersJSONAPI, init
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBaseGeneric, ListViewBaseGeneric
from fastapi_jsonapi.schema_base import BaseModel
from fastapi_jsonapi.views.utils import HTTPMethod, HTTPMethodConfig
from fastapi_jsonapi.views.view_base import ViewBase

CURRENT_FILE = Path(__file__).resolve()
CURRENT_DIR = CURRENT_FILE.parent
PROJECT_DIR = CURRENT_DIR.parent.parent
DB_URL = f"sqlite+aiosqlite:///{CURRENT_DIR}/db.sqlite3"
sys.path.append(str(PROJECT_DIR))

Base = declarative_base()


class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(Text, nullable=True)


class UserAttributesBaseSchema(BaseModel):
    name: str

    class Config:
        orm_mode = True


class UserSchema(UserAttributesBaseSchema):
    """User base schema."""


def async_session() -> sessionmaker:
    engine = create_async_engine(url=make_url(DB_URL))
    _async_session = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
    return _async_session


class Connector:
    @classmethod
    async def get_session(cls):
        """
        Get session as dependency

        :return:
        """
        sess = async_session()
        async with sess() as db_session:  # type: AsyncSession
            yield db_session
            await db_session.rollback()


async def sqlalchemy_init() -> None:
    engine = create_async_engine(url=make_url(DB_URL))
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)


class SessionDependency(BaseModel):
    session: AsyncSession = Depends(Connector.get_session)

    class Config:
        arbitrary_types_allowed = True


def session_dependency_handler(view: ViewBase, dto: SessionDependency) -> Dict[str, Any]:
    return {
        "session": dto.session,
    }


class UserDetailView(DetailViewBaseGeneric):
    method_dependencies: ClassVar = {
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=session_dependency_handler,
        ),
    }


class UserListView(ListViewBaseGeneric):
    method_dependencies: ClassVar = {
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=session_dependency_handler,
        ),
    }


def add_routes(app: FastAPI):
    tags = [
        {
            "name": "User",
            "description": "",
        },
    ]

    router: APIRouter = APIRouter()
    RoutersJSONAPI(
        router=router,
        path="/users",
        tags=["User"],
        class_detail=UserDetailView,
        class_list=UserListView,
        schema=UserSchema,
        model=User,
        resource_type="user",
    )

    app.include_router(router, prefix="")
    return tags


def create_app() -> FastAPI:
    """
    Create app factory.

    :return: app
    """
    app = FastAPI(
        title="FastAPI and SQLAlchemy",
        debug=True,
        openapi_url="/openapi.json",
        docs_url="/docs",
    )
    add_routes(app)
    app.on_event("startup")(sqlalchemy_init)
    init(app)
    return app


app = create_app()

if __name__ == "__main__":
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8080,
    )

This example provides the following API structure:

URL

method

endpoint

Usage

/users

GET

user_list

Get a collection of users

/users

POST

user_list

Create a user

/users

DELETE

user_list

Delete users

/users/{user_id}

GET

user_detail

Get user details

/users/{user_id}

PATCH

user_detail

Update a user

/users/{user_id}

DELETE

user_detail

Delete a user

Request:

POST /users HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "user",
    "attributes": {
        "name": "John"
    }
  }
}

Response:

HTTP/1.1 201 Created
Content-Type: application/vnd.api+json

{
  "data": {
    "attributes": {
      "name": "John"
    },
    "id": "1",
    "links": {
      "self": "/users/1"
    },
    "type": "user"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/users/1"
  }
}

Request:

GET /users/1 HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "attributes": {
      "name": "John"
    },
    "id": "1",
    "links": {
      "self": "/users/1"
    },
    "type": "user"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/users/1"
  }
}

Request:

GET /users HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "name": "John"
      },
      "id": "1",
      "links": {
        "self": "/users/1"
      },
      "type": "user"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/users"
  },
  "meta": {
    "count": 1
  }
}

Request:

PATCH /users/1 HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": {
    "id": 1,
    "type": "user",
    "attributes": {
        "name": "Sam"
    }
  }
}

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "attributes": {
      "name": "Sam"
    },
    "id": "1",
    "links": {
      "self": "/users/1"
    },
    "type": "user"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/users/1"
  }
}

Request:

DELETE /users/1 HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "jsonapi": {
    "version": "1.0"
  },
  "meta": {
    "message": "Object successfully deleted"
  }
}

API Reference

If you are looking for information on a specific function, class or method, this part of the documentation is for you.