diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 00000000..78f7b94d
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,7 @@
+coverage:
+  status:
+    patch:
+      default:
+        target: 100%
+    project: false
+comment: false
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..7cb238e2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,15 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+## Description of the Bug Report
+
+## Checklist
+
+- [ ] Certain that this is a bug (if unsure or you have a question use [discussions](https://github.com/django-json-api/django-rest-framework-json-api/discussions) instead)
+- [ ] Code snippet or unit test added to reproduce bug
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..99156b88
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
+---
+name: Feature request
+about: Please only raise a feature request if you've been advised to do so after discussion.
+  Thanks!
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+## Description of feature request
+
+## Checklist
+
+- [ ] Raised initially as discussion #...
+- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be in the form of third party libraries where feasible.)
+- [ ] I have reduced the issue to the simplest possible case.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..c2b66bc4
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,9 @@
+version: 2
+updates:
+  - package-ecosystem: "pip"
+    directory: "/requirements"
+    schedule:
+      interval: "monthly"
+    groups:
+      all-deps:
+        patterns: ["*"]
diff --git a/docs/pull_request_template.md b/.github/pull_request_template.md
similarity index 100%
rename from docs/pull_request_template.md
rename to .github/pull_request_template.md
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 00000000..03a1db78
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,58 @@
+name: Tests
+permissions:
+  contents: read
+on:
+  push:
+    branches: ["main"]
+  pull_request:
+    branches: ["main"]
+  schedule:
+    - cron: '0 4 * * *'
+jobs:
+  test:
+    name: Run test
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
+    env:
+      PYTHON: ${{ matrix.python-version }}
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install tox
+      - name: Run tox targets for ${{ matrix.python-version }}
+        run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d . | cut -f 1 -d '-')
+      - name: Upload coverage report
+        uses: codecov/codecov-action@v4
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+          env_vars: PYTHON
+  check:
+    name: Run check
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        tox-env: ["black", "lint", "docs"]
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python 3.10
+        uses: actions/setup-python@v2
+        with:
+          python-version: "3.10"
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install tox
+      - name: Run lint
+        run: tox
+        env:
+          TOXENV: ${{ matrix.tox-env }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..49f72fe2
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,21 @@
+repos:
+  - repo: local
+    hooks:
+      - id: black
+        stages: [commit]
+        name: black
+        language: system
+        entry: black
+        types: [python]
+      - id: isort
+        stages: [commit]
+        name: isort
+        language: system
+        entry: isort
+        types: [python]
+      - id: flake8
+        stages: [commit]
+        name: flake8
+        language: system
+        entry: flake8
+        types: [python]
diff --git a/.pyup.yml b/.pyup.yml
deleted file mode 100644
index 7ee57975..00000000
--- a/.pyup.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-search: False
-schedule: "every two weeks"
-requirements:
-  - requirements/requirements-codestyle.txt:
-      update: all
-      pin: True
-  - requirements/requirements-documentation.txt:
-      update: all
-      pin: True
-  - requirements/requirements-optionals.txt:
-      update: all
-      pin: True
-  - requirements/requirements-packaging.txt:
-      update: all
-      pin: True
-  - requirements/requirements-testing.txt:
-      update: all
-      pin: True
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 00000000..fa503604
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,16 @@
+version: 2
+
+build:
+  os: "ubuntu-22.04"
+  tools:
+    python: "3.10"
+
+sphinx:
+  configuration: docs/conf.py
+
+python:
+  install:
+    - requirements: requirements/requirements-optionals.txt
+    - requirements: requirements/requirements-documentation.txt
+    - method: pip
+      path: .
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 05feb314..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,86 +0,0 @@
-language: python
-dist: xenial
-sudo: required
-cache: pip
-# Favor explicit over implicit and use an explicit build matrix.
-matrix:
-  allow_failures:
-    - env: TOXENV=py36-django22-drfmaster
-    - env: TOXENV=py37-django22-drfmaster
-    - env: TOXENV=py38-django22-drfmaster
-    - env: TOXENV=py39-django22-drfmaster
-    - env: TOXENV=py36-django30-drfmaster
-    - env: TOXENV=py37-django30-drfmaster
-    - env: TOXENV=py38-django30-drfmaster
-    - env: TOXENV=py39-django30-drfmaster
-    - env: TOXENV=py36-django31-drfmaster
-    - env: TOXENV=py37-django31-drfmaster
-    - env: TOXENV=py38-django31-drfmaster
-    - env: TOXENV=py39-django31-drfmaster
-
-  include:
-    - python: 3.6
-      env: TOXENV=lint
-    - python: 3.6
-      env: TOXENV=docs
-
-    - python: 3.6
-      env: TOXENV=py36-django22-drf312
-    - python: 3.6
-      env: TOXENV=py36-django22-drfmaster
-    - python: 3.6
-      env: TOXENV=py36-django30-drf312
-    - python: 3.6
-      env: TOXENV=py36-django30-drfmaster
-    - python: 3.6
-      env: TOXENV=py36-django31-drf312
-    - python: 3.6
-      env: TOXENV=py36-django31-drfmaster
-
-    - python: 3.7
-      env: TOXENV=py37-django22-drf312
-    - python: 3.7
-      env: TOXENV=py37-django22-drfmaster
-    - python: 3.7
-      env: TOXENV=py37-django30-drf312
-    - python: 3.7
-      env: TOXENV=py37-django30-drfmaster
-    - python: 3.7
-      env: TOXENV=py37-django31-drf312
-    - python: 3.7
-      env: TOXENV=py37-django31-drfmaster
-
-    - python: 3.8
-      env: TOXENV=py38-django22-drf312
-    - python: 3.8
-      env: TOXENV=py38-django22-drfmaster
-    - python: 3.8
-      env: TOXENV=py38-django30-drf312
-    - python: 3.8
-      env: TOXENV=py38-django30-drfmaster
-    - python: 3.8
-      env: TOXENV=py38-django31-drf312
-    - python: 3.8
-      env: TOXENV=py38-django31-drfmaster
-
-    - python: 3.9
-      env: TOXENV=py39-django22-drf312
-    - python: 3.9
-      env: TOXENV=py39-django22-drfmaster
-    - python: 3.9
-      env: TOXENV=py39-django30-drf312
-    - python: 3.9
-      env: TOXENV=py39-django30-drfmaster
-    - python: 3.9
-      env: TOXENV=py39-django31-drf312
-    - python: 3.9
-      env: TOXENV=py39-django31-drfmaster
-
-
-install:
-  - pip install tox
-script:
-  - tox
-after_success:
-  - pip install codecov
-  - codecov -e TOXENV --required
diff --git a/AUTHORS b/AUTHORS
index b876d4ae..d421d5e6 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,35 +1,55 @@
 Adam Wróbel <https://adamwrobel.com>
 Adam Ziolkowski <adam@adsized.com>
 Alan Crosswell <alan@columbia.edu>
+Alex Seidmann <alex@leanpnt.de>
+Antoine Auger <https://antoineauger.fr/en>
 Anton Shutik <shutikanton@gmail.com>
+Arttu Perälä <arttu@perala.me>
+Ashley Loewen <github@ashleycodes.tech>
+Asif Saif Uddin <auvipy@gmail.com>
 Beni Keller <beni@matraxi.ch>
 Boris Pleshakov <koordinator.kun@gmail.com>
 Charlie Allatson <charles.allatson@gmail.com>
 Christian Zosel <https://zosel.ch>
+David Guillot, for Contexte <dguillot@contexte.com>
 David Vogt <david.vogt@adfinis-sygroup.ch>
 Felix Viernickel <felix@gedankenspieler.org>
 Greg Aker <greg@gregaker.net>
+Humayun Ahmad <humayunahbh@gmail.com>
 Jamie Bliss <astronouth7303@gmail.com>
 Jason Housley <housleyjk@gmail.com>
+Jeppe Fihl-Pearson <jeppe@tenzer.dk>
 Jerel Unruh <mail@unruhdesigns.com>
+Jonas Kiefer <https://github.com/jokiefer>
+Jonas Metzener <jonas.metzener@adfinis.com>
+Jonathan Hiles <jonathan@hil.es>
 Jonathan Senecal <contact@jonathansenecal.com>
 Joseba Mendivil <git@jma.email>
+Kal <kal+oss@tedspot.com>
+Kevin Partington <platinumazure@gmail.com>
 Kieran Evans <keyz182@gmail.com>
 Léo S. <leo@naeka.fr>
 Luc Cary <luc.cary@gmail.com>
+Mansi Dhruv <mansi.p.dhruv@gmail.com>
 Matt Layman <https://www.mattlayman.com>
+Mehdy Khoshnoody <mehdy.khoshnoody@gmail.com>
 Michael Haselton <icereval@gmail.com>
 Mohammed Ali Zubair <mazg1493@gmail.com>
 Nathanael Gordon <nathanael.l.gordon@gmail.com>
+Nick Kozhenin <kozhenin.nick@gmail.com>
 Ola Tarkowska <ola@red-aries.com>
 Oliver Sauder <os@esite.ch>
 Raphael Cohen <raphael.cohen.utt@gmail.com>
 René Kälin <rene@matraxi.ch>
 Roberto Barreda <roberto.barreda@gmail.com>
 Rohith PR <praroh2@gmail.com>
+Safa AlFulaij <safa1996alfulaij@gmail.com>
 santiavenda <santiavenda2@gmail.com>
 Sergey Kolomenkin <https://kolomenkin.com>
 Stas S. <stas@nerd.ro>
+Steven A. <sha0160@protonmail.com>
+Swaraj Baral <baralswaraj40@gmail.com>
 Tim Selman <timcbaoth@gmail.com>
 Tom Glowka <glowka.tom@gmail.com>
+Ulrich Schuster <ulrich.schuster@mailworks.org>
 Yaniv Peer <yanivpeer@gmail.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99629181..fe7c9e14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,9 +5,269 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-Note that in line with [Django REST Framework policy](http://www.django-rest-framework.org/topics/release-notes/),
+Note that in line with [Django REST framework policy](https://www.django-rest-framework.org/topics/release-notes/),
 any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change.
 
+## [8.0.0] - 2025-07-24
+
+### Added
+
+* Added support for Django REST framework 3.16.
+* Added support for Django 5.2.
+
+### Fixed
+
+* Ensured that compound documents' `include` query parameter fully support `JSON_API_FORMAT_FIELD_NAMES`.
+* Ensured that sparse fieldset's `fields` query parameter fully supports `JSON_API_FORMAT_FIELD_NAMES`.
+
+### Changed
+
+* Set minimum required version of optional Polymorphic Models for Django to 4.0.0.
+
+### Removed
+
+* Removed support for Python 3.8.
+* Removed support for Django REST framework 3.14.
+* Removed support for Django 5.0.
+* Removed built-in support for generating OpenAPI schema. Use [drf-spectacular-json-api](https://github.com/jokiefer/drf-spectacular-json-api/) instead.
+
+## [7.1.0] - 2024-10-25
+
+This is the last release supporting Python 3.8, Django 5.0 and Django REST framework 3.14.
+
+### Fixed
+
+* Handled zero as a valid ID for resource (regression since 6.1.0)
+* Ensured that patching a To-Many relationship with the `RelationshipView` correctly raises request error when passing in `None`.
+  For emptying a To-Many relationship an empty array should be used as per [JSON:API spec](https://jsonapi.org/format/#crud-updating-to-many-relationships)
+
+### Added
+
+* Added support for Django 5.1
+* Added support for Python 3.13
+
+### Deprecated
+
+* Deprecated built-in support for generating OpenAPI schema. Use [drf-spectacular-json-api](https://github.com/jokiefer/drf-spectacular-json-api/) instead.
+
+## [7.0.2] - 2024-06-28
+
+### Fixed
+
+* Allow overwriting of url field again (regression since 7.0.0)
+* Ensured that no fields are rendered when sparse fields is set to an empty value. (regression since 7.0.0)
+
+## [7.0.1] - 2024-06-06
+
+### Added
+
+* Added `429 Too Many Requests` as a possible error response in the OpenAPI schema.
+
+### Fixed
+
+* Ensured that URL and id field are kept when using sparse fields (regression since 7.0.0)
+
+## [7.0.0] - 2024-05-02
+
+### Added
+
+* Added support for Python 3.12
+* Added support for Django 5.0
+* Added support for Django REST framework 3.15
+
+### Fixed
+
+* Fixed OpenAPI schema generation for `Serializer` when used inside another `Serializer` or as a child of `ListField`.
+* `ModelSerializer` fields are now returned in the same order than DRF
+* Avoided that an empty attributes dict is rendered in case serializer does not
+  provide any attribute fields.
+* Avoided shadowing of exception when rendering errors (regression since 4.3.0).
+* Ensured that sparse fields only applies when rendering, not when parsing.
+* Adjusted that sparse fields properly removes meta fields when not defined.
+
+### Removed
+
+* Removed support for Python 3.7.
+* Removed support for Django 3.2.
+* Removed support for Django 4.0.
+* Removed support for Django 4.1.
+* Removed support for Django REST framework 3.13.
+* Removed obsolete compat `NullBooleanField` and `get_reference` definitions.
+
+## [6.1.0] - 2023-08-25
+
+This is the last release supporting Python 3.7, Django 3.2, Django 4.0, Django 4.1 and Django REST framework 3.13.
+
+### Added
+
+* Added support for Python 3.11.
+* Added support for Django 4.2.
+* Added `400 Bad Request` as a possible error response in the OpenAPI schema.
+
+### Changed
+
+* Added support to overwrite serializer methods in customized schema class
+* Adjusted some still old formatted strings to f-strings.
+* Replaced `OrderedDict` with `dict` which is also ordered since Python 3.7.
+* Compound document "include" parameter is only included in the OpenAPI schema if serializer
+  implements `included_serializers`.
+* Allowed overwriting of resource id by defining an `id` field on the serializer.
+
+    Example:
+    ```python
+    class CustomIdSerializer(serializers.Serializer):
+        id = serializers.CharField(source='name')
+        body = serializers.CharField()
+    ```
+
+* Allowed overwriting resource id on resource related fields by creating custom `ResourceRelatedField`.
+
+    Example:
+    ```python
+    class CustomResourceRelatedField(relations.ResourceRelatedField):
+        def get_resource_id(self, value):
+            return value.name
+    ```
+
+* `SerializerMethodResourceRelatedField(many=True)` relationship data now includes a meta section.
+* Required relationship fields are now marked as required in the OpenAPI schema.
+* Objects in the included array are documented in the OpenAPI schema to possibly have additional
+  properties in their "attributes" and "relationships" objects.
+
+### Fixed
+
+* Refactored handling of the `sort` query parameter to fix duplicate declaration in the generated OpenAPI schema definition
+* Non-field serializer errors are given a source.pointer value of "/data".
+* Fixed "id" field being added to /data/attributes in the OpenAPI schema when it is not rendered there.
+* Fixed `SerializerMethodResourceRelatedField(many=True)` fields being given
+  a "reltoone" schema reference instead of "reltomany".
+* Callable field default values are excluded from the OpenAPI schema, as they don't resolve to YAML data types.
+
+## [6.0.0] - 2022-09-24
+
+### Fixed
+
+* Fixed invalid relationship pointer in error objects when field naming formatting is used.
+* Properly resolved related resource type when nested source field is defined.
+* Prevented overwriting of pointer in custom error object
+* Adhered to field naming format setting when generating schema parameters and required fields.
+
+### Added
+
+* Added support for Django 4.1.
+* Added support for Django REST framework 3.14.
+* Expanded JSONParser API with `parse_data` method
+
+### Changed
+
+* Improved documentation of how to override DRF's generateschema `--generator_class` to generate a proper DJA OAS schema.
+
+### Removed
+
+* Removed support for Django 2.2.
+* Removed support for Django REST framework 3.12.
+
+## [5.0.0] - 2022-01-03
+
+This release is not backwards compatible. For easy migration best upgrade first to version
+4.3.0 and resolve all deprecation warnings before updating to 5.0.0
+
+This is the last release supporting Django 2.2 and Django REST framework 3.12.
+
+### Added
+
+* Added support for Django REST framework 3.13.
+
+### Changed
+
+* Adjusted to only use f-strings for slight performance improvement.
+* Set minimum required version of inflection to 0.5.
+* Set minimum required version of Django Filter to 2.4.
+* Set minimum required version of Polymorphic Models for Django to 3.0.
+* Set minimum required version of PyYAML to 5.4.
+
+### Removed
+
+* Removed support for Django 3.0.
+* Removed support for Django 3.1.
+* Removed support for Python 3.6.
+* Removed obsolete method `utils.get_included_serializers`.
+* Removed optional `format_type` argument of `utils.format_link_segment`.
+* Removed `format_type`s default argument of `utils.format_value`. `format_type` is now required.
+
+## [4.3.0] - 2021-12-10
+
+This is the last release supporting Django 3.0, Django 3.1 and Python 3.6.
+
+### Added
+
+* Added support for Django 4.0.
+* Added support for Python 3.10.
+
+### Fixed
+
+* Adjusted error messages to correctly use capital "JSON:API" abbreviation as used in the specification.
+* Avoid error when `parser_context` is `None` while parsing.
+* Raise comprehensible error when reserved field names `meta` and `results` are used.
+* Use `relationships` in the error object `pointer` when the field is actually a relationship.
+* Added missing inflection to the generated OpenAPI schema.
+* Added missing error message when `resource_name` is not properly configured.
+
+### Changed
+
+* Moved resolving of `included_serialzers` and `related_serializers` classes to serializer's meta class.
+
+### Deprecated
+
+* Deprecated `get_included_serializers(serializer)` function under `rest_framework_json_api.utils`. Use `serializer.included_serializers` instead.
+* Deprecated support for field name `type` as it may not be used according to the [JSON:API spec](https://jsonapi.org/format/#document-resource-object-fields).
+
+## [4.2.1] - 2021-07-06
+
+### Fixed
+
+* Included `PreloadIncludesMixin` in `ReadOnlyModelViewSet` to enable the usage of [performance utilities](https://django-rest-framework-json-api.readthedocs.io/en/stable/usage.html#performance-improvements) on read only views (regression since 2.8.0)
+* Removed invalid validation of default `included_resources` (regression since 4.2.0)
+
+## [4.2.0] - 2021-05-12
+
+### Added
+
+* Added support for Django 3.2.
+* Added support for tags in OAS schema
+
+### Fixed
+
+* Allow `get_serializer_class` to be overwritten when using related urls without defining `serializer_class` fallback
+* Preserve field names when no formatting is configured.
+* Properly support `JSON_API_FORMAT_RELATED_LINKS` setting in related urls. In case you want to use `dasherize` for formatting links make sure that your url pattern matches dashes as well like following example:
+  ```
+  url(r'^orders/(?P<pk>[^/.]+)/(?P<related_field>[-\w]+)/$',
+      OrderViewSet.as_view({'get': 'retrieve_related'}),
+      name='order-related'),
+  ```
+* Ensure default `included_resources` are considered when calculating prefetches.
+* Avoided error when using `include` query parameter on related urls (a regression since 4.1.0)
+
+### Deprecated
+
+* Deprecated default `format_type` argument of `rest_framework_json_api.utils.format_value`. Use `rest_framework_json_api.utils.format_field_name` or specify specifc `format_type` instead.
+* Deprecated `format_type` argument of `rest_framework_json_api.utils.format_link_segment`. Use `rest_framework_json_api.utils.format_value` instead.
+
+## [4.1.0] - 2021-03-08
+
+### Added
+
+* Ability for the user to select `included_serializers` to apply when using `BrowsableAPI`, based on available `included_serializers` defined for the current endpoint.
+* Ability for the user to format serializer properties in URL segments using the `JSON_API_FORMAT_RELATED_LINKS` setting.
+
+### Fixed
+
+* Allow users to overwrite a view's `get_serializer_class()` method when using [related urls](https://django-rest-framework-json-api.readthedocs.io/en/stable/usage.html#related-urls)
+* Correctly resolve the resource type of `ResourceRelatedField(many=True)` fields on plain serializers
+* Render `meta_fields` in included resources
+
+
 ## [4.0.0] - 2020-10-31
 
 This release is not backwards compatible. For easy migration best upgrade first to version
@@ -43,15 +303,15 @@ This release is not backwards compatible. For easy migration best upgrade first
 
 ## [3.2.0] - 2020-08-26
 
-This is the last release supporting Django 1.11, Django 2.1, Django REST Framework 3.10, Django REST Framework 3.11 and Python 3.5.
+This is the last release supporting Django 1.11, Django 2.1, Django REST framework 3.10, Django REST framework 3.11 and Python 3.5.
 
 ### Added
 
 * Added support for serializing nested serializers as attribute json value introducing setting `JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE`
- * Note: As keys of nested serializers are not json:api spec field names they are not inflected by format field names option.
-* Added `rest_framework_json_api.serializer.Serializer` class to support initial JSON API views without models.
+ * Note: As keys of nested serializers are not JSON:API spec field names they are not inflected by format field names option.
+* Added `rest_framework_json_api.serializer.Serializer` class to support initial JSON:API views without models.
   * Note that serializers derived from this class need to define `resource_name` in their `Meta` class.
-  * This fix might be a **BREAKING CHANGE** if you use `rest_framework_json_api.serializers.Serializer` for non json:api spec views (usually `APIView`). You need to change those serializers classes to use `rest_framework.serializers.Serializer` instead.
+  * This fix might be a **BREAKING CHANGE** if you use `rest_framework_json_api.serializers.Serializer` for non JSON:API spec views (usually `APIView`). You need to change those serializers classes to use `rest_framework.serializers.Serializer` instead.
 
 ### Fixed
 
@@ -111,7 +371,7 @@ This release is not backwards compatible. For easy migration best upgrade first
 * Removed support for Python 2.7 and 3.4.
 * Removed support for Django Filter 1.1.
 * Removed obsolete dependency six.
-* Removed support for Django REST Framework <=3.9.
+* Removed support for Django REST framework <=3.9.
 * Removed support for Django 2.0.
 * Removed obsolete mixins `MultipleIDMixin` and `PrefetchForIncludesHelperMixin`
 * Removed obsolete settings `JSON_API_FORMAT_KEYS`, `JSON_API_FORMAT_RELATION_KEYS` and
@@ -128,7 +388,7 @@ This release is not backwards compatible. For easy migration best upgrade first
 
 ## [2.8.0] - 2019-06-13
 
-This is the last release supporting Python 2.7, Python 3.4, Django Filter 1.1, Django REST Framework <=3.9 and Django 2.0.
+This is the last release supporting Python 2.7, Python 3.4, Django Filter 1.1, Django REST framework <=3.9 and Django 2.0.
 
 ### Added
 
@@ -146,7 +406,7 @@ This is the last release supporting Python 2.7, Python 3.4, Django Filter 1.1, D
 * Don't swallow `filter[]` params when there are several
 * Fix DeprecationWarning regarding collections.abc import in Python 3.7
 * Allow OPTIONS request to be used on RelationshipView
-* Remove non-JSONAPI methods (PUT and TRACE) from ModelViewSet and RelationshipView.
+* Remove non-JSON:API methods (PUT and TRACE) from ModelViewSet and RelationshipView.
   This fix might be a **BREAKING CHANGE** if your clients are incorrectly using PUT instead of PATCH.
 * Avoid validation error for missing fields on a PATCH request using polymorphic serializers
 
@@ -179,7 +439,7 @@ This is the last release supporting Python 2.7, Python 3.4, Django Filter 1.1, D
 * Add testing configuration to `REST_FRAMEWORK` configuration as described in [DRF](https://www.django-rest-framework.org/api-guide/testing/#configuration)
 * Add `HyperlinkedRelatedField` and `SerializerMethodHyperlinkedRelatedField`. See [usage docs](docs/usage.md#related-fields)
 * Add related urls support. See [usage docs](docs/usage.md#related-urls)
-* Add optional [jsonapi-style](http://jsonapi.org/format/) filter backends. See [usage docs](docs/usage.md#filter-backends)
+* Add optional [jsonapi-style](https://jsonapi.org/format/) filter backends. See [usage docs](docs/usage.md#filter-backends)
 
 ### Deprecated
 
@@ -205,9 +465,9 @@ This is the last release supporting Python 2.7, Python 3.4, Django Filter 1.1, D
 * Add new pagination classes based on JSON:API query parameter *recommendations*:
   * `JsonApiPageNumberPagination` and `JsonApiLimitOffsetPagination`. See [usage docs](docs/usage.md#pagination).
 * Add `ReadOnlyModelViewSet` extension with prefetch mixins
-* Add support for Django REST Framework 3.8.x
+* Add support for Django REST framework 3.8.x
 * Introduce `JSON_API_FORMAT_FIELD_NAMES` option replacing `JSON_API_FORMAT_KEYS` but in comparison preserving
-  values from being formatted as attributes can contain any [json value](http://jsonapi.org/format/#document-resource-object-attributes).
+  values from being formatted as attributes can contain any [json value](https://jsonapi.org/format/#document-resource-object-attributes).
 * Allow overwriting of `get_queryset()` in custom `ResourceRelatedField`
 
 ### Deprecated
@@ -233,13 +493,13 @@ This is the last release supporting Python 2.7, Python 3.4, Django Filter 1.1, D
 
 ### Added
 
-* Add support for Django REST Framework 3.7.x.
+* Add support for Django REST framework 3.7.x.
 * Add support for Django 2.0.
 
 ### Removed
 
 * Drop support for Django 1.8 - 1.10 (EOL)
-* Drop support for Django REST Framework < 3.6.3
+* Drop support for Django REST framework < 3.6.3
   (3.6.3 is the first to support Django 1.11)
 * Drop support for Python 3.3 (EOL)
 
@@ -266,7 +526,7 @@ This is the last release supporting Python 2.7, Python 3.4, Django Filter 1.1, D
 
 ### Added
 
-* Add support for Django REST Framework 3.5 and 3.6
+* Add support for Django REST framework 3.5 and 3.6
 * Add support for Django 1.11
 * Add support for Python 3.6
 
diff --git a/MANIFEST.in b/MANIFEST.in
index 44e3d86c..9102318a 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,7 +2,7 @@ include AUTHORS
 include LICENSE
 include README.rst
 recursive-include example *
-recursive-exclude example *.pyc *.pyo
+recursive-include tests *
 
 global-exclude __pycache__
 global-exclude *.py[co]
diff --git a/README.rst b/README.rst
index d5d5959f..05c01c04 100644
--- a/README.rst
+++ b/README.rst
@@ -1,9 +1,10 @@
 ==================================
-JSON API and Django Rest Framework
+JSON:API and Django REST framework
 ==================================
 
-.. image:: https://travis-ci.com/django-json-api/django-rest-framework-json-api.svg?branch=master
-   :target: https://travis-ci.com/django-json-api/django-rest-framework-json-api
+.. image:: https://github.com/django-json-api/django-rest-framework-json-api/workflows/Tests/badge.svg
+   :alt: Tests
+   :target: https://github.com/django-json-api/django-rest-framework-json-api/actions
 
 .. image:: https://readthedocs.org/projects/django-rest-framework-json-api/badge/?version=latest
    :alt: Read the docs
@@ -17,18 +18,20 @@ JSON API and Django Rest Framework
 Overview
 --------
 
-**JSON API support for Django REST Framework**
+**JSON:API support for Django REST framework**
 
 * Documentation: https://django-rest-framework-json-api.readthedocs.org/
-* Format specification: http://jsonapi.org/format/
+* Format specification: https://jsonapi.org/format/
 
 
-By default, Django REST Framework will produce a response like::
+By default, Django REST framework will produce a response like:
+
+.. code:: JSON
 
     {
         "count": 20,
-        "next": "http://example.com/api/1.0/identities/?page=3",
-        "previous": "http://example.com/api/1.0/identities/?page=1",
+        "next": "https://example.com/api/1.0/identities/?page=3",
+        "previous": "https://example.com/api/1.0/identities/?page=1",
         "results": [{
             "id": 3,
             "username": "john",
@@ -37,14 +40,16 @@ By default, Django REST Framework will produce a response like::
     }
 
 
-However, for an ``identity`` model in JSON API format the response should look
-like the following::
+However, for an ``identity`` model in JSON:API format the response should look
+like the following:
+
+.. code:: JSON
 
     {
         "links": {
-            "prev": "http://example.com/api/1.0/identities",
-            "self": "http://example.com/api/1.0/identities?page=2",
-            "next": "http://example.com/api/1.0/identities?page=3",
+            "prev": "https://example.com/api/1.0/identities",
+            "self": "https://example.com/api/1.0/identities?page=2",
+            "next": "https://example.com/api/1.0/identities?page=3",
         },
         "data": [{
             "type": "identities",
@@ -66,13 +71,13 @@ like the following::
 Goals
 -----
 
-As a Django REST Framework JSON API (short DJA) we are trying to address following goals:
+As a Django REST framework JSON:API (short DJA) we are trying to address following goals:
 
-1. Support the `JSON API`_ spec to compliance
+1. Support the `JSON:API`_ spec to compliance
 
-2. Be as compatible with `Django REST Framework`_ as possible
+2. Be as compatible with `Django REST framework`_ as possible
 
-   e.g. issues in Django REST Framework should be fixed upstream and not worked around in DJA
+   e.g. issues in Django REST framework should be fixed upstream and not worked around in DJA
 
 3. Have sane defaults to be as easy to pick up as possible
 
@@ -80,79 +85,77 @@ As a Django REST Framework JSON API (short DJA) we are trying to address followi
 
 5. Be performant
 
-.. _JSON API: http://jsonapi.org
-.. _Django REST Framework: https://www.django-rest-framework.org/
+.. _JSON:API: https://jsonapi.org
+.. _Django REST framework: https://www.django-rest-framework.org/
 
 ------------
 Requirements
 ------------
 
-1. Python (3.6, 3.7, 3.8, 3.9)
-2. Django (2.2, 3.0, 3.1)
-3. Django REST Framework (3.12)
+1. Python (3.9, 3.10, 3.11, 3.12, 3.13)
+2. Django (4.2, 5.1, 5.2)
+3. Django REST framework (3.15, 3.16)
 
-We **highly** recommend and only officially support the latest patch release of each Python, Django and REST Framework series.
+We **highly** recommend and only officially support the latest patch release of each Python, Django and REST framework series.
 
-Generally Python and Django series are supported till the official end of life. For Django REST Framework the last two series are supported.
+Generally Python and Django series are supported till the official end of life. For Django REST framework the last two series are supported.
+
+For optional dependencies such as Django Filter only the latest release is officially supported even though lower versions should work as well.
 
 ------------
 Installation
 ------------
 
-From PyPI
-^^^^^^^^^
+Install using ``pip``...
 
-::
+.. code:: sh
 
     $ pip install djangorestframework-jsonapi
     $ # for optional package integrations
     $ pip install djangorestframework-jsonapi['django-filter']
     $ pip install djangorestframework-jsonapi['django-polymorphic']
-    $ pip install djangorestframework-jsonapi['openapi']
 
 
-From Source
-^^^^^^^^^^^
+or from source...
 
-::
+.. code:: sh
 
     $ git clone https://github.com/django-json-api/django-rest-framework-json-api.git
     $ cd django-rest-framework-json-api
     $ pip install -e .
 
 
+and add ``rest_framework_json_api`` to your ``INSTALLED_APPS`` setting below ``rest_framework``.
+
+.. code:: python
+
+    INSTALLED_APPS = [
+        ...
+        'rest_framework',
+        'rest_framework_json_api',
+        ...
+    ]
+
+
 Running the example app
 ^^^^^^^^^^^^^^^^^^^^^^^
 
 It is recommended to create a virtualenv for testing. Assuming it is already
 installed and activated:
 
-::
+.. code:: sh
 
     $ git clone https://github.com/django-json-api/django-rest-framework-json-api.git
     $ cd django-rest-framework-json-api
     $ pip install -Ur requirements.txt
-    $ django-admin migrate --settings=example.settings
-    $ django-admin loaddata drf_example --settings=example.settings
-    $ django-admin runserver --settings=example.settings
+    $ django-admin migrate --settings=example.settings --pythonpath .
+    $ django-admin loaddata drf_example --settings=example.settings --pythonpath .
+    $ django-admin runserver --settings=example.settings --pythonpath .
 
 Browse to
-* http://localhost:8000 for the list of available collections (in a non-JSONAPI format!),
-* http://localhost:8000/swagger-ui/ for a Swagger user interface to the dynamic schema view, or
-* http://localhost:8000/openapi for the schema view's OpenAPI specification document.
 
+* http://localhost:8000 for the list of available collections (in a non-JSON:API format!),
 
-Running Tests and linting
-^^^^^^^^^^^^^^^^^^^^^^^^^
-
-It is recommended to create a virtualenv for testing. Assuming it is already
-installed and activated:
-
-::
-
-    $ pip install -Ur requirements.txt
-    $ flake8
-    $ pytest
 
 -----
 Usage
@@ -160,7 +163,7 @@ Usage
 
 
 ``rest_framework_json_api`` assumes you are using class-based views in Django
-Rest Framework.
+REST framework.
 
 
 Settings
@@ -170,7 +173,7 @@ One can either add ``rest_framework_json_api.parsers.JSONParser`` and
 ``rest_framework_json_api.renderers.JSONRenderer`` to each ``ViewSet`` class, or
 override ``settings.REST_FRAMEWORK``
 
-::
+.. code:: python
 
     REST_FRAMEWORK = {
         'PAGE_SIZE': 10,
@@ -184,7 +187,7 @@ override ``settings.REST_FRAMEWORK``
         ),
         'DEFAULT_RENDERER_CLASSES': (
             'rest_framework_json_api.renderers.JSONRenderer',
-            'rest_framework.renderers.BrowsableAPIRenderer',
+            'rest_framework_json_api.renderers.BrowsableAPIRenderer',
         ),
         'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
         'DEFAULT_FILTER_BACKENDS': (
@@ -202,4 +205,4 @@ override ``settings.REST_FRAMEWORK``
 
 This package provides much more including automatic inflection of JSON keys, extra top level data (using nested
 serializers), relationships, links, paginators, filters, and handy shortcuts.
-Read more at http://django-rest-framework-json-api.readthedocs.org/
+Read more at https://django-rest-framework-json-api.readthedocs.org/
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..5cb25b29
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,9 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+If you believe you've found something in Django REST framework JSON:API which has security implications, please **do not raise the issue in a public forum**.
+
+Use the security advisory to [report a vulnerability](https://github.com/django-json-api/django-rest-framework-json-api/security/advisories/new) instead.
+
+The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 5ab82991..0715c364 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -1,30 +1,71 @@
-Contributing
-============
+# Contributing
 
-DJA should be easy to contribute to.
+Django REST framework JSON:API (aka DJA)  should be easy to contribute to.
 If anything is unclear about how to contribute,
 please submit an issue on GitHub so that we can fix it!
 
-How
----
+Before writing any code, have a conversation on a GitHub issue to see
+if the proposed change makes sense for the project.
 
-Before writing any code,
-have a conversation on a GitHub issue
-to see if the proposed change makes sense
-for the project.
+## Setup development environment
 
-Fork DJA on [GitHub](https://github.com/django-json-api/django-rest-framework-json-api) and
-[submit a Pull Request](https://help.github.com/articles/creating-a-pull-request/)
-when you're ready.
+### Clone
 
-For maintainers
----------------
+To start developing on Django REST framework JSON:API you need to first clone the repository:
 
-To upload a release (using version 1.2.3 as the example):
+    git clone https://github.com/django-json-api/django-rest-framework-json-api.git
 
-```bash
-(venv)$ python setup.py sdist bdist_wheel
-(venv)$ twine upload dist/*
-(venv)$ git tag -a v1.2.3 -m 'Release 1.2.3'
-(venv)$ git push --tags
-```
+### Testing
+
+To run tests clone the repository, and then:
+
+     # Setup the virtual environment
+     python3 -m venv env
+     source env/bin/activate
+     pip install -r requirements.txt
+
+     # Format code
+     black .
+
+     # Run linting
+     flake8
+
+     # Run tests
+     pytest
+
+### Running against multiple environments
+
+You can also use the excellent [tox](https://tox.readthedocs.io/en/latest/) testing tool to run the tests against all supported versions of Python and Django.  Install `tox` globally, and then simply run:
+
+    tox
+
+
+### Setup pre-commit
+
+pre-commit hooks is an additional option to check linting and formatting of code independent of
+an editor before you commit your changes with git.
+
+To setup pre-commit hooks first create a testing environment as explained above before running below commands:
+
+    pip install pre-commit
+    pre-commit install
+
+## For maintainers
+
+### Create release
+
+To upload a release (using version 1.2.3 as the example) first setup testing environment as above before running below commands:
+
+    python setup.py sdist bdist_wheel
+    twine upload dist/*
+    git tag -a v1.2.3 -m 'Release 1.2.3'
+    git push --tags
+
+
+### Add maintainer
+
+In case a new maintainer joins our team we need to consider to what of following services we want to add them too:
+
+* [Github organization](https://github.com/django-json-api)
+* [Read the Docs project](https://django-rest-framework-json-api.readthedocs.io/)
+* [PyPi project](https://pypi.org/project/djangorestframework-jsonapi/)
diff --git a/docs/Makefile b/docs/Makefile
index fc44e850..ba724aee 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -9,7 +9,7 @@ BUILDDIR      = _build
 
 # User-friendly check for sphinx-build
 ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
-$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/)
 endif
 
 # Internal variables.
diff --git a/docs/conf.py b/docs/conf.py
index ee435d36..80a85e4b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
 #
-# Django REST Framework JSON API documentation build configuration file, created by
+#
+# Django REST framework JSON:API documentation build configuration file, created by
 # sphinx-quickstart on Fri Jul 24 23:31:15 2015.
 #
 # This file is execfile()d with the current directory set to its
@@ -15,7 +15,6 @@
 
 import datetime
 import os
-import shlex
 import sys
 
 import django
@@ -26,44 +25,44 @@
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
-sys.path.insert(0, os.path.abspath('..'))
-os.environ['DJANGO_SETTINGS_MODULE'] = 'example.settings'
+sys.path.insert(0, os.path.abspath(".."))
+os.environ["DJANGO_SETTINGS_MODULE"] = "example.settings"
 django.setup()
 
 # Auto-generate API documentation.
-main(['-o', 'apidoc', '-f', '-e', '-T', '-M', '../rest_framework_json_api'])
+main(["-o", "apidoc", "-f", "-e", "-T", "-M", "../rest_framework_json_api"])
 
 # -- General configuration ------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
+# needs_sphinx = '1.0'
 
 # Add any Sphinx extension module names here, as strings. They can be
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
-extensions = ['sphinx.ext.autodoc', 'recommonmark']
-autodoc_member_order = 'bysource'
+extensions = ["sphinx.ext.autodoc", "recommonmark", "sphinx_rtd_theme"]
+autodoc_member_order = "bysource"
 autodoc_inherit_docstrings = False
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix(es) of source filenames.
 # You can specify multiple suffix as a list of string:
 # source_suffix = ['.rst', '.md']
-source_suffix = ['.rst', '.md']
+source_suffix = [".rst", ".md"]
 
 # The encoding of source files.
-#source_encoding = 'utf-8-sig'
+# source_encoding = 'utf-8-sig'
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # General information about the project.
-project = 'Django REST Framework JSON API'
+project = "Django REST framework JSON:API"
 year = datetime.date.today().year
-copyright = '{}, Django REST Framework JSON API contributors'.format(year)
-author = 'Django REST Framework JSON API contributors'
+copyright = f"{year}, Django REST framework JSON:API contributors"
+author = "Django REST framework JSON:API contributors"
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -79,41 +78,41 @@
 #
 # This is also used if you do content translation via gettext catalogs.
 # Usually you set "language" from the command line for these cases.
-language = None
+language = "en"
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
-#today = ''
+# today = ''
 # Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
-exclude_patterns = ['_build', 'pull_request_template.md']
+exclude_patterns = ["_build"]
 
 # The reST default role (used for this markup: `text`) to use for all
 # documents.
-#default_role = None
+# default_role = None
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
 
 # If true, the current module name will be prepended to all description
 # unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
 
 # If true, sectionauthor and moduleauthor directives will be shown in the
 # output. They are ignored by default.
-#show_authors = False
+# show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'default'
+pygments_style = "default"
 
 # A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
 
 # If true, keep warnings as "system message" paragraphs in the built documents.
-#keep_warnings = False
+# keep_warnings = False
 
 # If true, `todo` and `todoList` produce output, else they produce nothing.
 todo_include_todos = False
@@ -123,150 +122,145 @@
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
-html_theme = 'default'
-
-on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
-
-if not on_rtd:  # only import and set the theme if we're building docs locally
-    import sphinx_rtd_theme
-    html_theme = 'sphinx_rtd_theme'
-    html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+html_theme = "sphinx_rtd_theme"
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+# html_theme_options = {}
 
 # Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# html_theme_path = []
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-#html_title = None
+# html_title = None
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-#html_logo = None
+# html_logo = None
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
-#html_favicon = None
+# html_favicon = None
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
 
 # Add any extra paths that contain custom files (such as robots.txt or
 # .htaccess) here, relative to this directory. These files are copied
 # directly to the root of the documentation.
-#html_extra_path = []
+# html_extra_path = []
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
 
 # If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = True
 
 # If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
 
 # If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
 
 # If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# html_show_sourcelink = True
 
 # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+# html_show_sphinx = True
 
 # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+# html_show_copyright = True
 
 # If true, an OpenSearch description file will be output, and all pages will
 # contain a <link> tag referring to it.  The value of this option must be the
 # base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
 
 # This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
 
 # Language to be used for generating the HTML full-text search index.
 # Sphinx supports the following languages:
 #   'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
 #   'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr'
-#html_search_language = 'en'
+# html_search_language = 'en'
 
 # A dictionary with options for the search language support, empty by default.
 # Now only 'ja' uses this config value
-#html_search_options = {'type': 'default'}
+# html_search_options = {'type': 'default'}
 
 # The name of a javascript file (relative to the configuration directory) that
 # implements a search results scorer. If empty, the default will be used.
-#html_search_scorer = 'scorer.js'
+# html_search_scorer = 'scorer.js'
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'DjangoRESTFrameworkJSONAPIdoc'
+htmlhelp_basename = "DjangoRESTFrameworkJSONAPIdoc"
 
 # -- Options for LaTeX output ---------------------------------------------
 
 latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
-
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
-
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
-
-# Latex figure (float) alignment
-#'figure_align': 'htbp',
+    # The paper size ('letterpaper' or 'a4paper').
+    # 'papersize': 'letterpaper',
+    # The font size ('10pt', '11pt' or '12pt').
+    # 'pointsize': '10pt',
+    # Additional stuff for the LaTeX preamble.
+    # 'preamble': '',
+    # Latex figure (float) alignment
+    # 'figure_align': 'htbp',
 }
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-  (master_doc, 'DjangoRESTFrameworkJSONAPI.tex', 'Django REST Framework JSON API Documentation',
-   'Django REST Framework JSON API contributors', 'manual'),
+    (
+        master_doc,
+        "DjangoRESTFrameworkJSONAPI.tex",
+        "Django REST framework JSON:API Documentation",
+        "Django REST framework JSON:API contributors",
+        "manual",
+    ),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
 # the title page.
-#latex_logo = None
+# latex_logo = None
 
 # For "manual" documents, if this is true, then toplevel headings are parts,
 # not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
 
 # If true, show page references after internal links.
-#latex_show_pagerefs = False
+# latex_show_pagerefs = False
 
 # If true, show URL addresses after external links.
-#latex_show_urls = False
+# latex_show_urls = False
 
 # Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
 
 # If false, no module index is generated.
-#latex_domain_indices = True
+# latex_domain_indices = True
 
 
 # -- Options for manual page output ---------------------------------------
@@ -274,12 +268,17 @@
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    (master_doc, 'djangorestframeworkjsonapi', 'Django REST Framework JSON API Documentation',
-     [author], 1)
+    (
+        master_doc,
+        "djangorestframeworkjsonapi",
+        "Django REST framework JSON:API Documentation",
+        [author],
+        1,
+    )
 ]
 
 # If true, show URL addresses after external links.
-#man_show_urls = False
+# man_show_urls = False
 
 
 # -- Options for Texinfo output -------------------------------------------
@@ -288,19 +287,25 @@
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-  (master_doc, 'DjangoRESTFrameworkJSONAPI', 'Django REST Framework JSON API Documentation',
-   author, 'DjangoRESTFrameworkJSONAPI', 'One line description of project.',
-   'Miscellaneous'),
+    (
+        master_doc,
+        "DjangoRESTFrameworkJSONAPI",
+        "Django REST framework JSON:API Documentation",
+        author,
+        "DjangoRESTFrameworkJSONAPI",
+        "One line description of project.",
+        "Miscellaneous",
+    ),
 ]
 
 # Documents to append as an appendix to all manuals.
-#texinfo_appendices = []
+# texinfo_appendices = []
 
 # If false, no module index is generated.
-#texinfo_domain_indices = True
+# texinfo_domain_indices = True
 
 # How to display URL addresses: 'footnote', 'no', or 'inline'.
-#texinfo_show_urls = 'footnote'
+# texinfo_show_urls = 'footnote'
 
 # If true, do not generate a @detailmenu in the "Top" node's menu.
-#texinfo_no_detailmenu = False
+# texinfo_no_detailmenu = False
diff --git a/docs/getting-started.md b/docs/getting-started.md
index c305c801..81040e8e 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -1,16 +1,16 @@
 
 # Getting Started
 
-*Note: this package is named Django REST Framework JSON API to follow the naming
-convention of other Django REST Framework packages. Since that's quite a bit
+*Note: this package is named Django REST framework JSON:API to follow the naming
+convention of other Django REST framework packages. Since that's quite a bit
 to say or type this package will be referred to as DJA elsewhere in these docs.*
 
-By default, Django REST Framework produces a response like:
+By default, Django REST framework produces a response like:
 ``` js
 {
     "count": 20,
-    "next": "http://example.com/api/1.0/identities/?page=3",
-    "previous": "http://example.com/api/1.0/identities/?page=1",
+    "next": "https://example.com/api/1.0/identities/?page=3",
+    "previous": "https://example.com/api/1.0/identities/?page=1",
     "results": [{
         "id": 3,
         "username": "john",
@@ -20,15 +20,15 @@ By default, Django REST Framework produces a response like:
 ```
 
 
-However, for the same `identity` model in JSON API format the response should look
+However, for the same `identity` model in JSON:API format the response should look
 like the following:
 ``` js
 {
     "links": {
-        "first": "http://example.com/api/1.0/identities",
-        "last": "http://example.com/api/1.0/identities?page=5",
-        "next": "http://example.com/api/1.0/identities?page=3",
-        "prev": "http://example.com/api/1.0/identities",
+        "first": "https://example.com/api/1.0/identities",
+        "last": "https://example.com/api/1.0/identities?page=5",
+        "next": "https://example.com/api/1.0/identities?page=3",
+        "prev": "https://example.com/api/1.0/identities",
     },
     "data": [{
         "type": "identities",
@@ -51,29 +51,40 @@ like the following:
 
 ## Requirements
 
-1. Python (3.6, 3.7, 3.8, 3.9)
-2. Django (2.2, 3.0, 3.1)
-3. Django REST Framework (3.12)
+1. Python (3.9, 3.10, 3.11, 3.12, 3.13)
+2. Django (4.2, 5.1, 5.2)
+3. Django REST framework (3.15, 3.16)
 
-We **highly** recommend and only officially support the latest patch release of each Python, Django and REST Framework series.
+We **highly** recommend and only officially support the latest patch release of each Python, Django and REST framework series.
 
-Generally Python and Django series are supported till the official end of life. For Django REST Framework the last two series are supported.
+Generally Python and Django series are supported till the official end of life. For Django REST framework the last two series are supported.
+
+For optional dependencies such as Django Filter only the latest release is officially supported even though lower versions should work as well.
 
 ## Installation
 
-From PyPI
+Install using `pip`...
 
     pip install djangorestframework-jsonapi
     # for optional package integrations
     pip install djangorestframework-jsonapi['django-filter']
     pip install djangorestframework-jsonapi['django-polymorphic']
-    pip install djangorestframework-jsonapi['openapi']
 
-From Source
+or from source...
 
     git clone https://github.com/django-json-api/django-rest-framework-json-api.git
     cd django-rest-framework-json-api && pip install -e .
 
+
+and add `rest_framework_json_api` to your `INSTALLED_APPS` setting below `rest_framework`.
+
+    INSTALLED_APPS = [
+        ...
+        'rest_framework',
+        'rest_framework_json_api',
+        ...
+    ]
+
 ## Running the example app
 
 	git clone https://github.com/django-json-api/django-rest-framework-json-api.git
@@ -87,9 +98,7 @@ From Source
 
 
 Browse to
-* [http://localhost:8000](http://localhost:8000) for the list of available collections (in a non-JSONAPI format!),
-* [http://localhost:8000/swagger-ui/](http://localhost:8000/swagger-ui/) for a Swagger user interface to the dynamic schema view, or
-* [http://localhost:8000/openapi](http://localhost:8000/openapi) for the schema view's OpenAPI specification document.
+* [http://localhost:8000](http://localhost:8000) for the list of available collections (in a non-JSON:API format!),
 
 ## Running Tests
 
diff --git a/docs/index.rst b/docs/index.rst
index b18b8b6e..a1219bc0 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,9 +1,9 @@
-.. Django REST Framework JSON API documentation master file, created by
+.. Django REST framework JSON:API documentation master file, created by
    sphinx-quickstart on Fri Jul 24 23:31:15 2015.
    You can adapt this file completely to your liking, but it should at least
    contain the root `toctree` directive.
 
-Welcome to Django REST Framework JSON API
+Welcome to Django REST framework JSON:API
 ==========================================================
 
 Contents:
diff --git a/docs/make.bat b/docs/make.bat
index ed3e42a0..c2ddc7bb 100644
--- a/docs/make.bat
+++ b/docs/make.bat
@@ -65,7 +65,7 @@ if errorlevel 9009 (
 	echo.may add the Sphinx directory to PATH.
 	echo.
 	echo.If you don't have Sphinx installed, grab it from
-	echo.http://sphinx-doc.org/
+	echo.https://sphinx-doc.org/
 	exit /b 1
 )
 
diff --git a/docs/usage.md b/docs/usage.md
index c091cd4d..00c12ee8 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -3,7 +3,7 @@
 The DJA package implements a custom renderer, parser, exception handler, query filter backends, and
 pagination. To get started enable the pieces in `settings.py` that you want to use.
 
-Many features of the [JSON:API](http://jsonapi.org/format) format standard have been implemented using
+Many features of the [JSON:API](https://jsonapi.org/format) format standard have been implemented using
 Mixin classes in `serializers.py`.
 The easiest way to make use of those features is to import ModelSerializer variants
 from `rest_framework_json_api` instead of the usual `rest_framework`
@@ -28,10 +28,9 @@ REST_FRAMEWORK = {
         # If performance testing, enable:
         # 'example.utils.BrowsableAPIRendererWithoutForms',
         # Otherwise, to play around with the browseable API, enable:
-        'rest_framework.renderers.BrowsableAPIRenderer'
+        'rest_framework_json_api.renderers.BrowsableAPIRenderer'
     ),
     'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
-    'DEFAULT_SCHEMA_CLASS': 'rest_framework_json_api.schemas.openapi.AutoSchema',
     'DEFAULT_FILTER_BACKENDS': (
         'rest_framework_json_api.filters.QueryParameterValidationFilter',
         'rest_framework_json_api.filters.OrderingFilter',
@@ -116,9 +115,9 @@ is used. This can help the client identify misspelled query parameters, for exam
 
 If you want to change the list of valid query parameters, override the `.query_regex` attribute:
 ```python
-# compiled regex that matches the allowed http://jsonapi.org/format/#query-parameters
+# compiled regex that matches the allowed https://jsonapi.org/format/#query-parameters
 # `sort` and `include` stand alone; `filter`, `fields`, and `page` have []'s
-query_regex = re.compile(r'^(sort|include)$|^(filter|fields|page)(\[[\w\.\-]+\])?$')
+query_regex = re.compile(r"^(sort|include)$|^(?P<type>filter|fields|page)(\[[\w\.\-]+\])?$")
 ```
 For example:
 ```python
@@ -126,7 +125,7 @@ import re
 from rest_framework_json_api.filters import QueryParameterValidationFilter
 
 class MyQPValidator(QueryParameterValidationFilter):
-    query_regex = re.compile(r'^(sort|include|page|page_size)$|^(filter|fields|page)(\[[\w\.\-]+\])?$')
+    query_regex = re.compile(r"^(sort|include|page|page_size)$|^(?P<type>filter|fields|page)(\[[\w\.\-]+\])?$")
 ```
 
 If you don't care if non-JSON:API query parameters are allowed (and potentially silently ignored),
@@ -134,8 +133,8 @@ simply don't use this filter backend.
 
 #### OrderingFilter
 
-`OrderingFilter` implements the [JSON:API `sort`](http://jsonapi.org/format/#fetching-sorting) and uses
-DRF's [ordering filter](http://django-rest-framework.readthedocs.io/en/latest/api-guide/filtering/#orderingfilter).
+`OrderingFilter` implements the [JSON:API `sort`](https://jsonapi.org/format/#fetching-sorting) and uses
+DRF's [ordering filter](https://www.django-rest-framework.org/api-guide/filtering/#orderingfilter).
 
 Per the JSON:API specification, "If the server does not support sorting as specified in the query parameter `sort`,
 it **MUST** return `400 Bad Request`." For example, for `?sort=abc,foo,def` where `foo` is a valid
@@ -159,14 +158,14 @@ If you want to silently ignore bad sort fields, just use `rest_framework.filters
 
 #### DjangoFilterBackend
 
-`DjangoFilterBackend` implements a Django ORM-style [JSON:API `filter`](http://jsonapi.org/format/#fetching-filtering)
+`DjangoFilterBackend` implements a Django ORM-style [JSON:API `filter`](https://jsonapi.org/format/#fetching-filtering)
 using the [django-filter](https://django-filter.readthedocs.io/) package.
 
 This filter is not part of the JSON:API standard per-se, other than the requirement
 to use the `filter` keyword: It is an optional implementation of a style of
 filtering in which each filter is an ORM expression as implemented by
 `DjangoFilterBackend` and seems to be in alignment with an interpretation of the
-[JSON:API _recommendations_](http://jsonapi.org/recommendations/#filtering), including relationship
+[JSON:API _recommendations_](https://jsonapi.org/recommendations/#filtering), including relationship
 chaining.
 
 Filters can be:
@@ -205,7 +204,7 @@ As this feature depends on `django-filter` you need to run
 #### SearchFilter
 
 To comply with JSON:API query parameter naming standards, DRF's
-[SearchFilter](https://django-rest-framework.readthedocs.io/en/latest/api-guide/filtering/#searchfilter) should
+[SearchFilter](https://www.django-rest-framework.org/api-guide/filtering/#searchfilter) should
 be configured to use a `filter[_something_]` query parameter. This can be done by default by adding the
 SearchFilter to `REST_FRAMEWORK['DEFAULT_FILTER_BACKENDS']` and setting `REST_FRAMEWORK['SEARCH_PARAM']` or
 adding the `.search_param` attribute to a custom class derived from `SearchFilter`.  If you do this and also
@@ -237,14 +236,29 @@ class MyViewset(ModelViewSet):
 
 ```
 
-### Exception handling
+### Error objects / Exception handling
 
 For the `exception_handler` class, if the optional `JSON_API_UNIFORM_EXCEPTIONS` is set to True,
-all exceptions will respond with the JSON API [error format](http://jsonapi.org/format/#error-objects).
+all exceptions will respond with the JSON:API [error format](https://jsonapi.org/format/#error-objects).
 
-When `JSON_API_UNIFORM_EXCEPTIONS` is False (the default), non-JSON API views will respond
+When `JSON_API_UNIFORM_EXCEPTIONS` is False (the default), non-JSON:API views will respond
 with the normal DRF error format.
 
+In case you need a custom error object you can simply raise an `rest_framework.serializers.ValidationError` like the following:
+
+```python
+raise serializers.ValidationError(
+    {
+        "id": "your-id",
+        "detail": "your detail message",
+        "source": {
+            "pointer": "/data/attributes/your-pointer",
+        }
+
+    }
+)
+```
+
 ### Performance Testing
 
 If you are trying to see if your viewsets are configured properly to optimize performance,
@@ -263,18 +277,53 @@ class MyModelSerializer(serializers.ModelSerializer):
     # ...
 ```
 
-### Setting the resource_name
+### Overwriting the resource object's id
+
+Per default the primary key property `pk` on the instance is used as the resource identifier.
+
+It is possible to overwrite the resource id by defining an `id` field on the serializer like:
+
+```python
+class UserSerializer(serializers.ModelSerializer):
+    id = serializers.CharField(source='email')
+    name = serializers.CharField()
+
+    class Meta:
+        model = User
+```
+
+This also works on generic serializers.
+
+In case you also use a model as a resource related field make sure to overwrite `get_resource_id` by creating a custom `ResourceRelatedField` class:
+
+```python
+class UserResourceRelatedField(ResourceRelatedField):
+    def get_resource_id(self, value):
+        return value.email
+
+class GroupSerializer(serializers.ModelSerializer):
+    user = UserResourceRelatedField(queryset=User.objects)
+    name = serializers.CharField()
+
+    class Meta:
+        model = Group
+```
+
+<div class="warning">
+    <strong>Note:</strong>
+    When using different id than primary key, make sure that your view
+    manages it properly by overwriting `get_object`.
+</div>
 
-You may manually set the `resource_name` property on views, serializers, or
-models to specify the `type` key in the json output. In the case of setting the
-`resource_name` property for models you must include the property inside a
-`JSONAPIMeta` class on the model. It is automatically set for you as the plural
-of the view or model name except on resources that do not subclass
-`rest_framework.viewsets.ModelViewSet`:
+### Setting resource identifier object type
 
+You may manually set resource identifier object type by using `resource_name` property on views, serializers, or
+models. In case of setting the `resource_name` property for models you must include the property inside a
+`JSONAPIMeta` class on the model. It is usually automatically set for you as the plural of the view or model name except
+on resources that do not subclass `rest_framework.viewsets.ModelViewSet`:
 
 Example - `resource_name` on View:
-``` python
+```python
 class Me(generics.GenericAPIView):
     """
     Current user's identity endpoint.
@@ -286,12 +335,9 @@ class Me(generics.GenericAPIView):
     allowed_methods = ['GET']
     permission_classes = (permissions.IsAuthenticated, )
 ```
-If you set the `resource_name` property on the object to `False` the data
-will be returned without modification.
-
 
 Example - `resource_name` on Model:
-``` python
+```python
 class Me(models.Model):
     """
     A simple model
@@ -309,12 +355,25 @@ on the view should be used sparingly as serializers and models are shared betwee
 multiple endpoints. Setting the `resource_name` on views may result in a different
 `type` being set depending on which endpoint the resource is fetched from.
 
+### Build JSON:API view output manually
+
+If in a view you want to build the output manually, you can set `resource_name` to `False`.
+
+Example:
+```python
+class User(ModelViewSet):
+    resource_name = False
+    queryset = User.objects.all()
+    serializer_class = UserSerializer
+
+    def retrieve(self, request, *args, **kwargs):
+        instance = self.get_object()
+        data = [{"id": 1, "type": "users", "attributes": {"fullName": "Test User"}}])
+```
 
 ### Inflecting object and relation keys
 
-This package includes the ability (off by default) to automatically convert [json
-api field names](http://jsonapi.org/format/#document-resource-object-fields) of requests and responses from the python/rest_framework's preferred underscore to
-a format of your choice. To hook this up include the following setting in your
+This package includes the ability (off by default) to automatically convert [JSON:API field names](https://jsonapi.org/format/#document-resource-object-fields) of requests and responses from the Django REST framework's preferred underscore to a format of your choice. To hook this up include the following setting in your
 project settings:
 
 ``` python
@@ -477,12 +536,54 @@ When set to pluralize:
 }
 ```
 
+#### Related URL segments
+
+Serializer properties in relationship and related resource URLs may be infected using the `JSON_API_FORMAT_RELATED_LINKS` setting.
+
+``` python
+JSON_API_FORMAT_RELATED_LINKS = 'dasherize'
+```
+
+For example, with a serializer property `created_by` and with `'dasherize'` formatting:
+
+```json
+{
+  "data": {
+      "type": "comments",
+      "id": "1",
+      "attributes": {
+          "text": "Comments are fun!"
+      },
+      "links": {
+          "self": "/comments/1"
+      },
+      "relationships": {
+        "created_by": {
+          "links": {
+            "self": "/comments/1/relationships/created-by",
+            "related": "/comments/1/created-by"
+          }
+        }
+      }
+  },
+  "links": {
+      "self": "/comments/1"
+  }
+}
+```
+
+The relationship name is formatted by the `JSON_API_FORMAT_FIELD_NAMES` setting, but the URL segments are formatted by the `JSON_API_FORMAT_RELATED_LINKS` setting.
+
+<div class="warning">
+    <strong>Note:</strong>
+    When using this setting make sure that your url pattern matches the formatted url segement.
+</div>
+
 ### Related fields
 
 #### ResourceRelatedField
 
-Because of the additional structure needed to represent relationships in JSON
-API, this package provides the `ResourceRelatedField` for serializers, which
+Because of the additional structure needed to represent relationships in JSON:API, this package provides the `ResourceRelatedField` for serializers, which
 works similarly to `PrimaryKeyRelatedField`. By default,
 `rest_framework_json_api.serializers.ModelSerializer` will use this for
 related fields automatically. It can be instantiated explicitly as in the
@@ -510,7 +611,7 @@ class OrderSerializer(serializers.ModelSerializer):
 
 ```
 
-In the [JSON:API spec](http://jsonapi.org/format/#document-resource-objects),
+In the [JSON:API spec](https://jsonapi.org/format/#document-resource-objects),
 relationship objects contain links to related objects. To make this work
 on a serializer we need to tell the `ResourceRelatedField` about the
 corresponding view. Use the `HyperlinkedModelSerializer` and instantiate
@@ -579,14 +680,14 @@ class LineItemViewSet(viewsets.ModelViewSet):
     serializer_class = LineItemSerializer
 
     def get_queryset(self):
-        queryset = super(LineItemViewSet, self).get_queryset()
+        queryset = super().get_queryset()
 
         # if this viewset is accessed via the 'order-lineitems-list' route,
         # it wll have been passed the `order_pk` kwarg and the queryset
         # needs to be filtered accordingly; if it was accessed via the
         # unnested '/lineitems' route, the queryset should include all LineItems
-        if 'order_pk' in self.kwargs:
-            order_pk = self.kwargs['order_pk']
+        order_pk = self.kwargs.get('order_pk')
+        if order_pk is not None:
             queryset = queryset.filter(order__pk=order_pk)
 
         return queryset
@@ -664,7 +765,7 @@ All you need is just add to `urls.py`:
 url(r'^orders/(?P<pk>[^/.]+)/$',
         OrderViewSet.as_view({'get': 'retrieve'}),
         name='order-detail'),
-url(r'^orders/(?P<pk>[^/.]+)/(?P<related_field>\w+)/$',
+url(r'^orders/(?P<pk>[^/.]+)/(?P<related_field>[-\w]+)/$',
         OrderViewSet.as_view({'get': 'retrieve_related'}),
         name='order-related'),
 ```
@@ -703,15 +804,23 @@ class OrderSerializer(serializers.HyperlinkedModelSerializer):
     }
 ```
 
+<div class="warning">
+    <strong>Note:</strong>
+    Even though with related urls relations are served on different urls there are still served
+    by the same view. This means that the object permission check is performed on the parent object.
+    In other words when the parent object is accessible by the user the related object will be as well.
+</div>
+
+
 ### RelationshipView
 `rest_framework_json_api.views.RelationshipView` is used to build
 relationship views (see the
-[JSON:API spec](http://jsonapi.org/format/#fetching-relationships)).
+[JSON:API spec](https://jsonapi.org/format/#fetching-relationships)).
 The `self` link on a relationship object should point to the corresponding
 relationship view.
 
 The relationship view is fairly simple because it only serializes
-[Resource Identifier Objects](http://jsonapi.org/format/#document-resource-identifier-objects)
+[Resource Identifier Objects](https://jsonapi.org/format/#document-resource-identifier-objects)
 rather than full resource objects. In most cases the following is sufficient:
 
 ```python
@@ -729,7 +838,7 @@ The urlconf would need to contain a route like the following:
 
 ```python
 url(
-    regex=r'^orders/(?P<pk>[^/.]+)/relationships/(?P<related_field>[^/.]+)$',
+    regex=r'^orders/(?P<pk>[^/.]+)/relationships/(?P<related_field>[-/w]+)$',
     view=OrderRelationshipView.as_view(),
     name='order-relationships'
 )
@@ -845,9 +954,9 @@ Related links will be created automatically when using the Relationship View.
 
 ### Included
 
-JSON API can include additional resources in a single network request.
+JSON:API can include additional resources in a single network request.
 The specification refers to this feature as
-[Compound Documents](http://jsonapi.org/format/#document-compound-documents).
+[Compound Documents](https://jsonapi.org/format/#document-compound-documents).
 Compound Documents can reduce the number of network requests
 which can lead to a better performing web application.
 To accomplish this,
@@ -891,7 +1000,7 @@ class QuestSerializer(serializers.ModelSerializer):
 
 Be aware that using included resources without any form of prefetching **WILL HURT PERFORMANCE** as it will introduce m\*(n+1) queries.
 
-A viewset helper was therefore designed to automatically preload data when possible. Such is automatically available when subclassing `ModelViewSet`.
+A viewset helper was therefore designed to automatically preload data when possible. Such is automatically available when subclassing `ModelViewSet` or `ReadOnlyModelViewSet`.
 
 It also allows to define custom `select_related` and `prefetch_related` for each requested `include` when needed in special cases:
 
@@ -945,123 +1054,21 @@ The `prefetch_related` case will issue 4 queries, but they will be small and fas
 ### Errors
 -->
 
-## Generating an OpenAPI Specification (OAS) 3.0 schema document
+## Third Party Packages
 
-DRF >= 3.12 has a [new OAS schema functionality](https://www.django-rest-framework.org/api-guide/schemas/) to generate an
-[OAS 3.0 schema](https://www.openapis.org/) as a YAML or JSON file.
+### About Third Party Packages
 
-DJA extends DRF's schema support to generate an OAS schema in the JSON:API format.
+Following the example of [Django REST framework](https://www.django-rest-framework.org/community/third-party-packages/) we also support, encourage and strongly favor the creation of Third Party Packages to encapsulate new behavior rather than adding additional functionality directly to Django REST framework JSON:API especially when it involves adding new dependencies.
 
-### AutoSchema Settings
+We aim to make creating third party packages as easy as possible, whilst keeping a simple and well maintained core API. By promoting third party packages we ensure that the responsibility for a package remains with its author. If a package proves suitably popular it can always be considered for inclusion into the DJA core.
 
-In order to produce an OAS schema that properly represents the JSON:API structure
-you have to either add a `schema` attribute to each view class or set the `REST_FRAMEWORK['DEFAULT_SCHEMA_CLASS']`
-to DJA's version of AutoSchema.
+### Existing Third Party Packages
 
-#### View-based
+To submit new content, [open an issue](https://github.com/django-json-api/django-rest-framework-json-api/issues/new/choose) or [create a pull request](https://github.com/django-json-api/django-rest-framework-json-api/compare).
 
-```python
-from rest_framework_json_api.schemas.openapi import AutoSchema
+* [drf-yasg-json-api](https://github.com/glowka/drf-yasg-json-api) - Automated generation of Swagger/OpenAPI 2.0 from Django REST framework JSON:API endpoints.
+* [drf-spectacular-json-api] - OpenAPI 3 schema generator for Django REST framework JSON:API based on drf-spectacular.
 
-class MyViewset(ModelViewSet):
-    schema = AutoSchema
-    ...
-```
 
-#### Default schema class
-
-```python
-REST_FRAMEWORK = {
-    # ...
-    'DEFAULT_SCHEMA_CLASS': 'rest_framework_json_api.schemas.openapi.AutoSchema',
-}
-```
-
-### Adding additional OAS schema content
-
-You can extend the OAS schema document by subclassing
-[`SchemaGenerator`](https://www.django-rest-framework.org/api-guide/schemas/#schemagenerator)
-and extending `get_schema`.
-
-
-Here's an example that adds OAS `info` and `servers` objects.
-
-```python
-from rest_framework_json_api.schemas.openapi import SchemaGenerator as JSONAPISchemaGenerator
-
-
-class MySchemaGenerator(JSONAPISchemaGenerator):
-    """
-    Describe my OAS schema info in detail (overriding what DRF put in) and list the servers where it can be found.
-    """
-    def get_schema(self, request, public):
-        schema = super().get_schema(request, public)
-        schema['info'] = {
-            'version': '1.0',
-            'title': 'my demo API',
-            'description': 'A demonstration of [OAS 3.0](https://www.openapis.org)',
-            'contact': {
-                'name': 'my name'
-            },
-            'license': {
-                'name': 'BSD 2 clause',
-                'url': 'https://github.com/django-json-api/django-rest-framework-json-api/blob/master/LICENSE',
-            }
-        }
-        schema['servers'] = [
-            {'url': 'https://localhost/v1', 'description': 'local docker'},
-            {'url': 'http://localhost:8000/v1', 'description': 'local dev'},
-            {'url': 'https://api.example.com/v1', 'description': 'demo server'},
-            {'url': '{serverURL}', 'description': 'provide your server URL',
-             'variables': {'serverURL': {'default': 'http://localhost:8000/v1'}}}
-        ]
-        return schema
-```
-
-### Generate a Static Schema on Command Line
-
-See [DRF documentation for generateschema](https://www.django-rest-framework.org/api-guide/schemas/#generating-a-static-schema-with-the-generateschema-management-command)
-To generate an OAS schema document, use something like:
-
-```text
-$ django-admin generateschema --settings=example.settings \
-                              --generator_class myapp.views.MySchemaGenerator >myschema.yaml
-```
-
-You can then use any number of OAS tools such as
-[swagger-ui-watcher](https://www.npmjs.com/package/swagger-ui-watcher)
-to render the schema:
-```text
-$ swagger-ui-watcher myschema.yaml
-```
-
-Note: Swagger-ui-watcher will complain that "DELETE operations cannot have a requestBody"
-but it will still work. This [error](https://github.com/OAI/OpenAPI-Specification/pull/2117)
-in the OAS specification will be fixed when [OAS 3.1.0](https://www.openapis.org/blog/2020/06/18/openapi-3-1-0-rc0-its-here)
-is published.
-
-([swagger-ui](https://www.npmjs.com/package/swagger-ui) will work silently.)
-
-### Generate a Dynamic Schema in a View
-
-See [DRF documentation for a Dynamic Schema](https://www.django-rest-framework.org/api-guide/schemas/#generating-a-dynamic-schema-with-schemaview).
-
-```python
-from rest_framework.schemas import get_schema_view
-
-urlpatterns = [
-    ...
-    path('openapi', get_schema_view(
-        title="Example API",
-        description="API for all things …",
-        version="1.0.0",
-        generator_class=MySchemaGenerator,
-    ), name='openapi-schema'),
-    path('swagger-ui/', TemplateView.as_view(
-        template_name='swagger-ui.html',
-        extra_context={'schema_url': 'openapi-schema'}
-    ), name='swagger-ui'),
-    ...
-]
-```
 
+[drf-spectacular-json-api]: https://github.com/jokiefer/drf-spectacular-json-api/
diff --git a/example/api/resources/identity.py b/example/api/resources/identity.py
index 6785e5d9..12705553 100644
--- a/example/api/resources/identity.py
+++ b/example/api/resources/identity.py
@@ -11,7 +11,7 @@
 
 
 class Identity(viewsets.ModelViewSet):
-    queryset = auth_models.User.objects.all().order_by('pk')
+    queryset = auth_models.User.objects.all().order_by("pk")
     serializer_class = IdentitySerializer
 
     # demonstrate sideloading data for use at app boot time
@@ -20,22 +20,24 @@ def posts(self, request):
         self.resource_name = False
 
         identities = self.queryset
-        posts = [{'id': 1, 'title': 'Test Blog Post'}]
+        posts = [{"id": 1, "title": "Test Blog Post"}]
 
         data = {
-            encoding.force_str('identities'): IdentitySerializer(identities, many=True).data,
-            encoding.force_str('posts'): PostSerializer(posts, many=True).data,
+            encoding.force_str("identities"): IdentitySerializer(
+                identities, many=True
+            ).data,
+            encoding.force_str("posts"): PostSerializer(posts, many=True).data,
         }
-        return Response(utils.format_field_names(data, format_type='camelize'))
+        return Response(utils.format_field_names(data, format_type="camelize"))
 
     @action(detail=True)
     def manual_resource_name(self, request, *args, **kwargs):
-        self.resource_name = 'data'
-        return super(Identity, self).retrieve(request, args, kwargs)
+        self.resource_name = "data"
+        return super().retrieve(request, args, kwargs)
 
     @action(detail=True)
     def validation(self, request, *args, **kwargs):
-        raise serializers.ValidationError('Oh nohs!')
+        raise serializers.ValidationError("Oh nohs!")
 
 
 class GenericIdentity(generics.GenericAPIView):
@@ -44,10 +46,11 @@ class GenericIdentity(generics.GenericAPIView):
 
     GET /identities/generic
     """
+
     serializer_class = IdentitySerializer
-    allowed_methods = ['GET']
-    renderer_classes = (renderers.JSONRenderer, )
-    parser_classes = (parsers.JSONParser, )
+    allowed_methods = ["GET"]
+    renderer_classes = (renderers.JSONRenderer,)
+    parser_classes = (parsers.JSONParser,)
 
     def get_queryset(self):
         return auth_models.User.objects.all()
diff --git a/example/api/serializers/identity.py b/example/api/serializers/identity.py
index 2538c6df..5e2a42e3 100644
--- a/example/api/serializers/identity.py
+++ b/example/api/serializers/identity.py
@@ -9,22 +9,32 @@ class IdentitySerializer(serializers.ModelSerializer):
 
     def validate_first_name(self, data):
         if len(data) > 10:
-            raise serializers.ValidationError(
-                'There\'s a problem with first name')
+            raise serializers.ValidationError("There's a problem with first name")
         return data
 
     def validate_last_name(self, data):
         if len(data) > 10:
             raise serializers.ValidationError(
                 {
-                    'id': 'armageddon101',
-                    'detail': 'Hey! You need a last name!',
-                    'meta': 'something',
+                    "id": "armageddon101",
+                    "detail": "Hey! You need a last name!",
+                    "meta": "something",
                 }
             )
         return data
 
+    def validate(self, data):
+        if data["first_name"] == data["last_name"]:
+            raise serializers.ValidationError(
+                "First name cannot be the same as last name!"
+            )
+        return data
+
     class Meta:
         model = auth_models.User
         fields = (
-            'id', 'first_name', 'last_name', 'email', )
+            "id",
+            "first_name",
+            "last_name",
+            "email",
+        )
diff --git a/example/api/serializers/post.py b/example/api/serializers/post.py
index bf0cf463..dbd78cfc 100644
--- a/example/api/serializers/post.py
+++ b/example/api/serializers/post.py
@@ -5,4 +5,5 @@ class PostSerializer(serializers.Serializer):
     """
     Blog post serializer
     """
+
     title = serializers.CharField(max_length=50)
diff --git a/example/factories.py b/example/factories.py
index 0639a0dd..87698fa2 100644
--- a/example/factories.py
+++ b/example/factories.py
@@ -1,5 +1,3 @@
-# -*- encoding: utf-8 -*-
-
 import factory
 from faker import Factory as FakerFactory
 
@@ -14,8 +12,9 @@
     Company,
     Entry,
     ProjectType,
+    Questionnaire,
     ResearchProject,
-    TaggedItem
+    TaggedItem,
 )
 
 faker = FakerFactory.create()
@@ -38,23 +37,27 @@ class Meta:
 
 class AuthorFactory(factory.django.DjangoModelFactory):
     class Meta:
+        skip_postgeneration_save = True
         model = Author
 
     name = factory.LazyAttribute(lambda x: faker.name())
     email = factory.LazyAttribute(lambda x: faker.email())
 
-    bio = factory.RelatedFactory('example.factories.AuthorBioFactory', 'author')
-    type = factory.SubFactory(AuthorTypeFactory)
+    bio = factory.RelatedFactory("example.factories.AuthorBioFactory", "author")
+    author_type = factory.SubFactory(AuthorTypeFactory)
 
 
 class AuthorBioFactory(factory.django.DjangoModelFactory):
     class Meta:
         model = AuthorBio
+        skip_postgeneration_save = True
 
     author = factory.SubFactory(AuthorFactory)
     body = factory.LazyAttribute(lambda x: faker.text())
 
-    metadata = factory.RelatedFactory('example.factories.AuthorBioMetadataFactory', 'bio')
+    metadata = factory.RelatedFactory(
+        "example.factories.AuthorBioMetadataFactory", "bio"
+    )
 
 
 class AuthorBioMetadataFactory(factory.django.DjangoModelFactory):
@@ -68,6 +71,7 @@ class Meta:
 class EntryFactory(factory.django.DjangoModelFactory):
     class Meta:
         model = Entry
+        skip_postgeneration_save = True
 
     headline = factory.LazyAttribute(lambda x: faker.sentence(nb_words=4))
     body_text = factory.LazyAttribute(lambda x: faker.text())
@@ -129,6 +133,7 @@ class Meta:
 class CompanyFactory(factory.django.DjangoModelFactory):
     class Meta:
         model = Company
+        skip_postgeneration_save = True
 
     name = factory.LazyAttribute(lambda x: faker.company())
     current_project = factory.SubFactory(ArtProjectFactory)
@@ -140,3 +145,24 @@ def future_projects(self, create, extracted, **kwargs):
         if extracted:
             for project in extracted:
                 self.future_projects.add(project)
+
+
+class QuestionnaireFactory(factory.django.DjangoModelFactory):
+    class Meta:
+        model = Questionnaire
+
+    name = factory.LazyAttribute(lambda x: faker.text())
+    questions = [
+        {
+            "text": "What is your name?",
+            "required": True,
+        },
+        {
+            "text": "What is your quest?",
+            "required": False,
+        },
+        {
+            "text": "What is the air-speed velocity of an unladen swallow?",
+        },
+    ]
+    metadata = {"author": "Bridgekeeper"}
diff --git a/example/fixtures/blogentry.json b/example/fixtures/blogentry.json
index 464573e4..ceb4a9fe 100644
--- a/example/fixtures/blogentry.json
+++ b/example/fixtures/blogentry.json
@@ -77,7 +77,7 @@
     "modified_at": "2016-05-02T10:09:48.277",
     "name": "Alice",
     "email": "alice@example.com",
-    "type": null
+    "author_type": null
   }
 },
 {
@@ -88,7 +88,7 @@
     "modified_at": "2016-05-02T10:09:57.133",
     "name": "Bob",
     "email": "bob@example.com",
-    "type": null
+    "author_type": null
   }
 },
 {
diff --git a/example/fixtures/drf_example.json b/example/fixtures/drf_example.json
index 498c0d1c..944f502c 100644
--- a/example/fixtures/drf_example.json
+++ b/example/fixtures/drf_example.json
@@ -26,8 +26,9 @@
     "created_at": "2016-05-02T10:09:48.277",
     "modified_at": "2016-05-02T10:09:48.277",
     "name": "Alice",
+    "full_name": "Alice Test",
     "email": "alice@example.com",
-    "type": null
+    "author_type": null
   }
 },
 {
@@ -37,8 +38,9 @@
     "created_at": "2016-05-02T10:09:57.133",
     "modified_at": "2016-05-02T10:09:57.133",
     "name": "Bob",
+    "full_name": "Bob Test",
     "email": "bob@example.com",
-    "type": null
+    "author_type": null
   }
 },
 {
diff --git a/example/migrations/0001_initial.py b/example/migrations/0001_initial.py
index 38cc0be9..18805099 100644
--- a/example/migrations/0001_initial.py
+++ b/example/migrations/0001_initial.py
@@ -1,94 +1,150 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.9.5 on 2016-05-02 08:26
-from __future__ import unicode_literals
-
-from django.db import migrations, models
 import django.db.models.deletion
+from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     initial = True
 
-    dependencies = [
-    ]
+    dependencies = []
 
     operations = [
         migrations.CreateModel(
-            name='Author',
+            name="Author",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('created_at', models.DateTimeField(auto_now_add=True)),
-                ('modified_at', models.DateTimeField(auto_now=True)),
-                ('name', models.CharField(max_length=50)),
-                ('email', models.EmailField(max_length=254)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("created_at", models.DateTimeField(auto_now_add=True)),
+                ("modified_at", models.DateTimeField(auto_now=True)),
+                ("name", models.CharField(max_length=50)),
+                ("email", models.EmailField(max_length=254)),
             ],
             options={
-                'abstract': False,
+                "abstract": False,
             },
         ),
         migrations.CreateModel(
-            name='AuthorBio',
+            name="AuthorBio",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('created_at', models.DateTimeField(auto_now_add=True)),
-                ('modified_at', models.DateTimeField(auto_now=True)),
-                ('body', models.TextField()),
-                ('author', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='bio', to='example.Author')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("created_at", models.DateTimeField(auto_now_add=True)),
+                ("modified_at", models.DateTimeField(auto_now=True)),
+                ("body", models.TextField()),
+                (
+                    "author",
+                    models.OneToOneField(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="bio",
+                        to="example.Author",
+                    ),
+                ),
             ],
             options={
-                'abstract': False,
+                "abstract": False,
             },
         ),
         migrations.CreateModel(
-            name='Blog',
+            name="Blog",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('created_at', models.DateTimeField(auto_now_add=True)),
-                ('modified_at', models.DateTimeField(auto_now=True)),
-                ('name', models.CharField(max_length=100)),
-                ('tagline', models.TextField()),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("created_at", models.DateTimeField(auto_now_add=True)),
+                ("modified_at", models.DateTimeField(auto_now=True)),
+                ("name", models.CharField(max_length=100)),
+                ("tagline", models.TextField()),
             ],
             options={
-                'abstract': False,
+                "abstract": False,
             },
         ),
         migrations.CreateModel(
-            name='Comment',
+            name="Comment",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('created_at', models.DateTimeField(auto_now_add=True)),
-                ('modified_at', models.DateTimeField(auto_now=True)),
-                ('body', models.TextField()),
-                ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='example.Author')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("created_at", models.DateTimeField(auto_now_add=True)),
+                ("modified_at", models.DateTimeField(auto_now=True)),
+                ("body", models.TextField()),
+                (
+                    "author",
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="example.Author",
+                    ),
+                ),
             ],
             options={
-                'abstract': False,
+                "abstract": False,
             },
         ),
         migrations.CreateModel(
-            name='Entry',
+            name="Entry",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('created_at', models.DateTimeField(auto_now_add=True)),
-                ('modified_at', models.DateTimeField(auto_now=True)),
-                ('headline', models.CharField(max_length=255)),
-                ('body_text', models.TextField(null=True)),
-                ('pub_date', models.DateField(null=True)),
-                ('mod_date', models.DateField(null=True)),
-                ('n_comments', models.IntegerField(default=0)),
-                ('n_pingbacks', models.IntegerField(default=0)),
-                ('rating', models.IntegerField(default=0)),
-                ('authors', models.ManyToManyField(to='example.Author')),
-                ('blog', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='example.Blog')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("created_at", models.DateTimeField(auto_now_add=True)),
+                ("modified_at", models.DateTimeField(auto_now=True)),
+                ("headline", models.CharField(max_length=255)),
+                ("body_text", models.TextField(null=True)),
+                ("pub_date", models.DateField(null=True)),
+                ("mod_date", models.DateField(null=True)),
+                ("n_comments", models.IntegerField(default=0)),
+                ("n_pingbacks", models.IntegerField(default=0)),
+                ("rating", models.IntegerField(default=0)),
+                ("authors", models.ManyToManyField(to="example.Author")),
+                (
+                    "blog",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE, to="example.Blog"
+                    ),
+                ),
             ],
             options={
-                'abstract': False,
+                "abstract": False,
             },
         ),
         migrations.AddField(
-            model_name='comment',
-            name='entry',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='example.Entry'),
+            model_name="comment",
+            name="entry",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE, to="example.Entry"
+            ),
         ),
     ]
diff --git a/example/migrations/0002_taggeditem.py b/example/migrations/0002_taggeditem.py
index 46a79de9..62b1cf81 100644
--- a/example/migrations/0002_taggeditem.py
+++ b/example/migrations/0002_taggeditem.py
@@ -1,31 +1,40 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.5 on 2017-02-01 08:34
-from __future__ import unicode_literals
-
-from django.db import migrations, models
 import django.db.models.deletion
+from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('contenttypes', '0002_remove_content_type_name'),
-        ('example', '0001_initial'),
+        ("contenttypes", "0002_remove_content_type_name"),
+        ("example", "0001_initial"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='TaggedItem',
+            name="TaggedItem",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('created_at', models.DateTimeField(auto_now_add=True)),
-                ('modified_at', models.DateTimeField(auto_now=True)),
-                ('tag', models.SlugField()),
-                ('object_id', models.PositiveIntegerField()),
-                ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("created_at", models.DateTimeField(auto_now_add=True)),
+                ("modified_at", models.DateTimeField(auto_now=True)),
+                ("tag", models.SlugField()),
+                ("object_id", models.PositiveIntegerField()),
+                (
+                    "content_type",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="contenttypes.ContentType",
+                    ),
+                ),
             ],
             options={
-                'abstract': False,
+                "abstract": False,
             },
         ),
     ]
diff --git a/example/migrations/0003_polymorphics.py b/example/migrations/0003_polymorphics.py
index 9020176b..70c62ddd 100644
--- a/example/migrations/0003_polymorphics.py
+++ b/example/migrations/0003_polymorphics.py
@@ -1,76 +1,121 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.1 on 2017-05-17 14:49
-from __future__ import unicode_literals
-
-from django.db import migrations, models
 import django.db.models.deletion
+from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('contenttypes', '0002_remove_content_type_name'),
-        ('example', '0002_taggeditem'),
+        ("contenttypes", "0002_remove_content_type_name"),
+        ("example", "0002_taggeditem"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='Company',
+            name="Company",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=100)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=100)),
             ],
         ),
         migrations.CreateModel(
-            name='Project',
+            name="Project",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('topic', models.CharField(max_length=30)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("topic", models.CharField(max_length=30)),
             ],
             options={
-                'abstract': False,
+                "abstract": False,
             },
         ),
         migrations.AlterField(
-            model_name='comment',
-            name='entry',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='example.Entry'),
+            model_name="comment",
+            name="entry",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="comments",
+                to="example.Entry",
+            ),
         ),
         migrations.CreateModel(
-            name='ArtProject',
+            name="ArtProject",
             fields=[
-                ('project_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='example.Project')),
-                ('artist', models.CharField(max_length=30)),
+                (
+                    "project_ptr",
+                    models.OneToOneField(
+                        auto_created=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        parent_link=True,
+                        primary_key=True,
+                        serialize=False,
+                        to="example.Project",
+                    ),
+                ),
+                ("artist", models.CharField(max_length=30)),
             ],
             options={
-                'abstract': False,
+                "abstract": False,
             },
-            bases=('example.project',),
+            bases=("example.project",),
         ),
         migrations.CreateModel(
-            name='ResearchProject',
+            name="ResearchProject",
             fields=[
-                ('project_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='example.Project')),
-                ('supervisor', models.CharField(max_length=30)),
+                (
+                    "project_ptr",
+                    models.OneToOneField(
+                        auto_created=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        parent_link=True,
+                        primary_key=True,
+                        serialize=False,
+                        to="example.Project",
+                    ),
+                ),
+                ("supervisor", models.CharField(max_length=30)),
             ],
             options={
-                'abstract': False,
+                "abstract": False,
             },
-            bases=('example.project',),
+            bases=("example.project",),
         ),
         migrations.AddField(
-            model_name='project',
-            name='polymorphic_ctype',
-            field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_example.project_set+', to='contenttypes.ContentType'),
+            model_name="project",
+            name="polymorphic_ctype",
+            field=models.ForeignKey(
+                editable=False,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="polymorphic_example.project_set+",
+                to="contenttypes.ContentType",
+            ),
         ),
         migrations.AddField(
-            model_name='company',
-            name='current_project',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='companies', to='example.Project'),
+            model_name="company",
+            name="current_project",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="companies",
+                to="example.Project",
+            ),
         ),
         migrations.AddField(
-            model_name='company',
-            name='future_projects',
-            field=models.ManyToManyField(to='example.Project'),
+            model_name="company",
+            name="future_projects",
+            field=models.ManyToManyField(to="example.Project"),
         ),
     ]
diff --git a/example/migrations/0004_auto_20171011_0631.py b/example/migrations/0004_auto_20171011_0631.py
index 96df2aa7..51db8e3f 100644
--- a/example/migrations/0004_auto_20171011_0631.py
+++ b/example/migrations/0004_auto_20171011_0631.py
@@ -1,62 +1,69 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.6 on 2017-10-11 06:31
-from __future__ import unicode_literals
-
-from django.db import migrations, models
 import django.db.models.deletion
+from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('example', '0003_polymorphics'),
+        ("example", "0003_polymorphics"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='AuthorType',
+            name="AuthorType",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('created_at', models.DateTimeField(auto_now_add=True)),
-                ('modified_at', models.DateTimeField(auto_now=True)),
-                ('name', models.CharField(max_length=50)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("created_at", models.DateTimeField(auto_now_add=True)),
+                ("modified_at", models.DateTimeField(auto_now=True)),
+                ("name", models.CharField(max_length=50)),
             ],
             options={
-                'ordering': ('id',),
+                "ordering": ("id",),
             },
         ),
         migrations.AlterModelOptions(
-            name='author',
-            options={'ordering': ('id',)},
+            name="author",
+            options={"ordering": ("id",)},
         ),
         migrations.AlterModelOptions(
-            name='authorbio',
-            options={'ordering': ('id',)},
+            name="authorbio",
+            options={"ordering": ("id",)},
         ),
         migrations.AlterModelOptions(
-            name='blog',
-            options={'ordering': ('id',)},
+            name="blog",
+            options={"ordering": ("id",)},
         ),
         migrations.AlterModelOptions(
-            name='comment',
-            options={'ordering': ('id',)},
+            name="comment",
+            options={"ordering": ("id",)},
         ),
         migrations.AlterModelOptions(
-            name='entry',
-            options={'ordering': ('id',)},
+            name="entry",
+            options={"ordering": ("id",)},
         ),
         migrations.AlterModelOptions(
-            name='taggeditem',
-            options={'ordering': ('id',)},
+            name="taggeditem",
+            options={"ordering": ("id",)},
         ),
         migrations.AlterField(
-            model_name='entry',
-            name='authors',
-            field=models.ManyToManyField(related_name='entries', to='example.Author'),
+            model_name="entry",
+            name="authors",
+            field=models.ManyToManyField(related_name="entries", to="example.Author"),
         ),
         migrations.AddField(
-            model_name='author',
-            name='type',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='example.AuthorType'),
+            model_name="author",
+            name="type",
+            field=models.ForeignKey(
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                to="example.AuthorType",
+            ),
         ),
     ]
diff --git a/example/migrations/0005_auto_20180922_1508.py b/example/migrations/0005_auto_20180922_1508.py
index 99d397f6..52c3a0ac 100644
--- a/example/migrations/0005_auto_20180922_1508.py
+++ b/example/migrations/0005_auto_20180922_1508.py
@@ -1,43 +1,54 @@
 # Generated by Django 2.1.1 on 2018-09-22 15:08
 
-from django.db import migrations, models
 import django.db.models.deletion
+from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('example', '0004_auto_20171011_0631'),
+        ("example", "0004_auto_20171011_0631"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='ProjectType',
+            name="ProjectType",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('created_at', models.DateTimeField(auto_now_add=True)),
-                ('modified_at', models.DateTimeField(auto_now=True)),
-                ('name', models.CharField(max_length=50)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("created_at", models.DateTimeField(auto_now_add=True)),
+                ("modified_at", models.DateTimeField(auto_now=True)),
+                ("name", models.CharField(max_length=50)),
             ],
             options={
-                'ordering': ('id',),
+                "ordering": ("id",),
             },
         ),
         migrations.AlterModelOptions(
-            name='artproject',
-            options={'base_manager_name': 'objects'},
+            name="artproject",
+            options={"base_manager_name": "objects"},
         ),
         migrations.AlterModelOptions(
-            name='project',
-            options={'base_manager_name': 'objects'},
+            name="project",
+            options={"base_manager_name": "objects"},
         ),
         migrations.AlterModelOptions(
-            name='researchproject',
-            options={'base_manager_name': 'objects'},
+            name="researchproject",
+            options={"base_manager_name": "objects"},
         ),
         migrations.AddField(
-            model_name='project',
-            name='project_type',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='example.ProjectType'),
+            model_name="project",
+            name="project_type",
+            field=models.ForeignKey(
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                to="example.ProjectType",
+            ),
         ),
     ]
diff --git a/example/migrations/0006_auto_20181228_0752.py b/example/migrations/0006_auto_20181228_0752.py
index 2cfb0c29..3ef2678e 100644
--- a/example/migrations/0006_auto_20181228_0752.py
+++ b/example/migrations/0006_auto_20181228_0752.py
@@ -1,32 +1,52 @@
 # Generated by Django 2.1.4 on 2018-12-28 07:52
 
-from django.db import migrations, models
 import django.db.models.deletion
+from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('example', '0005_auto_20180922_1508'),
+        ("example", "0005_auto_20180922_1508"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='AuthorBioMetadata',
+            name="AuthorBioMetadata",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('created_at', models.DateTimeField(auto_now_add=True)),
-                ('modified_at', models.DateTimeField(auto_now=True)),
-                ('body', models.TextField()),
-                ('bio', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='metadata', to='example.AuthorBio')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("created_at", models.DateTimeField(auto_now_add=True)),
+                ("modified_at", models.DateTimeField(auto_now=True)),
+                ("body", models.TextField()),
+                (
+                    "bio",
+                    models.OneToOneField(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="metadata",
+                        to="example.AuthorBio",
+                    ),
+                ),
             ],
             options={
-                'ordering': ('id',),
+                "ordering": ("id",),
             },
         ),
         migrations.AlterField(
-            model_name='comment',
-            name='author',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='example.Author'),
+            model_name="comment",
+            name="author",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="comments",
+                to="example.Author",
+            ),
         ),
     ]
diff --git a/example/migrations/0007_artproject_description.py b/example/migrations/0007_artproject_description.py
index 8dec0124..4ed2d7a9 100644
--- a/example/migrations/0007_artproject_description.py
+++ b/example/migrations/0007_artproject_description.py
@@ -4,15 +4,14 @@
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('example', '0006_auto_20181228_0752'),
+        ("example", "0006_auto_20181228_0752"),
     ]
 
     operations = [
         migrations.AddField(
-            model_name='artproject',
-            name='description',
+            model_name="artproject",
+            name="description",
             field=models.CharField(max_length=100, null=True),
         ),
     ]
diff --git a/example/migrations/0008_labresults.py b/example/migrations/0008_labresults.py
index 89323d77..290c2cb8 100644
--- a/example/migrations/0008_labresults.py
+++ b/example/migrations/0008_labresults.py
@@ -1,23 +1,37 @@
 # Generated by Django 3.0.3 on 2020-02-06 10:24
 
-from django.db import migrations, models
 import django.db.models.deletion
+from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('example', '0007_artproject_description'),
+        ("example", "0007_artproject_description"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='LabResults',
+            name="LabResults",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('date', models.DateField()),
-                ('measurements', models.TextField()),
-                ('research_project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lab_results', to='example.ResearchProject')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("date", models.DateField()),
+                ("measurements", models.TextField()),
+                (
+                    "research_project",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="lab_results",
+                        to="example.ResearchProject",
+                    ),
+                ),
             ],
         ),
     ]
diff --git a/example/migrations/0009_labresults_author.py b/example/migrations/0009_labresults_author.py
new file mode 100644
index 00000000..6c805ab0
--- /dev/null
+++ b/example/migrations/0009_labresults_author.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.3 on 2021-05-26 03:32
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("example", "0008_labresults"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="labresults",
+            name="author",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="lab_results",
+                to="example.author",
+            ),
+        ),
+    ]
diff --git a/example/migrations/0010_auto_20210714_0809.py b/example/migrations/0010_auto_20210714_0809.py
new file mode 100644
index 00000000..cd96cccc
--- /dev/null
+++ b/example/migrations/0010_auto_20210714_0809.py
@@ -0,0 +1,16 @@
+# Generated by Django 3.0.9 on 2021-07-14 08:09
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("example", "0009_labresults_author"),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name="labresults",
+            options={"ordering": ("id",)},
+        ),
+    ]
diff --git a/example/migrations/0011_rename_type_author_author_type_and_more.py b/example/migrations/0011_rename_type_author_author_type_and_more.py
new file mode 100644
index 00000000..cc2f5546
--- /dev/null
+++ b/example/migrations/0011_rename_type_author_author_type_and_more.py
@@ -0,0 +1,30 @@
+# Generated by Django 4.0 on 2021-12-29 13:07
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("contenttypes", "0002_remove_content_type_name"),
+        ("example", "0010_auto_20210714_0809"),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name="author",
+            old_name="type",
+            new_name="author_type",
+        ),
+        migrations.AlterField(
+            model_name="project",
+            name="polymorphic_ctype",
+            field=models.ForeignKey(
+                editable=False,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="polymorphic_%(app_label)s.%(class)s_set+",
+                to="contenttypes.contenttype",
+            ),
+        ),
+    ]
diff --git a/example/migrations/0012_author_full_name.py b/example/migrations/0012_author_full_name.py
new file mode 100644
index 00000000..0486d041
--- /dev/null
+++ b/example/migrations/0012_author_full_name.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1 on 2022-09-06 15:42
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("example", "0011_rename_type_author_author_type_and_more"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="author",
+            name="full_name",
+            field=models.CharField(default="", max_length=50),
+            preserve_default=False,
+        ),
+    ]
diff --git a/example/migrations/0013_questionnaire.py b/example/migrations/0013_questionnaire.py
new file mode 100644
index 00000000..0a3b7cd2
--- /dev/null
+++ b/example/migrations/0013_questionnaire.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.5 on 2023-09-07 02:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("example", "0012_author_full_name"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="Questionnaire",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=100)),
+                ("questions", models.JSONField()),
+            ],
+        ),
+    ]
diff --git a/example/migrations/0014_questionnaire_metadata.py b/example/migrations/0014_questionnaire_metadata.py
new file mode 100644
index 00000000..e320ebb8
--- /dev/null
+++ b/example/migrations/0014_questionnaire_metadata.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.5 on 2023-09-12 07:12
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("example", "0013_questionnaire"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="questionnaire",
+            name="metadata",
+            field=models.JSONField(default={}),
+            preserve_default=False,
+        ),
+    ]
diff --git a/example/models.py b/example/models.py
index 4df4dc27..35a15a8e 100644
--- a/example/models.py
+++ b/example/models.py
@@ -1,6 +1,3 @@
-# -*- encoding: utf-8 -*-
-from __future__ import unicode_literals
-
 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
@@ -11,6 +8,7 @@ class BaseModel(models.Model):
     """
     I hear RoR has this by default, who doesn't need these two fields!
     """
+
     created_at = models.DateTimeField(auto_now_add=True)
     modified_at = models.DateTimeField(auto_now=True)
 
@@ -22,13 +20,13 @@ class TaggedItem(BaseModel):
     tag = models.SlugField()
     content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
     object_id = models.PositiveIntegerField()
-    content_object = GenericForeignKey('content_type', 'object_id')
+    content_object = GenericForeignKey("content_type", "object_id")
 
     def __str__(self):
         return self.tag
 
     class Meta:
-        ordering = ('id',)
+        ordering = ("id",)
 
 
 class Blog(BaseModel):
@@ -40,7 +38,7 @@ def __str__(self):
         return self.name
 
     class Meta:
-        ordering = ('id',)
+        ordering = ("id",)
 
 
 class AuthorType(BaseModel):
@@ -50,44 +48,48 @@ def __str__(self):
         return self.name
 
     class Meta:
-        ordering = ('id',)
+        ordering = ("id",)
 
 
 class Author(BaseModel):
     name = models.CharField(max_length=50)
+    full_name = models.CharField(max_length=50)
     email = models.EmailField()
-    type = models.ForeignKey(AuthorType, null=True, on_delete=models.CASCADE)
+    author_type = models.ForeignKey(AuthorType, null=True, on_delete=models.CASCADE)
 
     def __str__(self):
         return self.name
 
     class Meta:
-        ordering = ('id',)
+        ordering = ("id",)
 
 
 class AuthorBio(BaseModel):
-    author = models.OneToOneField(Author, related_name='bio', on_delete=models.CASCADE)
+    author = models.OneToOneField(Author, related_name="bio", on_delete=models.CASCADE)
     body = models.TextField()
 
     def __str__(self):
         return self.author.name
 
     class Meta:
-        ordering = ('id',)
+        ordering = ("id",)
 
 
 class AuthorBioMetadata(BaseModel):
     """
     Just a class to have a relation with author bio
     """
-    bio = models.OneToOneField(AuthorBio, related_name='metadata', on_delete=models.CASCADE)
+
+    bio = models.OneToOneField(
+        AuthorBio, related_name="metadata", on_delete=models.CASCADE
+    )
     body = models.TextField()
 
     def __str__(self):
         return self.bio.author.name
 
     class Meta:
-        ordering = ('id',)
+        ordering = ("id",)
 
 
 class Entry(BaseModel):
@@ -96,7 +98,7 @@ class Entry(BaseModel):
     body_text = models.TextField(null=True)
     pub_date = models.DateField(null=True)
     mod_date = models.DateField(null=True)
-    authors = models.ManyToManyField(Author, related_name='entries')
+    authors = models.ManyToManyField(Author, related_name="entries")
     n_comments = models.IntegerField(default=0)
     n_pingbacks = models.IntegerField(default=0)
     rating = models.IntegerField(default=0)
@@ -106,25 +108,25 @@ def __str__(self):
         return self.headline
 
     class Meta:
-        ordering = ('id',)
+        ordering = ("id",)
 
 
 class Comment(BaseModel):
-    entry = models.ForeignKey(Entry, related_name='comments', on_delete=models.CASCADE)
+    entry = models.ForeignKey(Entry, related_name="comments", on_delete=models.CASCADE)
     body = models.TextField()
     author = models.ForeignKey(
         Author,
         null=True,
         blank=True,
         on_delete=models.CASCADE,
-        related_name='comments',
+        related_name="comments",
     )
 
     def __str__(self):
         return self.body
 
     class Meta:
-        ordering = ('id',)
+        ordering = ("id",)
 
 
 class ProjectType(BaseModel):
@@ -134,7 +136,7 @@ def __str__(self):
         return self.name
 
     class Meta:
-        ordering = ('id',)
+        ordering = ("id",)
 
 
 class Project(PolymorphicModel):
@@ -153,16 +155,34 @@ class ResearchProject(Project):
 
 class LabResults(models.Model):
     research_project = models.ForeignKey(
-        ResearchProject, related_name='lab_results', on_delete=models.CASCADE)
+        ResearchProject, related_name="lab_results", on_delete=models.CASCADE
+    )
     date = models.DateField()
     measurements = models.TextField()
+    author = models.ForeignKey(
+        Author,
+        null=True,
+        blank=True,
+        on_delete=models.CASCADE,
+        related_name="lab_results",
+    )
+
+    class Meta:
+        ordering = ("id",)
 
 
 class Company(models.Model):
     name = models.CharField(max_length=100)
     current_project = models.ForeignKey(
-        Project, related_name='companies', on_delete=models.CASCADE)
+        Project, related_name="companies", on_delete=models.CASCADE
+    )
     future_projects = models.ManyToManyField(Project)
 
     def __str__(self):
         return self.name
+
+
+class Questionnaire(models.Model):
+    name = models.CharField(max_length=100)
+    questions = models.JSONField()
+    metadata = models.JSONField()
diff --git a/example/requirements.txt b/example/requirements.txt
deleted file mode 100644
index 2f4213ee..00000000
--- a/example/requirements.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-# Requirements specifically for the example app
-Django>=1.11
-django-debug-toolbar
-django-polymorphic>=2.0
-djangorestframework
-inflection
-pluggy
-py
-pyparsing
-pytz
-sqlparse
-django-filter>=2.0
diff --git a/example/serializers.py b/example/serializers.py
index 9dc84a4a..94fe8556 100644
--- a/example/serializers.py
+++ b/example/serializers.py
@@ -18,24 +18,26 @@
     LabResults,
     Project,
     ProjectType,
+    Questionnaire,
     ResearchProject,
-    TaggedItem
+    TaggedItem,
 )
 
 
 class TaggedItemSerializer(serializers.ModelSerializer):
     class Meta:
         model = TaggedItem
-        fields = ('tag',)
+        fields = ("tag",)
 
 
 class TaggedItemDRFSerializer(drf_serilazers.ModelSerializer):
     """
     DRF default serializer to test default DRF functionalities
     """
+
     class Meta:
         model = TaggedItem
-        fields = ('tag',)
+        fields = ("tag",)
 
 
 class BlogSerializer(serializers.ModelSerializer):
@@ -43,28 +45,27 @@ class BlogSerializer(serializers.ModelSerializer):
     tags = relations.ResourceRelatedField(many=True, read_only=True)
 
     included_serializers = {
-        'tags': 'example.serializers.TaggedItemSerializer',
+        "tags": "example.serializers.TaggedItemSerializer",
     }
 
     def get_copyright(self, resource):
         return datetime.now().year
 
     def get_root_meta(self, resource, many):
-        return {
-            'api_docs': '/docs/api/blogs'
-        }
+        return {"api_docs": "/docs/api/blogs"}
 
     class Meta:
         model = Blog
-        fields = ('name', 'url', 'tags')
-        read_only_fields = ('tags',)
-        meta_fields = ('copyright',)
+        fields = ("name", "url", "tags")
+        read_only_fields = ("tags",)
+        meta_fields = ("copyright",)
 
 
 class BlogDRFSerializer(drf_serilazers.ModelSerializer):
     """
     DRF default serializer to test default DRF functionalities
     """
+
     copyright = serializers.SerializerMethodField()
     tags = TaggedItemDRFSerializer(many=True, read_only=True)
 
@@ -72,68 +73,65 @@ def get_copyright(self, resource):
         return datetime.now().year
 
     def get_root_meta(self, resource, many):
-        return {
-            'api_docs': '/docs/api/blogs'
-        }
+        return {"api_docs": "/docs/api/blogs"}
 
     class Meta:
         model = Blog
-        fields = ('name', 'url', 'tags', 'copyright')
-        read_only_fields = ('tags',)
-        meta_fields = ('copyright',)
+        fields = ("name", "url", "tags", "copyright")
+        read_only_fields = ("tags",)
+        meta_fields = ("copyright",)
 
 
 class EntrySerializer(serializers.ModelSerializer):
     def __init__(self, *args, **kwargs):
-        super(EntrySerializer, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         # to make testing more concise we'll only output the
         # `featured` field when it's requested via `include`
-        request = kwargs.get('context', {}).get('request')
-        if request and 'featured' not in request.query_params.get('include', []):
-            self.fields.pop('featured', None)
+        request = kwargs.get("context", {}).get("request")
+        if request and "featured" not in request.query_params.get("include", []):
+            self.fields.pop("featured", None)
 
     included_serializers = {
-        'authors': 'example.serializers.AuthorSerializer',
-        'comments': 'example.serializers.CommentSerializer',
-        'featured': 'example.serializers.EntrySerializer',
-        'suggested': 'example.serializers.EntrySerializer',
-        'tags': 'example.serializers.TaggedItemSerializer',
+        "authors": "example.serializers.AuthorSerializer",
+        "comments": "example.serializers.CommentSerializer",
+        "featured": "example.serializers.EntrySerializer",
+        "suggested": "example.serializers.EntrySerializer",
+        "tags": "example.serializers.TaggedItemSerializer",
     }
 
     body_format = serializers.SerializerMethodField()
     # single related from model
     blog_hyperlinked = relations.HyperlinkedRelatedField(
-        related_link_view_name='entry-blog',
-        related_link_url_kwarg='entry_pk',
-        self_link_view_name='entry-relationships',
+        related_link_view_name="entry-blog",
+        related_link_url_kwarg="entry_pk",
+        self_link_view_name="entry-relationships",
         read_only=True,
-        source='blog'
+        source="blog",
     )
     # many related from model
-    comments = relations.ResourceRelatedField(
-        many=True, read_only=True)
+    comments = relations.ResourceRelatedField(many=True, read_only=True)
     # many related hyperlinked from model
     comments_hyperlinked = relations.HyperlinkedRelatedField(
-        related_link_view_name='entry-comments',
-        related_link_url_kwarg='entry_pk',
-        self_link_view_name='entry-relationships',
+        related_link_view_name="entry-comments",
+        related_link_url_kwarg="entry_pk",
+        self_link_view_name="entry-relationships",
         many=True,
         read_only=True,
-        source='comments'
+        source="comments",
     )
     # many related from serializer
     suggested = relations.SerializerMethodResourceRelatedField(
-        related_link_view_name='entry-suggested',
-        related_link_url_kwarg='entry_pk',
-        self_link_view_name='entry-relationships',
+        related_link_view_name="entry-suggested",
+        related_link_url_kwarg="entry_pk",
+        self_link_view_name="entry-relationships",
         model=Entry,
         many=True,
     )
     # many related hyperlinked from serializer
     suggested_hyperlinked = relations.SerializerMethodHyperlinkedRelatedField(
-        related_link_view_name='entry-suggested',
-        related_link_url_kwarg='entry_pk',
-        self_link_view_name='entry-relationships',
+        related_link_view_name="entry-suggested",
+        related_link_url_kwarg="entry_pk",
+        self_link_view_name="entry-relationships",
         model=Entry,
         many=True,
     )
@@ -141,11 +139,11 @@ def __init__(self, *args, **kwargs):
     featured = relations.SerializerMethodResourceRelatedField(model=Entry)
     # single related hyperlinked from serializer
     featured_hyperlinked = relations.SerializerMethodHyperlinkedRelatedField(
-        related_link_view_name='entry-featured',
-        related_link_url_kwarg='entry_pk',
-        self_link_view_name='entry-relationships',
+        related_link_view_name="entry-featured",
+        related_link_url_kwarg="entry_pk",
+        self_link_view_name="entry-relationships",
         model=Entry,
-        read_only=True
+        read_only=True,
     )
     tags = relations.ResourceRelatedField(many=True, read_only=True)
 
@@ -156,154 +154,203 @@ def get_featured(self, obj):
         return Entry.objects.exclude(pk=obj.pk).first()
 
     def get_body_format(self, obj):
-        return 'text'
+        return "text"
 
     class Meta:
         model = Entry
-        fields = ('blog', 'blog_hyperlinked', 'headline', 'body_text', 'pub_date', 'mod_date',
-                  'authors', 'comments', 'comments_hyperlinked', 'featured', 'suggested',
-                  'suggested_hyperlinked', 'tags', 'featured_hyperlinked')
-        read_only_fields = ('tags',)
-        meta_fields = ('body_format',)
+        fields = (
+            "blog",
+            "blog_hyperlinked",
+            "headline",
+            "body_text",
+            "pub_date",
+            "mod_date",
+            "authors",
+            "comments",
+            "comments_hyperlinked",
+            "featured",
+            "suggested",
+            "suggested_hyperlinked",
+            "tags",
+            "featured_hyperlinked",
+        )
+        read_only_fields = ("tags",)
+        meta_fields = ("body_format",)
 
     class JSONAPIMeta:
-        included_resources = ['comments']
+        included_resources = ["comments"]
 
 
 class EntryDRFSerializers(drf_serilazers.ModelSerializer):
-
     tags = TaggedItemDRFSerializer(many=True, read_only=True)
     url = drf_serilazers.HyperlinkedIdentityField(
-        view_name='drf-entry-blog-detail',
-        lookup_url_kwarg='entry_pk',
+        view_name="drf-entry-blog-detail",
+        lookup_url_kwarg="entry_pk",
         read_only=True,
     )
 
     class Meta:
         model = Entry
-        fields = ('tags', 'url',)
-        read_only_fields = ('tags',)
+        fields = (
+            "tags",
+            "url",
+        )
+        read_only_fields = ("tags",)
 
 
 class AuthorTypeSerializer(serializers.ModelSerializer):
     class Meta:
         model = AuthorType
-        fields = ('name', )
+        fields = ("name",)
 
 
 class AuthorBioSerializer(serializers.ModelSerializer):
     class Meta:
         model = AuthorBio
-        fields = ('author', 'body', 'metadata')
+        fields = ("author", "body", "metadata")
 
     included_serializers = {
-        'metadata': 'example.serializers.AuthorBioMetadataSerializer',
+        "metadata": "example.serializers.AuthorBioMetadataSerializer",
     }
 
 
 class AuthorBioMetadataSerializer(serializers.ModelSerializer):
     class Meta:
         model = AuthorBioMetadata
-        fields = ('body',)
+        fields = ("body",)
 
 
 class AuthorSerializer(serializers.ModelSerializer):
     bio = relations.ResourceRelatedField(
-        related_link_view_name='author-related',
-        self_link_view_name='author-relationships',
+        related_link_view_name="author-related",
+        self_link_view_name="author-relationships",
         queryset=AuthorBio.objects,
     )
     entries = relations.ResourceRelatedField(
-        related_link_view_name='author-related',
-        self_link_view_name='author-relationships',
+        related_link_view_name="author-related",
+        self_link_view_name="author-relationships",
         queryset=Entry.objects,
-        many=True
+        many=True,
     )
     first_entry = relations.SerializerMethodResourceRelatedField(
-        related_link_view_name='author-related',
-        self_link_view_name='author-relationships',
+        related_link_view_name="author-related",
+        self_link_view_name="author-relationships",
         model=Entry,
     )
     comments = relations.HyperlinkedRelatedField(
-        related_link_view_name='author-related',
-        self_link_view_name='author-relationships',
+        related_link_view_name="author-related",
+        self_link_view_name="author-relationships",
         queryset=Comment.objects,
-        many=True
-    )
-    secrets = serializers.HiddenField(
-        default='Shhhh!'
+        many=True,
     )
+    secrets = serializers.HiddenField(default="Shhhh!")
     defaults = serializers.CharField(
-        default='default',
+        default="default",
         max_length=20,
         min_length=3,
         write_only=True,
-        help_text='help for defaults',
+        help_text="help for defaults",
     )
+    initials = serializers.SerializerMethodField()
     included_serializers = {
-        'bio': AuthorBioSerializer,
-        'type': AuthorTypeSerializer
+        "bio": AuthorBioSerializer,
+        "author_type": AuthorTypeSerializer,
     }
     related_serializers = {
-        'bio': 'example.serializers.AuthorBioSerializer',
-        'type': 'example.serializers.AuthorTypeSerializer',
-        'comments': 'example.serializers.CommentSerializer',
-        'entries': 'example.serializers.EntrySerializer',
-        'first_entry': 'example.serializers.EntrySerializer'
+        "bio": "example.serializers.AuthorBioSerializer",
+        "author_type": "example.serializers.AuthorTypeSerializer",
+        "comments": "example.serializers.CommentSerializer",
+        "entries": "example.serializers.EntrySerializer",
+        "first_entry": "example.serializers.EntrySerializer",
     }
 
     class Meta:
         model = Author
-        fields = ('name', 'email', 'bio', 'entries', 'comments', 'first_entry', 'type',
-                  'secrets', 'defaults')
+        fields = (
+            "name",
+            "full_name",
+            "email",
+            "bio",
+            "entries",
+            "comments",
+            "first_entry",
+            "author_type",
+            "secrets",
+            "defaults",
+            "initials",
+        )
+        meta_fields = ("initials",)
 
     def get_first_entry(self, obj):
         return obj.entries.first()
 
+    def get_initials(self, obj):
+        return "".join([word[0] for word in obj.name.split(" ")])
+
+
+class AuthorListSerializer(AuthorSerializer):
+    pass
+
+
+class AuthorDetailSerializer(AuthorSerializer):
+    pass
+
 
 class WriterSerializer(serializers.ModelSerializer):
-    included_serializers = {
-        'bio': AuthorBioSerializer
-    }
+    included_serializers = {"bio": AuthorBioSerializer}
 
     class Meta:
         model = Author
-        fields = ('name', 'email', 'bio')
-        resource_name = 'writers'
+        fields = ("name", "email", "bio")
+        resource_name = "writers"
 
 
 class CommentSerializer(serializers.ModelSerializer):
     # testing remapping of related name
-    writer = relations.ResourceRelatedField(source='author', read_only=True)
+    writer = relations.ResourceRelatedField(source="author", read_only=True)
+    modified_days_ago = serializers.SerializerMethodField()
 
     included_serializers = {
-        'entry': EntrySerializer,
-        'author': AuthorSerializer,
-        'writer': WriterSerializer
+        "entry": EntrySerializer,
+        "author": AuthorSerializer,
+        "writer": WriterSerializer,
     }
 
     class Meta:
         model = Comment
-        exclude = ('created_at', 'modified_at',)
+        exclude = (
+            "created_at",
+            "modified_at",
+        )
         # fields = ('entry', 'body', 'author',)
+        meta_fields = ("modified_days_ago",)
+
+    class JSONAPIMeta:
+        included_resources = ("writer",)
+
+    def get_modified_days_ago(self, obj):
+        return (datetime.now() - obj.modified_at).days
 
 
 class ProjectTypeSerializer(serializers.ModelSerializer):
     class Meta:
         model = ProjectType
-        fields = ('name', 'url',)
+        fields = (
+            "name",
+            "url",
+        )
 
 
 class BaseProjectSerializer(serializers.ModelSerializer):
     included_serializers = {
-        'project_type': ProjectTypeSerializer,
+        "project_type": ProjectTypeSerializer,
     }
 
 
 class ArtProjectSerializer(BaseProjectSerializer):
     class Meta:
         model = ArtProject
-        exclude = ('polymorphic_ctype',)
+        exclude = ("polymorphic_ctype",)
 
 
 class ResearchProjectSerializer(BaseProjectSerializer):
@@ -312,37 +359,37 @@ class ResearchProjectSerializer(BaseProjectSerializer):
 
     class Meta:
         model = ResearchProject
-        exclude = ('polymorphic_ctype',)
+        exclude = ("polymorphic_ctype",)
 
 
 class LabResultsSerializer(serializers.ModelSerializer):
+    included_serializers = {"author": AuthorSerializer}
+
     class Meta:
         model = LabResults
-        fields = ('date', 'measurements')
+        fields = ("date", "measurements", "author")
 
 
 class ProjectSerializer(serializers.PolymorphicModelSerializer):
     included_serializers = {
-        'project_type': ProjectTypeSerializer,
+        "project_type": ProjectTypeSerializer,
     }
     polymorphic_serializers = [ArtProjectSerializer, ResearchProjectSerializer]
 
     class Meta:
         model = Project
-        exclude = ('polymorphic_ctype',)
+        exclude = ("polymorphic_ctype",)
 
 
 class CurrentProjectRelatedField(relations.PolymorphicResourceRelatedField):
     def get_attribute(self, instance):
-        obj = super(CurrentProjectRelatedField, self).get_attribute(instance)
+        obj = super().get_attribute(instance)
 
-        is_art = (
-            self.field_name == 'current_art_project' and
-            isinstance(obj, ArtProject)
+        is_art = self.field_name == "current_art_project" and isinstance(
+            obj, ArtProject
         )
-        is_res = (
-            self.field_name == 'current_research_project' and
-            isinstance(obj, ResearchProject)
+        is_res = self.field_name == "current_research_project" and isinstance(
+            obj, ResearchProject
         )
 
         if is_art or is_res:
@@ -353,21 +400,44 @@ def get_attribute(self, instance):
 
 class CompanySerializer(serializers.ModelSerializer):
     current_project = relations.PolymorphicResourceRelatedField(
-        ProjectSerializer, queryset=Project.objects.all())
+        ProjectSerializer, queryset=Project.objects.all()
+    )
     current_art_project = CurrentProjectRelatedField(
-        ProjectSerializer, source='current_project', read_only=True)
+        ProjectSerializer, source="current_project", read_only=True
+    )
     current_research_project = CurrentProjectRelatedField(
-        ProjectSerializer, source='current_project', read_only=True)
+        ProjectSerializer, source="current_project", read_only=True
+    )
     future_projects = relations.PolymorphicResourceRelatedField(
-        ProjectSerializer, queryset=Project.objects.all(), many=True)
+        ProjectSerializer, queryset=Project.objects.all(), many=True
+    )
 
     included_serializers = {
-        'current_project': ProjectSerializer,
-        'future_projects': ProjectSerializer,
-        'current_art_project': ProjectSerializer,
-        'current_research_project': ProjectSerializer
+        "current_project": ProjectSerializer,
+        "future_projects": ProjectSerializer,
+        "current_art_project": ProjectSerializer,
+        "current_research_project": ProjectSerializer,
     }
 
     class Meta:
         model = Company
-        fields = '__all__'
+        fields = "__all__"
+
+
+class QuestionSerializer(serializers.Serializer):
+    text = serializers.CharField()
+    required = serializers.BooleanField(default=False)
+
+
+class QuestionnaireMetadataSerializer(serializers.Serializer):
+    author = serializers.CharField()
+    producer = serializers.CharField(default=None)
+
+
+class QuestionnaireSerializer(serializers.ModelSerializer):
+    questions = serializers.ListField(child=QuestionSerializer())
+    metadata = QuestionnaireMetadataSerializer()
+
+    class Meta:
+        model = Questionnaire
+        fields = ("name", "questions", "metadata")
diff --git a/example/settings/dev.py b/example/settings/dev.py
index 961807e3..05cab4d1 100644
--- a/example/settings/dev.py
+++ b/example/settings/dev.py
@@ -4,100 +4,93 @@
 DEBUG = True
 
 MEDIA_ROOT = os.path.normcase(os.path.dirname(os.path.abspath(__file__)))
-MEDIA_URL = '/media/'
+MEDIA_URL = "/media/"
+USE_TZ = False
+DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
 
-DATABASE_ENGINE = 'sqlite3'
+DATABASE_ENGINE = "sqlite3"
 
 DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': 'drf_example',
+    "default": {
+        "ENGINE": "django.db.backends.sqlite3",
+        "NAME": "drf_example",
     }
 }
 
 INSTALLED_APPS = [
-    'django.contrib.contenttypes',
-    'django.contrib.staticfiles',
-    'django.contrib.sites',
-    'django.contrib.sessions',
-    'django.contrib.auth',
-    'rest_framework_json_api',
-    'rest_framework',
-    'polymorphic',
-    'example',
-    'debug_toolbar',
-    'django_filters',
+    "django.contrib.contenttypes",
+    "django.contrib.staticfiles",
+    "django.contrib.sites",
+    "django.contrib.sessions",
+    "django.contrib.auth",
+    "rest_framework_json_api",
+    "rest_framework",
+    "polymorphic",
+    "example",
+    "django_filters",
+    "tests",
 ]
 
 TEMPLATES = [
     {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "DIRS": [
             # insert your TEMPLATE_DIRS here
         ],
-        'APP_DIRS': True,
-        'OPTIONS': {
-            'context_processors': [
+        "APP_DIRS": True,
+        "OPTIONS": {
+            "context_processors": [
                 # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this
                 # list if you haven't customized them:
-                'django.contrib.auth.context_processors.auth',
-                'django.template.context_processors.debug',
-                'django.template.context_processors.i18n',
-                'django.template.context_processors.media',
-                'django.template.context_processors.static',
-                'django.template.context_processors.tz',
-                'django.contrib.messages.context_processors.messages',
+                "django.contrib.auth.context_processors.auth",
+                "django.template.context_processors.debug",
+                "django.template.context_processors.i18n",
+                "django.template.context_processors.media",
+                "django.template.context_processors.static",
+                "django.template.context_processors.tz",
+                "django.contrib.messages.context_processors.messages",
             ],
         },
     },
 ]
 
-STATIC_URL = '/static/'
+STATIC_URL = "/static/"
 
-ROOT_URLCONF = 'example.urls'
+ROOT_URLCONF = "example.urls"
 
-SECRET_KEY = 'abc123'
+SECRET_KEY = "abc123"
 
-PASSWORD_HASHERS = ('django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', )
+INTERNAL_IPS = ("127.0.0.1",)
 
-MIDDLEWARE = (
-    'debug_toolbar.middleware.DebugToolbarMiddleware',
-)
-
-INTERNAL_IPS = ('127.0.0.1', )
-
-JSON_API_FORMAT_FIELD_NAMES = 'camelize'
-JSON_API_FORMAT_TYPES = 'camelize'
+JSON_API_FORMAT_FIELD_NAMES = "camelize"
+JSON_API_FORMAT_TYPES = "camelize"
 REST_FRAMEWORK = {
-    'PAGE_SIZE': 5,
-    'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
-    'DEFAULT_PAGINATION_CLASS':
-        'rest_framework_json_api.pagination.JsonApiPageNumberPagination',
-    'DEFAULT_PARSER_CLASSES': (
-        'rest_framework_json_api.parsers.JSONParser',
-        'rest_framework.parsers.FormParser',
-        'rest_framework.parsers.MultiPartParser'
+    "PAGE_SIZE": 5,
+    "EXCEPTION_HANDLER": "rest_framework_json_api.exceptions.exception_handler",
+    "DEFAULT_PAGINATION_CLASS": "rest_framework_json_api.pagination.JsonApiPageNumberPagination",  # noqa: B950
+    "DEFAULT_PARSER_CLASSES": (
+        "rest_framework_json_api.parsers.JSONParser",
+        "rest_framework.parsers.FormParser",
+        "rest_framework.parsers.MultiPartParser",
     ),
-    'DEFAULT_RENDERER_CLASSES': (
-        'rest_framework_json_api.renderers.JSONRenderer',
-
+    "DEFAULT_RENDERER_CLASSES": (
+        "rest_framework_json_api.renderers.JSONRenderer",
         # If you're performance testing, you will want to use the browseable API
         # without forms, as the forms can generate their own queries.
         # If performance testing, enable:
         # 'example.utils.BrowsableAPIRendererWithoutForms',
         # Otherwise, to play around with the browseable API, enable:
-        'rest_framework.renderers.BrowsableAPIRenderer',
+        "rest_framework_json_api.renderers.BrowsableAPIRenderer",
     ),
-    'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
-    'DEFAULT_SCHEMA_CLASS': 'rest_framework_json_api.schemas.openapi.AutoSchema',
-    'DEFAULT_FILTER_BACKENDS': (
-        'rest_framework_json_api.filters.OrderingFilter',
-        'rest_framework_json_api.django_filters.DjangoFilterBackend',
-        'rest_framework.filters.SearchFilter',
+    "DEFAULT_METADATA_CLASS": "rest_framework_json_api.metadata.JSONAPIMetadata",
+    "DEFAULT_FILTER_BACKENDS": (
+        "rest_framework_json_api.filters.OrderingFilter",
+        "rest_framework_json_api.django_filters.DjangoFilterBackend",
+        "rest_framework.filters.SearchFilter",
     ),
-    'SEARCH_PARAM': 'filter[search]',
-    'TEST_REQUEST_RENDERER_CLASSES': (
-        'rest_framework_json_api.renderers.JSONRenderer',
+    "SEARCH_PARAM": "filter[search]",
+    "TEST_REQUEST_RENDERER_CLASSES": (
+        "rest_framework_json_api.renderers.JSONRenderer",
     ),
-    'TEST_REQUEST_DEFAULT_FORMAT': 'vnd.api+json'
+    "TEST_REQUEST_DEFAULT_FORMAT": "vnd.api+json",
 }
diff --git a/example/settings/test.py b/example/settings/test.py
index b47c3fe5..2b92b5b3 100644
--- a/example/settings/test.py
+++ b/example/settings/test.py
@@ -1,18 +1,20 @@
 from .dev import *  # noqa
 
 DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': ':memory:',
+    "default": {
+        "ENGINE": "django.db.backends.sqlite3",
+        "NAME": ":memory:",
     }
 }
 
-ROOT_URLCONF = 'example.urls_test'
+ROOT_URLCONF = "example.urls_test"
 
-JSON_API_FORMAT_FIELD_NAMES = 'camelize'
-JSON_API_FORMAT_TYPES = 'camelize'
+JSON_API_FORMAT_FIELD_NAMES = "camelize"
+JSON_API_FORMAT_TYPES = "camelize"
 JSON_API_PLURALIZE_TYPES = True
 
-REST_FRAMEWORK.update({  # noqa
-    'PAGE_SIZE': 1,
-})
+REST_FRAMEWORK.update(  # noqa: F405
+    {  # noqa
+        "PAGE_SIZE": 1,
+    }
+)
diff --git a/example/templates/swagger-ui.html b/example/templates/swagger-ui.html
deleted file mode 100644
index 29776491..00000000
--- a/example/templates/swagger-ui.html
+++ /dev/null
@@ -1,28 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <title>Swagger</title>
-    <meta charset="utf-8"/>
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <link rel="stylesheet" type="text/css" href="//unpkg.com/swagger-ui-dist@3/swagger-ui.css" />
-  </head>
-  <body>
-    <div id="swagger-ui"></div>
-    <script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
-    <script>
-    const ui = SwaggerUIBundle({
-        url: "{% url schema_url %}",
-        dom_id: '#swagger-ui',
-        presets: [
-          SwaggerUIBundle.presets.apis,
-          SwaggerUIBundle.SwaggerUIStandalonePreset
-        ],
-        layout: "BaseLayout",
-        requestInterceptor: (request) => {
-          request.headers['X-CSRFToken'] = "{{ csrf_token }}"
-          return request;
-        }
-      })
-    </script>
-  </body>
-</html>
\ No newline at end of file
diff --git a/example/tests/__init__.py b/example/tests/__init__.py
index 30a04e22..94a86486 100644
--- a/example/tests/__init__.py
+++ b/example/tests/__init__.py
@@ -1,4 +1,3 @@
-
 from django.contrib.auth import get_user_model
 from rest_framework.test import APITestCase
 
@@ -12,18 +11,15 @@ def setUp(self):
         """
         Create those users
         """
-        super(TestBase, self).setUp()
+        super().setUp()
         self.create_users()
 
-    def create_user(self, username, email, password="pw",
-                    first_name='', last_name=''):
+    def create_user(self, username, email, password="pw", first_name="", last_name=""):
         """
         Helper method to create a user
         """
         User = get_user_model()
-        user = User.objects.create_user(
-            username, email, password=password
-        )
+        user = User.objects.create_user(username, email, password=password)
         if first_name or last_name:
             user.first_name = first_name
             user.last_name = last_name
@@ -35,8 +31,8 @@ def create_users(self):
         Create a couple users
         """
         self.john = self.create_user(
-            'trane', 'john@example.com',
-            first_name='John', last_name="Coltrane")
+            "trane", "john@example.com", first_name="John", last_name="Coltrane"
+        )
         self.miles = self.create_user(
-            'miles', 'miles@example.com',
-            first_name="Miles", last_name="Davis")
+            "miles", "miles@example.com", first_name="Miles", last_name="Davis"
+        )
diff --git a/example/tests/__snapshots__/test_errors.ambr b/example/tests/__snapshots__/test_errors.ambr
new file mode 100644
index 00000000..5dca2771
--- /dev/null
+++ b/example/tests/__snapshots__/test_errors.ambr
@@ -0,0 +1,186 @@
+# serializer version: 1
+# name: test_first_level_attribute_error
+  dict({
+    'errors': list([
+      dict({
+        'code': 'required',
+        'detail': 'This field is required.',
+        'source': dict({
+          'pointer': '/data/attributes/headline',
+        }),
+        'status': '400',
+      }),
+    ]),
+  })
+# ---
+# name: test_first_level_custom_attribute_error
+  dict({
+    'errors': list([
+      dict({
+        'detail': 'Too short',
+        'source': dict({
+          'pointer': '/data/attributes/body-text',
+        }),
+        'title': 'Too Short title',
+      }),
+    ]),
+  })
+# ---
+# name: test_many_third_level_dict_errors
+  dict({
+    'errors': list([
+      dict({
+        'code': 'required',
+        'detail': 'This field is required.',
+        'source': dict({
+          'pointer': '/data/attributes/comments/0/attachment/data',
+        }),
+        'status': '400',
+      }),
+      dict({
+        'code': 'required',
+        'detail': 'This field is required.',
+        'source': dict({
+          'pointer': '/data/attributes/comments/0/body',
+        }),
+        'status': '400',
+      }),
+    ]),
+  })
+# ---
+# name: test_relationship_errors_has_correct_pointers_with_camelize
+  dict({
+    'errors': list([
+      dict({
+        'code': 'incorrect_type',
+        'detail': 'Incorrect type. Expected resource identifier object, received str.',
+        'source': dict({
+          'pointer': '/data/relationships/authors',
+        }),
+        'status': '400',
+      }),
+      dict({
+        'code': 'incorrect_type',
+        'detail': 'Incorrect type. Expected resource identifier object, received str.',
+        'source': dict({
+          'pointer': '/data/relationships/mainAuthor',
+        }),
+        'status': '400',
+      }),
+    ]),
+  })
+# ---
+# name: test_relationship_errors_has_correct_pointers_with_dasherize
+  dict({
+    'errors': list([
+      dict({
+        'code': 'incorrect_type',
+        'detail': 'Incorrect type. Expected resource identifier object, received str.',
+        'source': dict({
+          'pointer': '/data/relationships/authors',
+        }),
+        'status': '400',
+      }),
+      dict({
+        'code': 'incorrect_type',
+        'detail': 'Incorrect type. Expected resource identifier object, received str.',
+        'source': dict({
+          'pointer': '/data/relationships/main-author',
+        }),
+        'status': '400',
+      }),
+    ]),
+  })
+# ---
+# name: test_relationship_errors_has_correct_pointers_with_no_formatting
+  dict({
+    'errors': list([
+      dict({
+        'code': 'incorrect_type',
+        'detail': 'Incorrect type. Expected resource identifier object, received str.',
+        'source': dict({
+          'pointer': '/data/relationships/authors',
+        }),
+        'status': '400',
+      }),
+      dict({
+        'code': 'incorrect_type',
+        'detail': 'Incorrect type. Expected resource identifier object, received str.',
+        'source': dict({
+          'pointer': '/data/relationships/main_author',
+        }),
+        'status': '400',
+      }),
+    ]),
+  })
+# ---
+# name: test_second_level_array_error
+  dict({
+    'errors': list([
+      dict({
+        'code': 'required',
+        'detail': 'This field is required.',
+        'source': dict({
+          'pointer': '/data/attributes/comments/0/body',
+        }),
+        'status': '400',
+      }),
+    ]),
+  })
+# ---
+# name: test_second_level_dict_error
+  dict({
+    'errors': list([
+      dict({
+        'code': 'required',
+        'detail': 'This field is required.',
+        'source': dict({
+          'pointer': '/data/attributes/comment/body',
+        }),
+        'status': '400',
+      }),
+    ]),
+  })
+# ---
+# name: test_third_level_array_error
+  dict({
+    'errors': list([
+      dict({
+        'code': 'required',
+        'detail': 'This field is required.',
+        'source': dict({
+          'pointer': '/data/attributes/comments/0/attachments/0/data',
+        }),
+        'status': '400',
+      }),
+    ]),
+  })
+# ---
+# name: test_third_level_custom_array_error
+  dict({
+    'errors': list([
+      dict({
+        'code': 'invalid',
+        'detail': 'Too short data',
+        'source': dict({
+          'pointer': '/data/attributes/comments/0/attachments/0/data',
+        }),
+        'status': '400',
+      }),
+    ]),
+  })
+# ---
+# name: test_third_level_dict_error
+  dict({
+    'errors': list([
+      dict({
+        'code': 'required',
+        'detail': 'This field is required.',
+        'source': dict({
+          'pointer': '/data/attributes/comments/0/attachment/data',
+        }),
+        'status': '400',
+      }),
+    ]),
+  })
+# ---
diff --git a/example/tests/conftest.py b/example/tests/conftest.py
index de2bea19..6e4b05ba 100644
--- a/example/tests/conftest.py
+++ b/example/tests/conftest.py
@@ -12,8 +12,9 @@
     CommentFactory,
     CompanyFactory,
     EntryFactory,
+    QuestionnaireFactory,
     ResearchProjectFactory,
-    TaggedItemFactory
+    TaggedItemFactory,
 )
 
 register(BlogFactory)
@@ -27,11 +28,11 @@
 register(ArtProjectFactory)
 register(ResearchProjectFactory)
 register(CompanyFactory)
+register(QuestionnaireFactory)
 
 
 @pytest.fixture
 def single_entry(blog, author, entry_factory, comment_factory, tagged_item_factory):
-
     entry = entry_factory(blog=blog, authors=(author,))
     comment_factory(entry=entry)
     tagged_item_factory(content_object=entry)
@@ -40,7 +41,6 @@ def single_entry(blog, author, entry_factory, comment_factory, tagged_item_facto
 
 @pytest.fixture
 def multiple_entries(blog_factory, author_factory, entry_factory, comment_factory):
-
     entries = [
         entry_factory(blog=blog_factory(), authors=(author_factory(),)),
         entry_factory(blog=blog_factory(), authors=(author_factory(),)),
@@ -59,7 +59,9 @@ def single_comment(blog, author, entry_factory, comment_factory):
 
 @pytest.fixture
 def single_company(art_project_factory, research_project_factory, company_factory):
-    company = company_factory(future_projects=(research_project_factory(), art_project_factory()))
+    company = company_factory(
+        future_projects=(research_project_factory(), art_project_factory())
+    )
     return company
 
 
diff --git a/example/tests/integration/test_browsable_api.py b/example/tests/integration/test_browsable_api.py
new file mode 100644
index 00000000..14ebe3fb
--- /dev/null
+++ b/example/tests/integration/test_browsable_api.py
@@ -0,0 +1,36 @@
+import re
+
+import pytest
+from django.urls import reverse
+
+pytestmark = pytest.mark.django_db
+
+
+def test_browsable_api_with_included_serializers(single_entry, client):
+    response = client.get(
+        reverse("entry-detail", kwargs={"pk": single_entry.pk, "format": "api"})
+    )
+    content = str(response.content)
+    assert response.status_code == 200
+    assert re.search(r"JSON:API includes", content)
+    assert re.search(
+        r'<input type="checkbox" name="includes" [^>]* value="authors.bio"', content
+    )
+
+
+def test_browsable_api_on_related_url(author, client):
+    url = reverse("author-related", kwargs={"pk": author.pk, "related_field": "bio"})
+    response = client.get(url, data={"format": "api"})
+    content = str(response.content)
+    assert response.status_code == 200
+    assert re.search(r"JSON:API includes", content)
+    assert re.search(
+        r'<input type="checkbox" name="includes" [^>]* value="metadata"', content
+    )
+
+
+def test_browsable_api_with_no_included_serializers(client):
+    response = client.get(reverse("projecttype-list", kwargs={"format": "api"}))
+    content = str(response.content)
+    assert response.status_code == 200
+    assert not re.search(r"JSON:API includes", content)
diff --git a/example/tests/integration/test_includes.py b/example/tests/integration/test_includes.py
index 953052de..d32ff7e3 100644
--- a/example/tests/integration/test_includes.py
+++ b/example/tests/integration/test_includes.py
@@ -4,178 +4,203 @@
 pytestmark = pytest.mark.django_db
 
 
-def test_included_data_on_list(multiple_entries, client):
-    response = client.get(reverse("entry-list"), data={'include': 'comments', 'page[size]': 5})
-    included = response.json().get('included')
-
-    assert len(response.json()['data']) == len(multiple_entries), (
-        'Incorrect entry count'
-    )
-    assert [x.get('type') for x in included] == ['comments', 'comments'], (
-        'List included types are incorrect'
-    )
-
-    comment_count = len([resource for resource in included if resource["type"] == "comments"])
-    expected_comment_count = sum([entry.comments.count() for entry in multiple_entries])
-    assert comment_count == expected_comment_count, 'List comment count is incorrect'
-
-
-def test_included_data_on_list_with_one_to_one_relations(multiple_entries, client):
-    response = client.get(reverse("entry-list"),
-                          data={'include': 'authors.bio.metadata', 'page[size]': 5})
-    included = response.json().get('included')
-
-    assert len(response.json()['data']) == len(multiple_entries), (
-        'Incorrect entry count'
-    )
-    expected_include_types = [
-        'authorBioMetadata', 'authorBioMetadata',
-        'authorBios', 'authorBios',
-        'authors', 'authors'
-    ]
-    include_types = [x.get('type') for x in included]
-    assert include_types == expected_include_types, (
-        'List included types are incorrect'
-    )
-
-
-def test_default_included_data_on_detail(single_entry, client):
-    return test_included_data_on_detail(single_entry=single_entry, client=client, query='')
-
-
-def test_included_data_on_detail(single_entry, client, query='?include=comments'):
-    response = client.get(reverse("entry-detail", kwargs={'pk': single_entry.pk}) + query)
-    included = response.json().get('included')
-
-    assert [x.get('type') for x in included] == ['comments'], 'Detail included types are incorrect'
-
-    comment_count = len([resource for resource in included if resource["type"] == "comments"])
-    expected_comment_count = single_entry.comments.count()
-    assert comment_count == expected_comment_count, 'Detail comment count is incorrect'
-
-
 def test_dynamic_related_data_is_included(single_entry, entry_factory, client):
     entry_factory()
     response = client.get(
-        reverse("entry-detail", kwargs={'pk': single_entry.pk}) + '?include=featured'
+        reverse("entry-detail", kwargs={"pk": single_entry.pk}) + "?include=featured"
     )
-    included = response.json().get('included')
+    included = response.json().get("included")
 
-    assert [x.get('type') for x in included] == ['entries'], 'Dynamic included types are incorrect'
-    assert len(included) == 1, 'The dynamically included blog entries are of an incorrect count'
+    assert [x.get("type") for x in included] == [
+        "entries"
+    ], "Dynamic included types are incorrect"
+    assert (
+        len(included) == 1
+    ), "The dynamically included blog entries are of an incorrect count"
 
 
 def test_dynamic_many_related_data_is_included(single_entry, entry_factory, client):
     entry_factory()
     response = client.get(
-        reverse("entry-detail", kwargs={'pk': single_entry.pk}) + '?include=suggested'
+        reverse("entry-detail", kwargs={"pk": single_entry.pk}) + "?include=suggested"
     )
-    included = response.json().get('included')
+    included = response.json().get("included")
 
     assert included
-    assert [x.get('type') for x in included] == ['entries'], 'Dynamic included types are incorrect'
+    assert [x.get("type") for x in included] == [
+        "entries"
+    ], "Dynamic included types are incorrect"
 
 
 def test_missing_field_not_included(author_bio_factory, author_factory, client):
     # First author does not have a bio
     author = author_factory(bio=None)
-    response = client.get(reverse('author-detail', args=[author.pk]) + '?include=bio')
-    assert 'included' not in response.json()
+    response = client.get(reverse("author-detail", args=[author.pk]) + "?include=bio")
+    assert "included" not in response.json()
     # Second author does
     author = author_factory()
-    response = client.get(reverse('author-detail', args=[author.pk]) + '?include=bio')
+    response = client.get(reverse("author-detail", args=[author.pk]) + "?include=bio")
     data = response.json()
-    assert 'included' in data
-    assert len(data['included']) == 1
-    assert data['included'][0]['attributes']['body'] == author.bio.body
+    assert "included" in data
+    assert len(data["included"]) == 1
+    assert data["included"][0]["attributes"]["body"] == author.bio.body
 
 
 def test_deep_included_data_on_list(multiple_entries, client):
-    response = client.get(reverse("entry-list") + '?include=comments,comments.author,'
-                          'comments.author.bio,comments.writer&page[size]=5')
-    included = response.json().get('included')
-
-    assert len(response.json()['data']) == len(multiple_entries), (
-        'Incorrect entry count'
+    response = client.get(
+        reverse("entry-list") + "?include=comments,comments.author,"
+        "comments.author.bio,comments.writer&page[size]=5"
     )
-    assert [x.get('type') for x in included] == [
-        'authorBios', 'authorBios', 'authors', 'authors',
-        'comments', 'comments', 'writers', 'writers'
-    ], 'List included types are incorrect'
-
-    comment_count = len([resource for resource in included if resource["type"] == "comments"])
-    expected_comment_count = sum([entry.comments.count() for entry in multiple_entries])
-    assert comment_count == expected_comment_count, 'List comment count is incorrect'
+    included = response.json().get("included")
+
+    assert len(response.json()["data"]) == len(
+        multiple_entries
+    ), "Incorrect entry count"
+    assert [x.get("type") for x in included] == [
+        "authorBios",
+        "authorBios",
+        "authors",
+        "authors",
+        "comments",
+        "comments",
+        "writers",
+        "writers",
+    ], "List included types are incorrect"
+
+    comment_count = len(
+        [resource for resource in included if resource["type"] == "comments"]
+    )
+    expected_comment_count = sum(entry.comments.count() for entry in multiple_entries)
+    assert comment_count == expected_comment_count, "List comment count is incorrect"
 
-    author_count = len([resource for resource in included if resource["type"] == "authors"])
+    author_count = len(
+        [resource for resource in included if resource["type"] == "authors"]
+    )
     expected_author_count = sum(
-        [entry.comments.filter(author__isnull=False).count() for entry in multiple_entries])
-    assert author_count == expected_author_count, 'List author count is incorrect'
+        entry.comments.filter(author__isnull=False).count()
+        for entry in multiple_entries
+    )
+    assert author_count == expected_author_count, "List author count is incorrect"
 
-    author_bio_count = len([resource for resource in included if resource["type"] == "authorBios"])
-    expected_author_bio_count = sum([entry.comments.filter(
-        author__bio__isnull=False).count() for entry in multiple_entries])
-    assert author_bio_count == expected_author_bio_count, 'List author bio count is incorrect'
+    author_bio_count = len(
+        [resource for resource in included if resource["type"] == "authorBios"]
+    )
+    expected_author_bio_count = sum(
+        entry.comments.filter(author__bio__isnull=False).count()
+        for entry in multiple_entries
+    )
+    assert (
+        author_bio_count == expected_author_bio_count
+    ), "List author bio count is incorrect"
 
     writer_count = len(
         [resource for resource in included if resource["type"] == "writers"]
     )
     expected_writer_count = sum(
-        [entry.comments.filter(author__isnull=False).count() for entry in multiple_entries])
-    assert writer_count == expected_writer_count, 'List writer count is incorrect'
+        entry.comments.filter(author__isnull=False).count()
+        for entry in multiple_entries
+    )
+    assert writer_count == expected_writer_count, "List writer count is incorrect"
 
     # Also include entry authors
-    response = client.get(reverse("entry-list") + '?include=authors,comments,comments.author,'
-                          'comments.author.bio&page[size]=5')
-    included = response.json().get('included')
-
-    assert len(response.json()['data']) == len(multiple_entries), (
-        'Incorrect entry count'
+    response = client.get(
+        reverse("entry-list") + "?include=authors,comments,comments.author,"
+        "comments.author.bio&page[size]=5"
+    )
+    included = response.json().get("included")
+
+    assert len(response.json()["data"]) == len(
+        multiple_entries
+    ), "Incorrect entry count"
+    assert [x.get("type") for x in included] == [
+        "authorBios",
+        "authorBios",
+        "authors",
+        "authors",
+        "authors",
+        "authors",
+        "comments",
+        "comments",
+    ], "List included types are incorrect"
+
+    author_count = len(
+        [resource for resource in included if resource["type"] == "authors"]
     )
-    assert [x.get('type') for x in included] == [
-        'authorBios', 'authorBios', 'authors', 'authors', 'authors', 'authors',
-        'comments', 'comments'], 'List included types are incorrect'
-
-    author_count = len([resource for resource in included if resource["type"] == "authors"])
     expected_author_count = sum(
-        [entry.authors.count() for entry in multiple_entries] +
-        [entry.comments.filter(author__isnull=False).count() for entry in multiple_entries])
-    assert author_count == expected_author_count, 'List author count is incorrect'
+        [entry.authors.count() for entry in multiple_entries]
+        + [
+            entry.comments.filter(author__isnull=False).count()
+            for entry in multiple_entries
+        ]
+    )
+    assert author_count == expected_author_count, "List author count is incorrect"
 
 
 def test_deep_included_data_on_detail(single_entry, client):
     # Same test as in list but also ensures that intermediate resources (here comments' authors)
     # are returned along with the leaf nodes
-    response = client.get(reverse("entry-detail", kwargs={'pk': single_entry.pk}) +
-                          '?include=comments,comments.author.bio')
-    included = response.json().get('included')
+    response = client.get(
+        reverse("entry-detail", kwargs={"pk": single_entry.pk})
+        + "?include=comments,comments.author.bio"
+    )
+    included = response.json().get("included")
 
-    assert [x.get('type') for x in included] == ['authorBios', 'authors', 'comments'], \
-        'Detail included types are incorrect'
+    assert [x.get("type") for x in included] == [
+        "authorBios",
+        "authors",
+        "comments",
+    ], "Detail included types are incorrect"
 
-    comment_count = len([resource for resource in included if resource["type"] == "comments"])
+    comment_count = len(
+        [resource for resource in included if resource["type"] == "comments"]
+    )
     expected_comment_count = single_entry.comments.count()
-    assert comment_count == expected_comment_count, 'Detail comment count is incorrect'
+    assert comment_count == expected_comment_count, "Detail comment count is incorrect"
 
-    author_bio_count = len([resource for resource in included if resource["type"] == "authorBios"])
-    expected_author_bio_count = single_entry.comments.filter(author__bio__isnull=False).count()
-    assert author_bio_count == expected_author_bio_count, 'Detail author bio count is incorrect'
+    author_bio_count = len(
+        [resource for resource in included if resource["type"] == "authorBios"]
+    )
+    expected_author_bio_count = single_entry.comments.filter(
+        author__bio__isnull=False
+    ).count()
+    assert (
+        author_bio_count == expected_author_bio_count
+    ), "Detail author bio count is incorrect"
 
 
 def test_data_resource_not_included_again(single_comment, client):
     # This test makes sure that the resource which is in the data field is excluded
     # from the included field.
-    response = client.get(reverse("comment-detail", kwargs={'pk': single_comment.pk}) +
-                          '?include=entry.comments')
+    response = client.get(
+        reverse("comment-detail", kwargs={"pk": single_comment.pk})
+        + "?include=entry.comments"
+    )
 
-    included = response.json().get('included')
+    included = response.json().get("included")
 
-    included_comments = [resource for resource in included if resource["type"] == "comments"]
-    assert single_comment.pk not in [int(x.get('id')) for x in included_comments], \
-        "Resource of the data field duplicated in included"
+    included_comments = [
+        resource for resource in included if resource["type"] == "comments"
+    ]
+    assert single_comment.pk not in [
+        int(x.get("id")) for x in included_comments
+    ], "Resource of the data field duplicated in included"
 
     comment_count = len(included_comments)
     expected_comment_count = single_comment.entry.comments.count()
     # The comment in the data attribute must not be included again.
     expected_comment_count -= 1
     assert comment_count == expected_comment_count, "Comment count incorrect"
+
+
+def test_meta_object_added_to_included_resources(single_entry, client):
+    response = client.get(
+        reverse("entry-detail", kwargs={"pk": single_entry.pk}) + "?include=comments"
+    )
+    assert response.json()["included"][0].get("meta")
+
+    response = client.get(
+        reverse("entry-detail", kwargs={"pk": single_entry.pk})
+        + "?include=comments.author"
+    )
+    assert response.json()["included"][0].get("meta")
+    assert response.json()["included"][1].get("meta")
diff --git a/example/tests/integration/test_meta.py b/example/tests/integration/test_meta.py
index 25457b1c..c63a6d9b 100644
--- a/example/tests/integration/test_meta.py
+++ b/example/tests/integration/test_meta.py
@@ -7,32 +7,27 @@
 
 
 def test_top_level_meta_for_list_view(blog, client):
-
     expected = {
-        "data": [{
-            "type": "blogs",
-            "id": "1",
-            "attributes": {
-                "name": blog.name
-            },
-            "links": {
-                "self": 'http://testserver/blogs/1'
-            },
-            'relationships': {'tags': {'data': [], 'meta': {'count': 0}}},
-            "meta": {
-                "copyright": datetime.now().year
-            },
-        }],
-        'links': {
-            'first': 'http://testserver/blogs?page%5Bnumber%5D=1',
-            'last': 'http://testserver/blogs?page%5Bnumber%5D=1',
-            'next': None,
-            'prev': None
+        "data": [
+            {
+                "type": "blogs",
+                "id": "1",
+                "attributes": {"name": blog.name},
+                "links": {"self": "http://testserver/blogs/1"},
+                "relationships": {"tags": {"data": [], "meta": {"count": 0}}},
+                "meta": {"copyright": datetime.now().year},
+            }
+        ],
+        "links": {
+            "first": "http://testserver/blogs?page%5Bnumber%5D=1",
+            "last": "http://testserver/blogs?page%5Bnumber%5D=1",
+            "next": None,
+            "prev": None,
+        },
+        "meta": {
+            "pagination": {"count": 1, "page": 1, "pages": 1},
+            "apiDocs": "/docs/api/blogs",
         },
-        'meta': {
-            'pagination': {'count': 1, 'page': 1, 'pages': 1},
-            'apiDocs': '/docs/api/blogs'
-        }
     }
 
     response = client.get(reverse("blog-list"))
@@ -41,27 +36,18 @@ def test_top_level_meta_for_list_view(blog, client):
 
 
 def test_top_level_meta_for_detail_view(blog, client):
-
     expected = {
         "data": {
             "type": "blogs",
             "id": "1",
-            "attributes": {
-                "name": blog.name
-            },
-            'relationships': {'tags': {'data': [], 'meta': {'count': 0}}},
-            "links": {
-                "self": "http://testserver/blogs/1"
-            },
-            "meta": {
-                "copyright": datetime.now().year
-            },
-        },
-        "meta": {
-            "apiDocs": "/docs/api/blogs"
+            "attributes": {"name": blog.name},
+            "relationships": {"tags": {"data": [], "meta": {"count": 0}}},
+            "links": {"self": "http://testserver/blogs/1"},
+            "meta": {"copyright": datetime.now().year},
         },
+        "meta": {"apiDocs": "/docs/api/blogs"},
     }
 
-    response = client.get(reverse("blog-detail", kwargs={'pk': blog.pk}))
+    response = client.get(reverse("blog-detail", kwargs={"pk": blog.pk}))
 
     assert expected == response.json()
diff --git a/example/tests/integration/test_model_resource_name.py b/example/tests/integration/test_model_resource_name.py
index a69503ae..b2fda333 100644
--- a/example/tests/integration/test_model_resource_name.py
+++ b/example/tests/integration/test_model_resource_name.py
@@ -18,90 +18,99 @@ def _check_resource_and_relationship_comment_type_match(django_client):
     entry_response = django_client.get(reverse("entry-list"))
     comment_response = django_client.get(reverse("comment-list"))
 
-    comment_resource_type = comment_response.json().get('data')[0].get('type')
-    comment_relationship_type = entry_response.json().get(
-        'data')[0].get('relationships').get('comments').get('data')[0].get('type')
-
-    assert comment_resource_type == comment_relationship_type, (
-        "The resource type seen in the relationships and head resource do not match"
+    comment_resource_type = comment_response.json().get("data")[0].get("type")
+    comment_relationship_type = (
+        entry_response.json()
+        .get("data")[0]
+        .get("relationships")
+        .get("comments")
+        .get("data")[0]
+        .get("type")
     )
 
+    assert (
+        comment_resource_type == comment_relationship_type
+    ), "The resource type seen in the relationships and head resource do not match"
+
 
 def _check_relationship_and_included_comment_type_are_the_same(django_client, url):
     response = django_client.get(url + "?include=comments")
-    data = response.json().get('data')[0]
-    comment = response.json().get('included')[0]
-
-    comment_relationship_type = data.get('relationships').get('comments').get('data')[0].get('type')
-    comment_included_type = comment.get('type')
+    data = response.json().get("data")[0]
+    comment = response.json().get("included")[0]
 
-    assert comment_relationship_type == comment_included_type, (
-        "The resource type seen in the relationships and included do not match"
+    comment_relationship_type = (
+        data.get("relationships").get("comments").get("data")[0].get("type")
     )
+    comment_included_type = comment.get("type")
+
+    assert (
+        comment_relationship_type == comment_included_type
+    ), "The resource type seen in the relationships and included do not match"
 
 
 @pytest.mark.usefixtures("single_entry")
 class TestModelResourceName:
-
     create_data = {
-        'data': {
-            'type': 'resource_name_from_JSONAPIMeta',
-            'id': None,
-            'attributes': {
-                'body': 'example',
+        "data": {
+            "type": "resource_name_from_JSONAPIMeta",
+            "id": None,
+            "attributes": {
+                "body": "example",
+            },
+            "relationships": {
+                "entry": {"data": {"type": "resource_name_from_JSONAPIMeta", "id": 1}}
             },
-            'relationships': {
-                'entry': {
-                    'data': {
-                        'type': 'resource_name_from_JSONAPIMeta',
-                        'id': 1
-                    }
-                }
-            }
         }
     }
 
     def test_model_resource_name_on_list(self, client):
         models.Comment.__bases__ += (_PatchedModel,)
         response = client.get(reverse("comment-list"))
-        data = response.json()['data'][0]
+        data = response.json()["data"][0]
         # name should be super-author instead of model name RenamedAuthor
-        assert (data.get('type') == 'resource_name_from_JSONAPIMeta'), (
-            'resource_name from model incorrect on list')
+        assert (
+            data.get("type") == "resource_name_from_JSONAPIMeta"
+        ), "resource_name from model incorrect on list"
 
     # Precedence tests
     def test_resource_name_precendence(self, client, monkeypatch):
         # default
         response = client.get(reverse("comment-list"))
-        data = response.json()['data'][0]
-        assert (data.get('type') == 'comments'), (
-            'resource_name from model incorrect on list')
+        data = response.json()["data"][0]
+        assert (
+            data.get("type") == "comments"
+        ), "resource_name from model incorrect on list"
 
         # model > default
         models.Comment.__bases__ += (_PatchedModel,)
         response = client.get(reverse("comment-list"))
-        data = response.json()['data'][0]
-        assert (data.get('type') == 'resource_name_from_JSONAPIMeta'), (
-            'resource_name from model incorrect on list')
+        data = response.json()["data"][0]
+        assert (
+            data.get("type") == "resource_name_from_JSONAPIMeta"
+        ), "resource_name from model incorrect on list"
 
         # serializer > model
         monkeypatch.setattr(
             serializers.CommentSerializer.Meta,
-            'resource_name',
-            'resource_name_from_serializer',
-            False
+            "resource_name",
+            "resource_name_from_serializer",
+            False,
         )
         response = client.get(reverse("comment-list"))
-        data = response.json()['data'][0]
-        assert (data.get('type') == 'resource_name_from_serializer'), (
-            'resource_name from serializer incorrect on list')
+        data = response.json()["data"][0]
+        assert (
+            data.get("type") == "resource_name_from_serializer"
+        ), "resource_name from serializer incorrect on list"
 
         # view > serializer > model
-        monkeypatch.setattr(views.CommentViewSet, 'resource_name', 'resource_name_from_view', False)
+        monkeypatch.setattr(
+            views.CommentViewSet, "resource_name", "resource_name_from_view", False
+        )
         response = client.get(reverse("comment-list"))
-        data = response.json()['data'][0]
-        assert (data.get('type') == 'resource_name_from_view'), (
-            'resource_name from view incorrect on list')
+        data = response.json()["data"][0]
+        assert (
+            data.get("type") == "resource_name_from_view"
+        ), "resource_name from view incorrect on list"
 
     def test_model_resource_name_create(self, client):
         models.Comment.__bases__ += (_PatchedModel,)
@@ -113,19 +122,18 @@ def test_model_resource_name_create(self, client):
     def test_serializer_resource_name_create(self, client, monkeypatch):
         monkeypatch.setattr(
             serializers.CommentSerializer.Meta,
-            'resource_name',
-            'renamed_comments',
-            False
+            "resource_name",
+            "renamed_comments",
+            False,
         )
         monkeypatch.setattr(
-            serializers.EntrySerializer.Meta,
-            'resource_name',
-            'renamed_entries',
-            False
+            serializers.EntrySerializer.Meta, "resource_name", "renamed_entries", False
         )
         create_data = deepcopy(self.create_data)
-        create_data['data']['type'] = 'renamed_comments'
-        create_data['data']['relationships']['entry']['data']['type'] = 'renamed_entries'
+        create_data["data"]["type"] = "renamed_comments"
+        create_data["data"]["relationships"]["entry"]["data"][
+            "type"
+        ] = "renamed_entries"
 
         response = client.post(reverse("comment-list"), create_data)
 
@@ -138,40 +146,61 @@ def teardown_method(self, method):
 
 @pytest.mark.usefixtures("single_entry")
 class TestResourceNameConsistency:
-
     # Included rename tests
     def test_type_match_on_included_and_inline_base(self, client):
-        _check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))
+        _check_relationship_and_included_comment_type_are_the_same(
+            client, reverse("entry-list")
+        )
 
     def test_type_match_on_included_and_inline_with_JSONAPIMeta(self, client):
         models.Comment.__bases__ += (_PatchedModel,)
 
-        _check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))
+        _check_relationship_and_included_comment_type_are_the_same(
+            client, reverse("entry-list")
+        )
 
-    def test_type_match_on_included_and_inline_with_serializer_resource_name(self, client):
-        serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"
+    def test_type_match_on_included_and_inline_with_serializer_resource_name(
+        self, client
+    ):
+        serializers.CommentSerializer.Meta.resource_name = (
+            "resource_name_from_serializer"
+        )
 
-        _check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))
+        _check_relationship_and_included_comment_type_are_the_same(
+            client, reverse("entry-list")
+        )
 
-    def test_type_match_on_included_and_inline_without_serializer_resource_name(self, client):
+    def test_type_match_on_included_and_inline_without_serializer_resource_name(
+        self, client
+    ):
         serializers.CommentSerializer.Meta.resource_name = None
 
-        _check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))
+        _check_relationship_and_included_comment_type_are_the_same(
+            client, reverse("entry-list")
+        )
 
     def test_type_match_on_included_and_inline_with_serializer_resource_name_and_JSONAPIMeta(
-            self, client
+        self, client
     ):
         models.Comment.__bases__ += (_PatchedModel,)
-        serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"
+        serializers.CommentSerializer.Meta.resource_name = (
+            "resource_name_from_serializer"
+        )
 
-        _check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))
+        _check_relationship_and_included_comment_type_are_the_same(
+            client, reverse("entry-list")
+        )
 
     # Relation rename tests
     def test_resource_and_relationship_type_match(self, client):
         _check_resource_and_relationship_comment_type_match(client)
 
-    def test_resource_and_relationship_type_match_with_serializer_resource_name(self, client):
-        serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"
+    def test_resource_and_relationship_type_match_with_serializer_resource_name(
+        self, client
+    ):
+        serializers.CommentSerializer.Meta.resource_name = (
+            "resource_name_from_serializer"
+        )
 
         _check_resource_and_relationship_comment_type_match(client)
 
@@ -181,10 +210,12 @@ def test_resource_and_relationship_type_match_with_JSONAPIMeta(self, client):
         _check_resource_and_relationship_comment_type_match(client)
 
     def test_resource_and_relationship_type_match_with_serializer_resource_name_and_JSONAPIMeta(
-            self, client
+        self, client
     ):
         models.Comment.__bases__ += (_PatchedModel,)
-        serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"
+        serializers.CommentSerializer.Meta.resource_name = (
+            "resource_name_from_serializer"
+        )
 
         _check_resource_and_relationship_comment_type_match(client)
 
diff --git a/example/tests/integration/test_non_paginated_responses.py b/example/tests/integration/test_non_paginated_responses.py
index b73eae5e..6434b7a7 100644
--- a/example/tests/integration/test_non_paginated_responses.py
+++ b/example/tests/integration/test_non_paginated_responses.py
@@ -7,134 +7,123 @@
 
 
 @mock.patch(
-    'rest_framework_json_api.utils'
-    '.get_default_included_resources_from_serializer',
-    new=lambda s: [])
+    "rest_framework_json_api.utils" ".get_default_included_resources_from_serializer",
+    new=lambda s: [],
+)
 def test_multiple_entries_no_pagination(multiple_entries, client):
-
     expected = {
         "data": [
             {
-                "type": "posts",
+                "type": "entries",
                 "id": "1",
-                "attributes":
-                {
+                "attributes": {
                     "headline": multiple_entries[0].headline,
                     "bodyText": multiple_entries[0].body_text,
                     "pubDate": None,
-                    "modDate": None
-                },
-                "meta": {
-                    "bodyFormat": "text"
+                    "modDate": None,
                 },
-                "relationships":
-                {
-                    "blog": {
-                        "data": {"type": "blogs", "id": "1"}
-                    },
+                "meta": {"bodyFormat": "text"},
+                "relationships": {
+                    "blog": {"data": {"type": "blogs", "id": "1"}},
                     "blogHyperlinked": {
                         "links": {
                             "related": "http://testserver/entries/1/blog",
-                            "self": "http://testserver/entries/1/relationships/blog_hyperlinked"
+                            "self": "http://testserver/entries/1/relationships/blog_hyperlinked",  # noqa: B950
                         }
                     },
                     "authors": {
                         "meta": {"count": 1},
-                        "data": [{"type": "authors", "id": "1"}]
+                        "data": [{"type": "authors", "id": "1"}],
                     },
                     "comments": {
                         "meta": {"count": 1},
-                        "data": [{"type": "comments", "id": "1"}]
+                        "data": [{"type": "comments", "id": "1"}],
                     },
                     "commentsHyperlinked": {
                         "links": {
                             "related": "http://testserver/entries/1/comments",
-                            "self": "http://testserver/entries/1/relationships/comments_hyperlinked"
+                            "self": "http://testserver/entries/1/relationships/comments_hyperlinked",  # noqa: B950
                         }
                     },
                     "suggested": {
                         "data": [{"type": "entries", "id": "2"}],
                         "links": {
                             "related": "http://testserver/entries/1/suggested/",
-                            "self": "http://testserver/entries/1/relationships/suggested"
-                        }
+                            "self": "http://testserver/entries/1/relationships/suggested",
+                        },
+                        "meta": {"count": 1},
                     },
                     "suggestedHyperlinked": {
                         "links": {
                             "related": "http://testserver/entries/1/suggested/",
                             "self": "http://testserver/entries/1"
-                                    "/relationships/suggested_hyperlinked"
+                            "/relationships/suggested_hyperlinked",
                         }
                     },
                     "featuredHyperlinked": {
                         "links": {
                             "related": "http://testserver/entries/1/featured",
-                            "self": "http://testserver/entries/1/relationships/featured_hyperlinked"
+                            "self": "http://testserver/entries/1/relationships/featured_hyperlinked",  # noqa: B950
                         }
                     },
-                    'tags': {'data': [], 'meta': {'count': 0}},
-                }
+                    "tags": {"data": [], "meta": {"count": 0}},
+                },
             },
             {
-                "type": "posts",
+                "type": "entries",
                 "id": "2",
-                "attributes":
-                {
+                "attributes": {
                     "headline": multiple_entries[1].headline,
                     "bodyText": multiple_entries[1].body_text,
                     "pubDate": None,
-                    "modDate": None
+                    "modDate": None,
                 },
-                "meta": {
-                    "bodyFormat": "text"
-                },
-                "relationships":
-                {
-                    "blog": {
-                        "data": {"type": "blogs", "id": "2"}
-                    },
+                "meta": {"bodyFormat": "text"},
+                "relationships": {
+                    "blog": {"data": {"type": "blogs", "id": "2"}},
                     "blogHyperlinked": {
                         "links": {
                             "related": "http://testserver/entries/2/blog",
-                            "self": "http://testserver/entries/2/relationships/blog_hyperlinked",
+                            "self": "http://testserver/entries/2/relationships/blog_hyperlinked",  # noqa: B950
                         }
                     },
                     "authors": {
                         "meta": {"count": 1},
-                        "data": [{"type": "authors", "id": "2"}]
+                        "data": [{"type": "authors", "id": "2"}],
                     },
                     "comments": {
                         "meta": {"count": 1},
-                        "data": [{"type": "comments", "id": "2"}]
+                        "data": [{"type": "comments", "id": "2"}],
                     },
                     "commentsHyperlinked": {
                         "links": {
                             "related": "http://testserver/entries/2/comments",
-                            "self": "http://testserver/entries/2/relationships/comments_hyperlinked"
+                            "self": "http://testserver/entries/2/relationships/comments_hyperlinked",  # noqa: B950
                         }
                     },
                     "suggested": {
                         "data": [{"type": "entries", "id": "1"}],
                         "links": {
                             "related": "http://testserver/entries/2/suggested/",
-                            "self": "http://testserver/entries/2/relationships/suggested"
-                        }
+                            "self": "http://testserver/entries/2/relationships/suggested",
+                        },
+                        "meta": {"count": 1},
                     },
                     "suggestedHyperlinked": {
                         "links": {
                             "related": "http://testserver/entries/2/suggested/",
                             "self": "http://testserver/entries/2"
-                                    "/relationships/suggested_hyperlinked"
+                            "/relationships/suggested_hyperlinked",
                         }
                     },
                     "featuredHyperlinked": {
                         "links": {
                             "related": "http://testserver/entries/2/featured",
-                            "self": "http://testserver/entries/2/relationships/featured_hyperlinked"
+                            "self": "http://testserver/entries/2/relationships/featured_hyperlinked",  # noqa: B950
                         }
                     },
-                    'tags': {'data': [], 'meta': {'count': 0}},
-                }
+                    "tags": {"data": [], "meta": {"count": 0}},
+                },
             },
         ]
     }
diff --git a/example/tests/integration/test_pagination.py b/example/tests/integration/test_pagination.py
index 1b12a0d2..0f5ac17e 100644
--- a/example/tests/integration/test_pagination.py
+++ b/example/tests/integration/test_pagination.py
@@ -7,97 +7,79 @@
 
 
 @mock.patch(
-    'rest_framework_json_api.utils'
-    '.get_default_included_resources_from_serializer',
-    new=lambda s: [])
+    "rest_framework_json_api.utils" ".get_default_included_resources_from_serializer",
+    new=lambda s: [],
+)
 def test_pagination_with_single_entry(single_entry, client):
-
     expected = {
         "data": [
             {
-                "type": "posts",
+                "type": "entries",
                 "id": "1",
-                "attributes":
-                {
+                "attributes": {
                     "headline": single_entry.headline,
                     "bodyText": single_entry.body_text,
                     "pubDate": None,
-                    "modDate": None
-                },
-                "meta": {
-                    "bodyFormat": "text"
+                    "modDate": None,
                 },
-                "relationships":
-                {
-                    "blog": {
-                        "data": {"type": "blogs", "id": "1"}
-                    },
+                "meta": {"bodyFormat": "text"},
+                "relationships": {
+                    "blog": {"data": {"type": "blogs", "id": "1"}},
                     "blogHyperlinked": {
                         "links": {
                             "related": "http://testserver/entries/1/blog",
-                            "self": "http://testserver/entries/1/relationships/blog_hyperlinked",
+                            "self": "http://testserver/entries/1/relationships/blog_hyperlinked",  # noqa: B950
                         }
                     },
                     "authors": {
                         "meta": {"count": 1},
-                        "data": [{"type": "authors", "id": "1"}]
+                        "data": [{"type": "authors", "id": "1"}],
                     },
                     "comments": {
                         "meta": {"count": 1},
-                        "data": [{"type": "comments", "id": "1"}]
+                        "data": [{"type": "comments", "id": "1"}],
                     },
                     "commentsHyperlinked": {
                         "links": {
                             "related": "http://testserver/entries/1/comments",
-                            "self": "http://testserver/entries/1/relationships/comments_hyperlinked"
+                            "self": "http://testserver/entries/1/relationships/comments_hyperlinked",  # noqa: B950
                         }
                     },
                     "suggested": {
                         "data": [],
                         "links": {
                             "related": "http://testserver/entries/1/suggested/",
-                            "self": "http://testserver/entries/1/relationships/suggested"
-                        }
+                            "self": "http://testserver/entries/1/relationships/suggested",
+                        },
+                        "meta": {"count": 0},
                     },
                     "suggestedHyperlinked": {
                         "links": {
                             "related": "http://testserver/entries/1/suggested/",
                             "self": "http://testserver/entries/1"
-                                    "/relationships/suggested_hyperlinked"
+                            "/relationships/suggested_hyperlinked",
                         }
                     },
                     "featuredHyperlinked": {
                         "links": {
                             "related": "http://testserver/entries/1/featured",
-                            "self": "http://testserver/entries/1/relationships/featured_hyperlinked"
+                            "self": "http://testserver/entries/1/relationships/featured_hyperlinked",  # noqa: B950
                         }
                     },
                     "tags": {
-                        'meta': {'count': 1},
-                        "data": [
-                            {
-                                "id": "1",
-                                "type": "taggedItems"
-                            }
-                        ]
-                    }
-                }
-            }],
+                        "meta": {"count": 1},
+                        "data": [{"id": "1", "type": "taggedItems"}],
+                    },
+                },
+            }
+        ],
         "links": {
-            'first': 'http://testserver/entries?page%5Bnumber%5D=1',
-            'last': 'http://testserver/entries?page%5Bnumber%5D=1',
+            "first": "http://testserver/entries?page%5Bnumber%5D=1",
+            "last": "http://testserver/entries?page%5Bnumber%5D=1",
             "next": None,
             "prev": None,
         },
-        "meta":
-        {
-            "pagination":
-            {
-                "page": 1,
-                "pages": 1,
-                "count": 1
-            }
-        }
+        "meta": {"pagination": {"page": 1, "pages": 1, "count": 1}},
     }
 
     response = client.get(reverse("entry-list"))
diff --git a/example/tests/integration/test_polymorphism.py b/example/tests/integration/test_polymorphism.py
index 5c64776a..836a9046 100644
--- a/example/tests/integration/test_polymorphism.py
+++ b/example/tests/integration/test_polymorphism.py
@@ -10,153 +10,154 @@
 
 
 def test_polymorphism_on_detail(single_art_project, client):
-    response = client.get(reverse("project-detail", kwargs={'pk': single_art_project.pk}))
+    response = client.get(
+        reverse("project-detail", kwargs={"pk": single_art_project.pk})
+    )
     content = response.json()
     assert content["data"]["type"] == "artProjects"
 
 
 def test_polymorphism_on_detail_relations(single_company, client):
-    response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk}))
+    response = client.get(reverse("company-detail", kwargs={"pk": single_company.pk}))
     content = response.json()
-    assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects"
     assert (
-        set([rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]]) ==
-        set(["researchProjects", "artProjects"])
+        content["data"]["relationships"]["currentProject"]["data"]["type"]
+        == "artProjects"
     )
+    assert {
+        rel["type"]
+        for rel in content["data"]["relationships"]["futureProjects"]["data"]
+    } == {"researchProjects", "artProjects"}
 
 
 def test_polymorphism_on_included_relations(single_company, client):
     response = client.get(
-        reverse("company-detail", kwargs={'pk': single_company.pk}) +
-        '?include=current_project,future_projects,current_art_project,current_research_project')
+        reverse("company-detail", kwargs={"pk": single_company.pk})
+        + "?include=current_project,future_projects,current_art_project,current_research_project"  # noqa: B950
+    )
     content = response.json()
-    assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects"
-    assert content["data"]["relationships"]["currentArtProject"]["data"]["type"] == "artProjects"
-    assert content["data"]["relationships"]["currentResearchProject"]["data"] is None
     assert (
-        set([rel["type"] for rel in content["data"]["relationships"]["futureProjects"]["data"]]) ==
-        set(["researchProjects", "artProjects"])
+        content["data"]["relationships"]["currentProject"]["data"]["type"]
+        == "artProjects"
     )
-    assert set([x.get('type') for x in content.get('included')]) == set([
-        'artProjects', 'artProjects', 'researchProjects']), 'Detail included types are incorrect'
+    assert (
+        content["data"]["relationships"]["currentArtProject"]["data"]["type"]
+        == "artProjects"
+    )
+    assert content["data"]["relationships"]["currentResearchProject"]["data"] is None
+    assert {
+        rel["type"]
+        for rel in content["data"]["relationships"]["futureProjects"]["data"]
+    } == {"researchProjects", "artProjects"}
+    assert {x.get("type") for x in content.get("included")} == {
+        "artProjects",
+        "researchProjects",
+    }, "Detail included types are incorrect"
     # Ensure that the child fields are present.
-    assert content.get('included')[0].get('attributes').get('artist') is not None
-    assert content.get('included')[1].get('attributes').get('artist') is not None
-    assert content.get('included')[2].get('attributes').get('supervisor') is not None
+    assert content.get("included")[0].get("attributes").get("artist") is not None
+    assert content.get("included")[1].get("attributes").get("artist") is not None
+    assert content.get("included")[2].get("attributes").get("supervisor") is not None
 
 
 def test_polymorphism_on_polymorphic_model_detail_patch(single_art_project, client):
-    url = reverse("project-detail", kwargs={'pk': single_art_project.pk})
+    url = reverse("project-detail", kwargs={"pk": single_art_project.pk})
     response = client.get(url)
     content = response.json()
-    test_topic = 'test-{}'.format(random.randint(0, 999999))
-    test_artist = 'test-{}'.format(random.randint(0, 999999))
-    content['data']['attributes']['topic'] = test_topic
-    content['data']['attributes']['artist'] = test_artist
+    test_topic = f"test-{random.randint(0, 999999)}"
+    test_artist = f"test-{random.randint(0, 999999)}"
+    content["data"]["attributes"]["topic"] = test_topic
+    content["data"]["attributes"]["artist"] = test_artist
     response = client.patch(url, data=content)
     new_content = response.json()
-    assert new_content['data']['type'] == "artProjects"
-    assert new_content['data']['attributes']['topic'] == test_topic
-    assert new_content['data']['attributes']['artist'] == test_artist
+    assert new_content["data"]["type"] == "artProjects"
+    assert new_content["data"]["attributes"]["topic"] == test_topic
+    assert new_content["data"]["attributes"]["artist"] == test_artist
 
 
-def test_patch_on_polymorphic_model_without_including_required_field(single_art_project, client):
-    url = reverse("project-detail", kwargs={'pk': single_art_project.pk})
+def test_patch_on_polymorphic_model_without_including_required_field(
+    single_art_project, client
+):
+    url = reverse("project-detail", kwargs={"pk": single_art_project.pk})
     data = {
-        'data': {
-            'id': single_art_project.pk,
-            'type': 'artProjects',
-            'attributes': {
-                'description': 'New description'
-            }
+        "data": {
+            "id": single_art_project.pk,
+            "type": "artProjects",
+            "attributes": {"description": "New description"},
         }
     }
     response = client.patch(url, data)
     assert response.status_code == status.HTTP_200_OK
-    assert response.json()['data']['attributes']['description'] == 'New description'
+    assert response.json()["data"]["attributes"]["description"] == "New description"
 
 
 def test_polymorphism_on_polymorphic_model_list_post(client):
-    test_topic = 'New test topic {}'.format(random.randint(0, 999999))
-    test_artist = 'test-{}'.format(random.randint(0, 999999))
+    test_topic = f"New test topic {random.randint(0, 999999)}"
+    test_artist = f"test-{random.randint(0, 999999)}"
     test_project_type = ProjectTypeFactory()
-    url = reverse('project-list')
+    url = reverse("project-list")
     data = {
-        'data': {
-            'type': 'artProjects',
-            'attributes': {
-                'topic': test_topic,
-                'artist': test_artist
-            },
-            'relationships': {
-                'projectType': {
-                    'data': {
-                        'type': 'projectTypes',
-                        'id': test_project_type.pk
-                    }
+        "data": {
+            "type": "artProjects",
+            "attributes": {"topic": test_topic, "artist": test_artist},
+            "relationships": {
+                "projectType": {
+                    "data": {"type": "projectTypes", "id": test_project_type.pk}
                 }
-            }
+            },
         }
     }
     response = client.post(url, data=data)
     content = response.json()
-    assert content['data']['id'] is not None
-    assert content['data']['type'] == "artProjects"
-    assert content['data']['attributes']['topic'] == test_topic
-    assert content['data']['attributes']['artist'] == test_artist
-    assert content['data']['relationships']['projectType']['data']['id'] == \
-        str(test_project_type.pk)
+    assert content["data"]["id"] is not None
+    assert content["data"]["type"] == "artProjects"
+    assert content["data"]["attributes"]["topic"] == test_topic
+    assert content["data"]["attributes"]["artist"] == test_artist
+    assert content["data"]["relationships"]["projectType"]["data"]["id"] == str(
+        test_project_type.pk
+    )
 
 
 def test_polymorphism_on_polymorphic_model_w_included_serializers(client):
     test_project = ArtProjectFactory()
-    query = '?include=projectType'
-    url = reverse('project-list')
+    query = "?include=projectType"
+    url = reverse("project-list")
     response = client.get(url + query)
     content = response.json()
-    assert content['data'][0]['id'] == str(test_project.pk)
-    assert content['data'][0]['type'] == 'artProjects'
-    assert content['data'][0]['relationships']['projectType']['data']['id'] == \
-        str(test_project.project_type.pk)
-    assert content['included'][0]['type'] == 'projectTypes'
-    assert content['included'][0]['id'] == str(test_project.project_type.pk)
+    assert content["data"][0]["id"] == str(test_project.pk)
+    assert content["data"][0]["type"] == "artProjects"
+    assert content["data"][0]["relationships"]["projectType"]["data"]["id"] == str(
+        test_project.project_type.pk
+    )
+    assert content["included"][0]["type"] == "projectTypes"
+    assert content["included"][0]["id"] == str(test_project.project_type.pk)
 
 
 def test_polymorphic_model_without_any_instance(client):
     expected = {
         "links": {
-            'first': 'http://testserver/projects?page%5Bnumber%5D=1',
-            'last': 'http://testserver/projects?page%5Bnumber%5D=1',
+            "first": "http://testserver/projects?page%5Bnumber%5D=1",
+            "last": "http://testserver/projects?page%5Bnumber%5D=1",
             "next": None,
-            "prev": None
+            "prev": None,
         },
         "data": [],
-        "meta": {
-            "pagination": {
-                "page": 1,
-                "pages": 1,
-                "count": 0
-            }
-        }
+        "meta": {"pagination": {"page": 1, "pages": 1, "count": 0}},
     }
 
-    response = client.get(reverse('project-list'))
+    response = client.get(reverse("project-list"))
     assert response.status_code == 200
     content = response.json()
     assert expected == content
 
 
 def test_invalid_type_on_polymorphic_model(client):
-    test_topic = 'New test topic {}'.format(random.randint(0, 999999))
-    test_artist = 'test-{}'.format(random.randint(0, 999999))
-    url = reverse('project-list')
+    test_topic = f"New test topic {random.randint(0, 999999)}"
+    test_artist = f"test-{random.randint(0, 999999)}"
+    url = reverse("project-list")
     data = {
-        'data': {
-            'type': 'invalidProjects',
-            'attributes': {
-                'topic': test_topic,
-                'artist': test_artist
-            }
+        "data": {
+            "type": "invalidProjects",
+            "attributes": {"topic": test_topic, "artist": test_artist},
         }
     }
     response = client.post(url, data=data)
@@ -165,72 +166,103 @@ def test_invalid_type_on_polymorphic_model(client):
     assert len(content["errors"]) == 1
     assert content["errors"][0]["status"] == "409"
     try:
-        assert content["errors"][0]["detail"] == \
-            "The resource object's type (invalidProjects) is not the type that constitute the " \
+        assert (
+            content["errors"][0]["detail"]
+            == "The resource object's type (invalidProjects) is not the type that constitute the "  # noqa: B950
             "collection represented by the endpoint (one of [researchProjects, artProjects])."
+        )
     except AssertionError:
         # Available type list order isn't enforced
-        assert content["errors"][0]["detail"] == \
-            "The resource object's type (invalidProjects) is not the type that constitute the " \
+        assert (
+            content["errors"][0]["detail"]
+            == "The resource object's type (invalidProjects) is not the type that constitute the "  # noqa: B950
             "collection represented by the endpoint (one of [artProjects, researchProjects])."
+        )
 
 
-def test_polymorphism_relations_update(single_company, research_project_factory, client):
-    response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk}))
+def test_polymorphism_relations_update(
+    single_company, research_project_factory, client
+):
+    response = client.get(reverse("company-detail", kwargs={"pk": single_company.pk}))
     content = response.json()
-    assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects"
+    assert (
+        content["data"]["relationships"]["currentProject"]["data"]["type"]
+        == "artProjects"
+    )
 
     research_project = research_project_factory()
     content["data"]["relationships"]["currentProject"]["data"] = {
         "type": "researchProjects",
-        "id": research_project.pk
+        "id": research_project.pk,
     }
-    response = client.patch(reverse("company-detail", kwargs={'pk': single_company.pk}),
-                            data=content)
+    response = client.patch(
+        reverse("company-detail", kwargs={"pk": single_company.pk}), data=content
+    )
     assert response.status_code == 200
     content = response.json()
-    assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "researchProjects"
-    assert int(content["data"]["relationships"]["currentProject"]["data"]["id"]) == \
-        research_project.pk
+    assert (
+        content["data"]["relationships"]["currentProject"]["data"]["type"]
+        == "researchProjects"
+    )
+    assert (
+        int(content["data"]["relationships"]["currentProject"]["data"]["id"])
+        == research_project.pk
+    )
 
 
-def test_polymorphism_relations_put_405(single_company, research_project_factory, client):
-    response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk}))
+def test_polymorphism_relations_put_405(
+    single_company, research_project_factory, client
+):
+    response = client.get(reverse("company-detail", kwargs={"pk": single_company.pk}))
     content = response.json()
-    assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects"
+    assert (
+        content["data"]["relationships"]["currentProject"]["data"]["type"]
+        == "artProjects"
+    )
 
     research_project = research_project_factory()
     content["data"]["relationships"]["currentProject"]["data"] = {
         "type": "researchProjects",
-        "id": research_project.pk
+        "id": research_project.pk,
     }
-    response = client.put(reverse("company-detail", kwargs={'pk': single_company.pk}),
-                          data=content)
+    response = client.put(
+        reverse("company-detail", kwargs={"pk": single_company.pk}), data=content
+    )
     assert response.status_code == 405
 
 
-def test_invalid_type_on_polymorphic_relation(single_company, research_project_factory, client):
-    response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk}))
+def test_invalid_type_on_polymorphic_relation(
+    single_company, research_project_factory, client
+):
+    response = client.get(reverse("company-detail", kwargs={"pk": single_company.pk}))
     content = response.json()
-    assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects"
+    assert (
+        content["data"]["relationships"]["currentProject"]["data"]["type"]
+        == "artProjects"
+    )
 
     research_project = research_project_factory()
     content["data"]["relationships"]["currentProject"]["data"] = {
         "type": "invalidProjects",
-        "id": research_project.pk
+        "id": research_project.pk,
     }
-    response = client.patch(reverse("company-detail", kwargs={'pk': single_company.pk}),
-                            data=content)
+    response = client.patch(
+        reverse("company-detail", kwargs={"pk": single_company.pk}), data=content
+    )
     assert response.status_code == 409
     content = response.json()
     assert len(content["errors"]) == 1
     assert content["errors"][0]["status"] == "409"
     try:
-        assert content["errors"][0]["detail"] == \
-            "Incorrect relation type. Expected one of [researchProjects, artProjects], " \
+        assert (
+            content["errors"][0]["detail"]
+            == "Incorrect relation type. Expected one of [researchProjects, artProjects], "
             "received invalidProjects."
+        )
     except AssertionError:
         # Available type list order isn't enforced
-        assert content["errors"][0]["detail"] == \
-            "Incorrect relation type. Expected one of [artProjects, researchProjects], " \
+        assert (
+            content["errors"][0]["detail"]
+            == "Incorrect relation type. Expected one of [artProjects, researchProjects], "
             "received invalidProjects."
+        )
diff --git a/example/tests/integration/test_sparse_fieldsets.py b/example/tests/integration/test_sparse_fieldsets.py
index 83b8560a..cf9cee20 100644
--- a/example/tests/integration/test_sparse_fieldsets.py
+++ b/example/tests/integration/test_sparse_fieldsets.py
@@ -6,18 +6,21 @@
 
 
 def test_sparse_fieldset_valid_fields(client, entry):
-    base_url = reverse('entry-list')
-    response = client.get(base_url, data={'fields[entries]': 'blog,headline'})
+    base_url = reverse("entry-list")
+    response = client.get(base_url, data={"fields[entries]": "blog,headline"})
     assert response.status_code == status.HTTP_200_OK
-    data = response.json()['data']
+    data = response.json()["data"]
 
     assert len(data) == 1
     entry = data[0]
-    assert entry['attributes'].keys() == {'headline'}
-    assert entry['relationships'].keys() == {'blog'}
+    assert entry["attributes"].keys() == {"headline"}
+    assert entry["relationships"].keys() == {"blog"}
+    assert "meta" not in entry
 
 
-@pytest.mark.parametrize("fields_param", ['invalidfields[entries]', 'fieldsinvalid[entries'])
+@pytest.mark.parametrize(
+    "fields_param", ["invalidfields[entries]", "fieldsinvalid[entries"]
+)
 def test_sparse_fieldset_invalid_fields_parameter(client, entry, fields_param):
     """
     Test that invalid fields query parameter is not processed by sparse fieldset.
@@ -25,12 +28,12 @@ def test_sparse_fieldset_invalid_fields_parameter(client, entry, fields_param):
     rest_framework_json_api.filters.QueryParameterValidationFilter takes care of error
     handling in such a case.
     """
-    base_url = reverse('entry-list')
-    response = client.get(base_url, data={'invalidfields[entries]': 'blog,headline'})
+    base_url = reverse("entry-list")
+    response = client.get(base_url, data={"invalidfields[entries]": "blog,headline"})
     assert response.status_code == status.HTTP_200_OK
-    data = response.json()['data']
+    data = response.json()["data"]
 
     assert len(data) == 1
     entry = data[0]
-    assert entry['attributes'].keys() != {'headline'}
-    assert entry['relationships'].keys() != {'blog'}
+    assert entry["attributes"].keys() != {"headline"}
+    assert entry["relationships"].keys() != {"blog"}
diff --git a/example/tests/snapshots/snap_test_errors.py b/example/tests/snapshots/snap_test_errors.py
deleted file mode 100644
index 48b8e474..00000000
--- a/example/tests/snapshots/snap_test_errors.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# -*- coding: utf-8 -*-
-# snapshottest: v1 - https://goo.gl/zC4yUc
-from __future__ import unicode_literals
-
-from snapshottest import Snapshot
-
-
-snapshots = Snapshot()
-
-snapshots['test_first_level_attribute_error 1'] = {
-    'errors': [
-        {
-            'code': 'required',
-            'detail': 'This field is required.',
-            'source': {
-                'pointer': '/data/attributes/headline'
-            },
-            'status': '400'
-        }
-    ]
-}
-
-snapshots['test_first_level_custom_attribute_error 1'] = {
-    'errors': [
-        {
-            'detail': 'Too short',
-            'source': {
-                'pointer': '/data/attributes/body-text'
-            },
-            'title': 'Too Short title'
-        }
-    ]
-}
-
-snapshots['test_many_third_level_dict_errors 1'] = {
-    'errors': [
-        {
-            'code': 'required',
-            'detail': 'This field is required.',
-            'source': {
-                'pointer': '/data/attributes/comments/0/attachment/data'
-            },
-            'status': '400'
-        },
-        {
-            'code': 'required',
-            'detail': 'This field is required.',
-            'source': {
-                'pointer': '/data/attributes/comments/0/body'
-            },
-            'status': '400'
-        }
-    ]
-}
-
-snapshots['test_second_level_array_error 1'] = {
-    'errors': [
-        {
-            'code': 'required',
-            'detail': 'This field is required.',
-            'source': {
-                'pointer': '/data/attributes/comments/0/body'
-            },
-            'status': '400'
-        }
-    ]
-}
-
-snapshots['test_second_level_dict_error 1'] = {
-    'errors': [
-        {
-            'code': 'required',
-            'detail': 'This field is required.',
-            'source': {
-                'pointer': '/data/attributes/comment/body'
-            },
-            'status': '400'
-        }
-    ]
-}
-
-snapshots['test_third_level_array_error 1'] = {
-    'errors': [
-        {
-            'code': 'required',
-            'detail': 'This field is required.',
-            'source': {
-                'pointer': '/data/attributes/comments/0/attachments/0/data'
-            },
-            'status': '400'
-        }
-    ]
-}
-
-snapshots['test_third_level_custom_array_error 1'] = {
-    'errors': [
-        {
-            'code': 'invalid',
-            'detail': 'Too short data',
-            'source': {
-                'pointer': '/data/attributes/comments/0/attachments/0/data'
-            },
-            'status': '400'
-        }
-    ]
-}
-
-snapshots['test_third_level_dict_error 1'] = {
-    'errors': [
-        {
-            'code': 'required',
-            'detail': 'This field is required.',
-            'source': {
-                'pointer': '/data/attributes/comments/0/attachment/data'
-            },
-            'status': '400'
-        }
-    ]
-}
diff --git a/example/tests/snapshots/snap_test_openapi.py b/example/tests/snapshots/snap_test_openapi.py
deleted file mode 100644
index ec8da388..00000000
--- a/example/tests/snapshots/snap_test_openapi.py
+++ /dev/null
@@ -1,647 +0,0 @@
-# -*- coding: utf-8 -*-
-# snapshottest: v1 - https://goo.gl/zC4yUc
-from __future__ import unicode_literals
-
-from snapshottest import Snapshot
-
-
-snapshots = Snapshot()
-
-snapshots['test_path_without_parameters 1'] = '''{
-  "description": "",
-  "operationId": "List/authors/",
-  "parameters": [
-    {
-      "$ref": "#/components/parameters/include"
-    },
-    {
-      "$ref": "#/components/parameters/fields"
-    },
-    {
-      "$ref": "#/components/parameters/sort"
-    },
-    {
-      "description": "A page number within the paginated result set.",
-      "in": "query",
-      "name": "page[number]",
-      "required": false,
-      "schema": {
-        "type": "integer"
-      }
-    },
-    {
-      "description": "Number of results to return per page.",
-      "in": "query",
-      "name": "page[size]",
-      "required": false,
-      "schema": {
-        "type": "integer"
-      }
-    },
-    {
-      "description": "Which field to use when ordering the results.",
-      "in": "query",
-      "name": "sort",
-      "required": false,
-      "schema": {
-        "type": "string"
-      }
-    },
-    {
-      "description": "A search term.",
-      "in": "query",
-      "name": "filter[search]",
-      "required": false,
-      "schema": {
-        "type": "string"
-      }
-    }
-  ],
-  "responses": {
-    "200": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "properties": {
-              "data": {
-                "items": {
-                  "$ref": "#/components/schemas/Author"
-                },
-                "type": "array"
-              },
-              "included": {
-                "items": {
-                  "$ref": "#/components/schemas/resource"
-                },
-                "type": "array",
-                "uniqueItems": true
-              },
-              "jsonapi": {
-                "$ref": "#/components/schemas/jsonapi"
-              },
-              "links": {
-                "allOf": [
-                  {
-                    "$ref": "#/components/schemas/links"
-                  },
-                  {
-                    "$ref": "#/components/schemas/pagination"
-                  }
-                ],
-                "description": "Link members related to primary data"
-              }
-            },
-            "required": [
-              "data"
-            ],
-            "type": "object"
-          }
-        }
-      },
-      "description": "List/authors/"
-    },
-    "401": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "not authorized"
-    },
-    "404": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "not found"
-    }
-  }
-}'''
-
-snapshots['test_path_with_id_parameter 1'] = '''{
-  "description": "",
-  "operationId": "retrieve/authors/{id}/",
-  "parameters": [
-    {
-      "description": "A unique integer value identifying this author.",
-      "in": "path",
-      "name": "id",
-      "required": true,
-      "schema": {
-        "type": "string"
-      }
-    },
-    {
-      "$ref": "#/components/parameters/include"
-    },
-    {
-      "$ref": "#/components/parameters/fields"
-    },
-    {
-      "$ref": "#/components/parameters/sort"
-    },
-    {
-      "description": "Which field to use when ordering the results.",
-      "in": "query",
-      "name": "sort",
-      "required": false,
-      "schema": {
-        "type": "string"
-      }
-    },
-    {
-      "description": "A search term.",
-      "in": "query",
-      "name": "filter[search]",
-      "required": false,
-      "schema": {
-        "type": "string"
-      }
-    }
-  ],
-  "responses": {
-    "200": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "properties": {
-              "data": {
-                "$ref": "#/components/schemas/Author"
-              },
-              "included": {
-                "items": {
-                  "$ref": "#/components/schemas/resource"
-                },
-                "type": "array",
-                "uniqueItems": true
-              },
-              "jsonapi": {
-                "$ref": "#/components/schemas/jsonapi"
-              },
-              "links": {
-                "allOf": [
-                  {
-                    "$ref": "#/components/schemas/links"
-                  },
-                  {
-                    "$ref": "#/components/schemas/pagination"
-                  }
-                ],
-                "description": "Link members related to primary data"
-              }
-            },
-            "required": [
-              "data"
-            ],
-            "type": "object"
-          }
-        }
-      },
-      "description": "retrieve/authors/{id}/"
-    },
-    "401": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "not authorized"
-    },
-    "404": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "not found"
-    }
-  }
-}'''
-
-snapshots['test_post_request 1'] = '''{
-  "description": "",
-  "operationId": "create/authors/",
-  "parameters": [],
-  "requestBody": {
-    "content": {
-      "application/vnd.api+json": {
-        "schema": {
-          "properties": {
-            "data": {
-              "additionalProperties": false,
-              "properties": {
-                "attributes": {
-                  "properties": {
-                    "defaults": {
-                      "default": "default",
-                      "description": "help for defaults",
-                      "maxLength": 20,
-                      "minLength": 3,
-                      "type": "string",
-                      "writeOnly": true
-                    },
-                    "email": {
-                      "format": "email",
-                      "maxLength": 254,
-                      "type": "string"
-                    },
-                    "name": {
-                      "maxLength": 50,
-                      "type": "string"
-                    }
-                  },
-                  "required": [
-                    "name",
-                    "email"
-                  ],
-                  "type": "object"
-                },
-                "id": {
-                  "$ref": "#/components/schemas/id"
-                },
-                "links": {
-                  "properties": {
-                    "self": {
-                      "$ref": "#/components/schemas/link"
-                    }
-                  },
-                  "type": "object"
-                },
-                "relationships": {
-                  "properties": {
-                    "bio": {
-                      "$ref": "#/components/schemas/reltoone"
-                    },
-                    "comments": {
-                      "$ref": "#/components/schemas/reltomany"
-                    },
-                    "entries": {
-                      "$ref": "#/components/schemas/reltomany"
-                    },
-                    "first_entry": {
-                      "$ref": "#/components/schemas/reltoone"
-                    },
-                    "type": {
-                      "$ref": "#/components/schemas/reltoone"
-                    }
-                  },
-                  "type": "object"
-                },
-                "type": {
-                  "$ref": "#/components/schemas/type"
-                }
-              },
-              "required": [
-                "type"
-              ],
-              "type": "object"
-            }
-          },
-          "required": [
-            "data"
-          ]
-        }
-      }
-    }
-  },
-  "responses": {
-    "201": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "properties": {
-              "data": {
-                "$ref": "#/components/schemas/Author"
-              },
-              "included": {
-                "items": {
-                  "$ref": "#/components/schemas/resource"
-                },
-                "type": "array",
-                "uniqueItems": true
-              },
-              "jsonapi": {
-                "$ref": "#/components/schemas/jsonapi"
-              },
-              "links": {
-                "allOf": [
-                  {
-                    "$ref": "#/components/schemas/links"
-                  },
-                  {
-                    "$ref": "#/components/schemas/pagination"
-                  }
-                ],
-                "description": "Link members related to primary data"
-              }
-            },
-            "required": [
-              "data"
-            ],
-            "type": "object"
-          }
-        }
-      },
-      "description": "[Created](https://jsonapi.org/format/#crud-creating-responses-201). Assigned `id` and/or any other changes are in this response."
-    },
-    "202": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/datum"
-          }
-        }
-      },
-      "description": "Accepted for [asynchronous processing](https://jsonapi.org/recommendations/#asynchronous-processing)"
-    },
-    "204": {
-      "description": "[Created](https://jsonapi.org/format/#crud-creating-responses-204) with the supplied `id`. No other changes from what was POSTed."
-    },
-    "401": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "not authorized"
-    },
-    "403": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "[Forbidden](https://jsonapi.org/format/#crud-creating-responses-403)"
-    },
-    "404": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "[Related resource does not exist](https://jsonapi.org/format/#crud-creating-responses-404)"
-    },
-    "409": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "[Conflict](https://jsonapi.org/format/#crud-creating-responses-409)"
-    }
-  }
-}'''
-
-snapshots['test_patch_request 1'] = '''{
-  "description": "",
-  "operationId": "update/authors/{id}",
-  "parameters": [
-    {
-      "description": "A unique integer value identifying this author.",
-      "in": "path",
-      "name": "id",
-      "required": true,
-      "schema": {
-        "type": "string"
-      }
-    }
-  ],
-  "requestBody": {
-    "content": {
-      "application/vnd.api+json": {
-        "schema": {
-          "properties": {
-            "data": {
-              "additionalProperties": false,
-              "properties": {
-                "attributes": {
-                  "properties": {
-                    "defaults": {
-                      "default": "default",
-                      "description": "help for defaults",
-                      "maxLength": 20,
-                      "minLength": 3,
-                      "type": "string",
-                      "writeOnly": true
-                    },
-                    "email": {
-                      "format": "email",
-                      "maxLength": 254,
-                      "type": "string"
-                    },
-                    "name": {
-                      "maxLength": 50,
-                      "type": "string"
-                    }
-                  },
-                  "type": "object"
-                },
-                "id": {
-                  "$ref": "#/components/schemas/id"
-                },
-                "links": {
-                  "properties": {
-                    "self": {
-                      "$ref": "#/components/schemas/link"
-                    }
-                  },
-                  "type": "object"
-                },
-                "relationships": {
-                  "properties": {
-                    "bio": {
-                      "$ref": "#/components/schemas/reltoone"
-                    },
-                    "comments": {
-                      "$ref": "#/components/schemas/reltomany"
-                    },
-                    "entries": {
-                      "$ref": "#/components/schemas/reltomany"
-                    },
-                    "first_entry": {
-                      "$ref": "#/components/schemas/reltoone"
-                    },
-                    "type": {
-                      "$ref": "#/components/schemas/reltoone"
-                    }
-                  },
-                  "type": "object"
-                },
-                "type": {
-                  "$ref": "#/components/schemas/type"
-                }
-              },
-              "required": [
-                "type",
-                "id"
-              ],
-              "type": "object"
-            }
-          },
-          "required": [
-            "data"
-          ]
-        }
-      }
-    }
-  },
-  "responses": {
-    "200": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "properties": {
-              "data": {
-                "$ref": "#/components/schemas/Author"
-              },
-              "included": {
-                "items": {
-                  "$ref": "#/components/schemas/resource"
-                },
-                "type": "array",
-                "uniqueItems": true
-              },
-              "jsonapi": {
-                "$ref": "#/components/schemas/jsonapi"
-              },
-              "links": {
-                "allOf": [
-                  {
-                    "$ref": "#/components/schemas/links"
-                  },
-                  {
-                    "$ref": "#/components/schemas/pagination"
-                  }
-                ],
-                "description": "Link members related to primary data"
-              }
-            },
-            "required": [
-              "data"
-            ],
-            "type": "object"
-          }
-        }
-      },
-      "description": "update/authors/{id}"
-    },
-    "401": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "not authorized"
-    },
-    "403": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "[Forbidden](https://jsonapi.org/format/#crud-updating-responses-403)"
-    },
-    "404": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "[Related resource does not exist](https://jsonapi.org/format/#crud-updating-responses-404)"
-    },
-    "409": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "[Conflict]([Conflict](https://jsonapi.org/format/#crud-updating-responses-409)"
-    }
-  }
-}'''
-
-snapshots['test_delete_request 1'] = '''{
-  "description": "",
-  "operationId": "destroy/authors/{id}",
-  "parameters": [
-    {
-      "description": "A unique integer value identifying this author.",
-      "in": "path",
-      "name": "id",
-      "required": true,
-      "schema": {
-        "type": "string"
-      }
-    }
-  ],
-  "responses": {
-    "200": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/onlymeta"
-          }
-        }
-      },
-      "description": "[OK](https://jsonapi.org/format/#crud-deleting-responses-200)"
-    },
-    "202": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/datum"
-          }
-        }
-      },
-      "description": "Accepted for [asynchronous processing](https://jsonapi.org/recommendations/#asynchronous-processing)"
-    },
-    "204": {
-      "description": "[no content](https://jsonapi.org/format/#crud-deleting-responses-204)"
-    },
-    "401": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "not authorized"
-    },
-    "404": {
-      "content": {
-        "application/vnd.api+json": {
-          "schema": {
-            "$ref": "#/components/schemas/failure"
-          }
-        }
-      },
-      "description": "[Resource does not exist](https://jsonapi.org/format/#crud-deleting-responses-404)"
-    }
-  }
-}'''
diff --git a/example/tests/test_errors.py b/example/tests/test_errors.py
index ff2e8f95..72e742c7 100644
--- a/example/tests/test_errors.py
+++ b/example/tests/test_errors.py
@@ -1,11 +1,11 @@
 import pytest
 from django.test import override_settings
 from django.urls import path, reverse
-from rest_framework import views
+from rest_framework import generics
 
 from rest_framework_json_api import serializers
 
-from example.models import Blog
+from example.models import Author, Blog
 
 
 # serializers
@@ -14,7 +14,7 @@ class CommentAttachmentSerializer(serializers.Serializer):
 
     def validate_data(self, value):
         if value and len(value) < 10:
-            raise serializers.ValidationError('Too short data')
+            raise serializers.ValidationError("Too short data")
 
 
 class CommentSerializer(serializers.Serializer):
@@ -30,41 +30,43 @@ class EntrySerializer(serializers.Serializer):
     comment = CommentSerializer(required=False)
     headline = serializers.CharField(allow_null=True, required=True)
     body_text = serializers.CharField()
+    main_author = serializers.ResourceRelatedField(
+        queryset=Author.objects.all(), required=False
+    )
+    authors = serializers.ResourceRelatedField(
+        queryset=Author.objects.all(), required=False, many=True
+    )
 
     def validate(self, attrs):
-        body_text = attrs['body_text']
+        body_text = attrs["body_text"]
         if len(body_text) < 5:
-            raise serializers.ValidationError({'body_text': {
-                'title': 'Too Short title', 'detail': 'Too short'}
-            })
+            raise serializers.ValidationError(
+                {"body_text": {"title": "Too Short title", "detail": "Too short"}}
+            )
 
 
 # view
-class DummyTestView(views.APIView):
+class DummyTestView(generics.CreateAPIView):
     serializer_class = EntrySerializer
-    resource_name = 'entries'
+    resource_name = "entries"
 
-    def post(self, request, *args, **kwargs):
-        serializer = self.serializer_class(data=request.data)
-        serializer.is_valid(raise_exception=True)
+    def get_serializer_context(self):
+        return {}
 
 
 urlpatterns = [
-    path('entries-nested', DummyTestView.as_view(),
-         name='entries-nested-list')
+    path("entries-nested", DummyTestView.as_view(), name="entries-nested-list")
 ]
 
 
-@pytest.fixture(scope='function')
+@pytest.fixture(scope="function")
 def some_blog(db):
-    return Blog.objects.create(name='Some Blog', tagline="It's a blog")
+    return Blog.objects.create(name="Some Blog", tagline="It's a blog")
 
 
 def perform_error_test(client, data):
-    with override_settings(
-            ROOT_URLCONF=__name__
-    ):
-        url = reverse('entries-nested-list')
+    with override_settings(ROOT_URLCONF=__name__):
+        url = reverse("entries-nested-list")
         response = client.post(url, data=data)
 
     return response.json()
@@ -72,152 +74,190 @@ def perform_error_test(client, data):
 
 def test_first_level_attribute_error(client, some_blog, snapshot):
     data = {
-        'data': {
-            'type': 'entries',
-            'attributes': {
-                'blog': some_blog.pk,
-                'bodyText': 'body_text',
-            }
+        "data": {
+            "type": "entries",
+            "attributes": {
+                "blog": some_blog.pk,
+                "bodyText": "body_text",
+            },
         }
     }
-    snapshot.assert_match(perform_error_test(client, data))
+    assert snapshot == perform_error_test(client, data)
 
 
 def test_first_level_custom_attribute_error(client, some_blog, snapshot):
     data = {
-        'data': {
-            'type': 'entries',
-            'attributes': {
-                'blog': some_blog.pk,
-                'body-text': 'body',
-                'headline': 'headline'
-            }
+        "data": {
+            "type": "entries",
+            "attributes": {
+                "blog": some_blog.pk,
+                "body-text": "body",
+                "headline": "headline",
+            },
         }
     }
-    with override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize'):
-        snapshot.assert_match(perform_error_test(client, data))
+    with override_settings(JSON_API_FORMAT_FIELD_NAMES="dasherize"):
+        assert snapshot == perform_error_test(client, data)
 
 
 def test_second_level_array_error(client, some_blog, snapshot):
     data = {
-        'data': {
-            'type': 'entries',
-            'attributes': {
-                'blog': some_blog.pk,
-                'bodyText': 'body_text',
-                'headline': 'headline',
-                'comments': [
-                    {
-                    }
-                ]
-            }
+        "data": {
+            "type": "entries",
+            "attributes": {
+                "blog": some_blog.pk,
+                "bodyText": "body_text",
+                "headline": "headline",
+                "comments": [{}],
+            },
         }
     }
 
-    snapshot.assert_match(perform_error_test(client, data))
+    assert snapshot == perform_error_test(client, data)
 
 
 def test_second_level_dict_error(client, some_blog, snapshot):
     data = {
-        'data': {
-            'type': 'entries',
-            'attributes': {
-                'blog': some_blog.pk,
-                'bodyText': 'body_text',
-                'headline': 'headline',
-                'comment': {}
-            }
+        "data": {
+            "type": "entries",
+            "attributes": {
+                "blog": some_blog.pk,
+                "bodyText": "body_text",
+                "headline": "headline",
+                "comment": {},
+            },
         }
     }
 
-    snapshot.assert_match(perform_error_test(client, data))
+    assert snapshot == perform_error_test(client, data)
 
 
 def test_third_level_array_error(client, some_blog, snapshot):
     data = {
-        'data': {
-            'type': 'entries',
-            'attributes': {
-                'blog': some_blog.pk,
-                'bodyText': 'body_text',
-                'headline': 'headline',
-                'comments': [
-                    {
-                        'body': 'test comment',
-                        'attachments': [
-                            {
-                            }
-                        ]
-                    }
-                ]
-            }
+        "data": {
+            "type": "entries",
+            "attributes": {
+                "blog": some_blog.pk,
+                "bodyText": "body_text",
+                "headline": "headline",
+                "comments": [{"body": "test comment", "attachments": [{}]}],
+            },
         }
     }
 
-    snapshot.assert_match(perform_error_test(client, data))
+    assert snapshot == perform_error_test(client, data)
 
 
 def test_third_level_custom_array_error(client, some_blog, snapshot):
     data = {
-        'data': {
-            'type': 'entries',
-            'attributes': {
-                'blog': some_blog.pk,
-                'bodyText': 'body_text',
-                'headline': 'headline',
-                'comments': [
-                    {
-                        'body': 'test comment',
-                        'attachments': [
-                            {
-                                'data': 'text'
-                            }
-                        ]
-                    }
-                ]
-            }
+        "data": {
+            "type": "entries",
+            "attributes": {
+                "blog": some_blog.pk,
+                "bodyText": "body_text",
+                "headline": "headline",
+                "comments": [
+                    {"body": "test comment", "attachments": [{"data": "text"}]}
+                ],
+            },
         }
     }
 
-    snapshot.assert_match(perform_error_test(client, data))
+    assert snapshot == perform_error_test(client, data)
 
 
 def test_third_level_dict_error(client, some_blog, snapshot):
     data = {
-        'data': {
-            'type': 'entries',
-            'attributes': {
-                'blog': some_blog.pk,
-                'bodyText': 'body_text',
-                'headline': 'headline',
-                'comments': [
-                    {
-                        'body': 'test comment',
-                        'attachment': {}
-                    }
-                ]
-            }
+        "data": {
+            "type": "entries",
+            "attributes": {
+                "blog": some_blog.pk,
+                "bodyText": "body_text",
+                "headline": "headline",
+                "comments": [{"body": "test comment", "attachment": {}}],
+            },
         }
     }
 
-    snapshot.assert_match(perform_error_test(client, data))
+    assert snapshot == perform_error_test(client, data)
 
 
 def test_many_third_level_dict_errors(client, some_blog, snapshot):
     data = {
-        'data': {
-            'type': 'entries',
-            'attributes': {
-                'blog': some_blog.pk,
-                'bodyText': 'body_text',
-                'headline': 'headline',
-                'comments': [
-                    {
-                        'attachment': {}
-                    }
-                ]
-            }
+        "data": {
+            "type": "entries",
+            "attributes": {
+                "blog": some_blog.pk,
+                "bodyText": "body_text",
+                "headline": "headline",
+                "comments": [{"attachment": {}}],
+            },
         }
     }
 
-    snapshot.assert_match(perform_error_test(client, data))
+    assert snapshot == perform_error_test(client, data)
+
+
+def test_relationship_errors_has_correct_pointers_with_camelize(
+    client, some_blog, snapshot
+):
+    data = {
+        "data": {
+            "type": "entries",
+            "attributes": {
+                "blog": some_blog.pk,
+                "bodyText": "body_text",
+                "headline": "headline",
+            },
+            "relationships": {
+                "mainAuthor": {"data": {"id": "INVALID_ID", "type": "authors"}},
+                "authors": {"data": [{"id": "INVALID_ID", "type": "authors"}]},
+            },
+        }
+    }
+
+    assert snapshot == perform_error_test(client, data)
+
+
+@override_settings(JSON_API_FORMAT_FIELD_NAMES="dasherize")
+def test_relationship_errors_has_correct_pointers_with_dasherize(
+    client, some_blog, snapshot
+):
+    data = {
+        "data": {
+            "type": "entries",
+            "attributes": {
+                "blog": some_blog.pk,
+                "bodyText": "body_text",
+                "headline": "headline",
+            },
+            "relationships": {
+                "main-author": {"data": {"id": "INVALID_ID", "type": "authors"}},
+                "authors": {"data": [{"id": "INVALID_ID", "type": "authors"}]},
+            },
+        }
+    }
+
+    assert snapshot == perform_error_test(client, data)
+
+
+@override_settings(JSON_API_FORMAT_FIELD_NAMES=None)
+def test_relationship_errors_has_correct_pointers_with_no_formatting(
+    client, some_blog, snapshot
+):
+    data = {
+        "data": {
+            "type": "entries",
+            "attributes": {
+                "blog": some_blog.pk,
+                "body_text": "body_text",
+                "headline": "headline",
+            },
+            "relationships": {
+                "main_author": {"data": {"id": "INVALID_ID", "type": "authors"}},
+                "authors": {"data": [{"id": "INVALID_ID", "type": "authors"}]},
+            },
+        }
+    }
+
+    assert snapshot == perform_error_test(client, data)
diff --git a/example/tests/test_filters.py b/example/tests/test_filters.py
index c3b1c42d..8e45ded1 100644
--- a/example/tests/test_filters.py
+++ b/example/tests/test_filters.py
@@ -8,24 +8,26 @@ class DJATestFilters(APITestCase):
     """
     tests of JSON:API filter backends
     """
-    fixtures = ('blogentry',)
+
+    fixtures = ("blogentry",)
 
     def setUp(self):
         self.entries = Entry.objects.all()
         self.blogs = Blog.objects.all()
-        self.url = reverse('nopage-entry-list')
-        self.fs_url = reverse('filterset-entry-list')
-        self.no_fs_url = reverse('nofilterset-entry-list')
+        self.url = reverse("nopage-entry-list")
+        self.fs_url = reverse("filterset-entry-list")
+        self.no_fs_url = reverse("nofilterset-entry-list")
 
     def test_sort(self):
         """
         test sort
         """
-        response = self.client.get(self.url, data={'sort': 'headline'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"sort": "headline"})
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        headlines = [c['attributes']['headline'] for c in dja_response['data']]
+        headlines = [c["attributes"]["headline"] for c in dja_response["data"]]
         sorted_headlines = sorted(headlines)
         self.assertEqual(headlines, sorted_headlines)
 
@@ -33,11 +35,12 @@ def test_sort_reverse(self):
         """
         confirm switching the sort order actually works
         """
-        response = self.client.get(self.url, data={'sort': '-headline'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"sort": "-headline"})
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        headlines = [c['attributes']['headline'] for c in dja_response['data']]
+        headlines = [c["attributes"]["headline"] for c in dja_response["data"]]
         sorted_headlines = sorted(headlines)
         self.assertNotEqual(headlines, sorted_headlines)
 
@@ -45,11 +48,12 @@ def test_sort_double_negative(self):
         """
         what if they provide multiple `-`'s? It's OK.
         """
-        response = self.client.get(self.url, data={'sort': '--headline'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"sort": "--headline"})
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        headlines = [c['attributes']['headline'] for c in dja_response['data']]
+        headlines = [c["attributes"]["headline"] for c in dja_response["data"]]
         sorted_headlines = sorted(headlines)
         self.assertNotEqual(headlines, sorted_headlines)
 
@@ -57,23 +61,28 @@ def test_sort_invalid(self):
         """
         test sort of invalid field
         """
-        response = self.client.get(self.url,
-                                   data={'sort': 'nonesuch,headline,-not_a_field'})
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(
+            self.url, data={"sort": "nonesuch,headline,-not_a_field"}
+        )
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'],
-                         "invalid sort parameters: nonesuch,-not_a_field")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"],
+            "invalid sort parameters: nonesuch,-not_a_field",
+        )
 
     def test_sort_camelcase(self):
         """
         test sort of camelcase field name
         """
-        response = self.client.get(self.url, data={'sort': 'bodyText'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"sort": "bodyText"})
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        blog_ids = [(c['attributes']['bodyText'] or '') for c in dja_response['data']]
+        blog_ids = [(c["attributes"]["bodyText"] or "") for c in dja_response["data"]]
         sorted_blog_ids = sorted(blog_ids)
         self.assertEqual(blog_ids, sorted_blog_ids)
 
@@ -84,25 +93,29 @@ def test_sort_underscore(self):
         "Be conservative in what you send, be liberal in what you accept"
                    --  https://en.wikipedia.org/wiki/Robustness_principle
         """
-        response = self.client.get(self.url, data={'sort': 'body_text'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"sort": "body_text"})
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        blog_ids = [(c['attributes']['bodyText'] or '') for c in dja_response['data']]
+        blog_ids = [(c["attributes"]["bodyText"] or "") for c in dja_response["data"]]
         sorted_blog_ids = sorted(blog_ids)
         self.assertEqual(blog_ids, sorted_blog_ids)
 
     def test_sort_related(self):
         """
-        test sort via related field using jsonapi path `.` and django orm `__` notation.
+        test sort via related field using JSON:API path `.` and django orm `__` notation.
         ORM relations must be predefined in the View's .ordering_fields attr
         """
-        for datum in ('blog__id', 'blog.id'):
-            response = self.client.get(self.url, data={'sort': datum})
-            self.assertEqual(response.status_code, 200,
-                             msg=response.content.decode("utf-8"))
+        for datum in ("blog__id", "blog.id"):
+            response = self.client.get(self.url, data={"sort": datum})
+            self.assertEqual(
+                response.status_code, 200, msg=response.content.decode("utf-8")
+            )
             dja_response = response.json()
-            blog_ids = [c['relationships']['blog']['data']['id'] for c in dja_response['data']]
+            blog_ids = [
+                c["relationships"]["blog"]["data"]["id"] for c in dja_response["data"]
+            ]
             sorted_blog_ids = sorted(blog_ids)
             self.assertEqual(blog_ids, sorted_blog_ids)
 
@@ -110,46 +123,50 @@ def test_filter_exact(self):
         """
         filter for an exact match
         """
-        response = self.client.get(self.url, data={'filter[headline]': 'CHEM3271X'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"filter[headline]": "CHEM3271X"})
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(len(dja_response['data']), 1)
+        self.assertEqual(len(dja_response["data"]), 1)
 
     def test_filter_exact_fail(self):
         """
         failed search for an exact match
         """
-        response = self.client.get(self.url, data={'filter[headline]': 'XXXXX'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"filter[headline]": "XXXXX"})
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(len(dja_response['data']), 0)
+        self.assertEqual(len(dja_response["data"]), 0)
 
     def test_filter_isnull(self):
         """
         search for null value
         """
-        response = self.client.get(self.url, data={'filter[bodyText.isnull]': 'true'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"filter[bodyText.isnull]": "true"})
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
         self.assertEqual(
-            len(dja_response['data']),
-            len([k for k in self.entries if k.body_text is None])
+            len(dja_response["data"]),
+            len([k for k in self.entries if k.body_text is None]),
         )
 
     def test_filter_not_null(self):
         """
         search for not null
         """
-        response = self.client.get(self.url, data={'filter[bodyText.isnull]': 'false'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"filter[bodyText.isnull]": "false"})
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
         self.assertEqual(
-            len(dja_response['data']),
-            len([k for k in self.entries if k.body_text is not None])
+            len(dja_response["data"]),
+            len([k for k in self.entries if k.body_text is not None]),
         )
 
     def test_filter_isempty(self):
@@ -157,26 +174,35 @@ def test_filter_isempty(self):
         search for an empty value (different from null!)
         the easiest way to do this is search for r'^$'
         """
-        response = self.client.get(self.url, data={'filter[bodyText.regex]': '^$'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"filter[bodyText.regex]": "^$"})
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(len(dja_response['data']),
-                         len([k for k in self.entries
-                              if k.body_text is not None and
-                              len(k.body_text) == 0]))
+        self.assertEqual(
+            len(dja_response["data"]),
+            len(
+                [
+                    k
+                    for k in self.entries
+                    if k.body_text is not None and len(k.body_text) == 0
+                ]
+            ),
+        )
 
     def test_filter_related(self):
         """
         filter via a relationship chain
         """
-        response = self.client.get(self.url, data={'filter[blog.name]': 'ANTB'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"filter[blog.name]": "ANTB"})
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(len(dja_response['data']),
-                         len([k for k in self.entries
-                              if k.blog.name == 'ANTB']))
+        self.assertEqual(
+            len(dja_response["data"]),
+            len([k for k in self.entries if k.blog.name == "ANTB"]),
+        )
 
     def test_filter_related_fieldset_class(self):
         """
@@ -184,133 +210,164 @@ def test_filter_related_fieldset_class(self):
         This tests a shortcut for a longer ORM path: `bname` is a shortcut
         name for `blog.name`.
         """
-        response = self.client.get(self.fs_url, data={'filter[bname]': 'ANTB'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.fs_url, data={"filter[bname]": "ANTB"})
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(len(dja_response['data']),
-                         len([k for k in self.entries
-                              if k.blog.name == 'ANTB']))
+        self.assertEqual(
+            len(dja_response["data"]),
+            len([k for k in self.entries if k.blog.name == "ANTB"]),
+        )
 
     def test_filter_related_missing_fieldset_class(self):
         """
         filter via with neither filterset_fields nor filterset_class
         This should return an error for any filter[]
         """
-        response = self.client.get(self.no_fs_url, data={'filter[bname]': 'ANTB'})
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.no_fs_url, data={"filter[bname]": "ANTB"})
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'],
-                         "invalid filter[bname]")
+        self.assertEqual(dja_response["errors"][0]["detail"], "invalid filter[bname]")
 
     def test_filter_fields_union_list(self):
         """
         test field for a list of values(ORed): ?filter[field.in]': 'val1,val2,val3
         """
-        response = self.client.get(self.url,
-                                   data={'filter[headline.in]': 'CLCV2442V,XXX,BIOL3594X'})
+        response = self.client.get(
+            self.url, data={"filter[headline.in]": "CLCV2442V,XXX,BIOL3594X"}
+        )
         dja_response = response.json()
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
         self.assertEqual(
-            len(dja_response['data']),
-            len([k for k in self.entries if k.headline == 'CLCV2442V']) +
-            len([k for k in self.entries if k.headline == 'XXX']) +
-            len([k for k in self.entries if k.headline == 'BIOL3594X']),
-            msg="filter field list (union)")
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
+        self.assertEqual(
+            len(dja_response["data"]),
+            len([k for k in self.entries if k.headline == "CLCV2442V"])
+            + len([k for k in self.entries if k.headline == "XXX"])
+            + len([k for k in self.entries if k.headline == "BIOL3594X"]),
+            msg="filter field list (union)",
+        )
 
     def test_filter_fields_intersection(self):
         """
         test fields (ANDed): ?filter[field1]': 'val1&filter[field2]'='val2
         """
         #
-        response = self.client.get(self.url,
-                                   data={'filter[headline.regex]': '^A',
-                                         'filter[body_text.icontains]': 'in'})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(
+            self.url,
+            data={"filter[headline.regex]": "^A", "filter[body_text.icontains]": "in"},
+        )
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertGreater(len(dja_response['data']), 1)
+        self.assertGreater(len(dja_response["data"]), 1)
         self.assertEqual(
-            len(dja_response['data']),
-            len([k for k in self.entries if k.headline.startswith('A') and
-                 'in' in k.body_text.lower()]))
+            len(dja_response["data"]),
+            len(
+                [
+                    k
+                    for k in self.entries
+                    if k.headline.startswith("A") and "in" in k.body_text.lower()
+                ]
+            ),
+        )
 
     def test_filter_invalid_association_name(self):
         """
         test for filter with invalid filter association name
         """
-        response = self.client.get(self.url, data={'filter[nonesuch]': 'CHEM3271X'})
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"filter[nonesuch]": "CHEM3271X"})
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'],
-                         "invalid filter[nonesuch]")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"], "invalid filter[nonesuch]"
+        )
 
     def test_filter_empty_association_name(self):
         """
         test for filter with missing association name
         error texts are different depending on whether QueryParameterValidationFilter is in use.
         """
-        response = self.client.get(self.url, data={'filter[]': 'foobar'})
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"filter[]": "foobar"})
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'], "invalid query parameter: filter[]")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"], "invalid query parameter: filter[]"
+        )
 
     def test_filter_no_brackets(self):
         """
         test for `filter=foobar` with missing filter[association] name
         """
-        response = self.client.get(self.url, data={'filter': 'foobar'})
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"filter": "foobar"})
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'],
-                         "invalid query parameter: filter")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"], "invalid query parameter: filter"
+        )
 
     def test_filter_missing_right_bracket(self):
         """
         test for filter missing right bracket
         """
-        response = self.client.get(self.url, data={'filter[headline': 'foobar'})
-        self.assertEqual(response.status_code, 400, msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"filter[headline": "foobar"})
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'],
-                         "invalid query parameter: filter[headline")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"],
+            "invalid query parameter: filter[headline",
+        )
 
     def test_filter_no_brackets_rvalue(self):
         """
         test for `filter=` with missing filter[association] and value
         """
-        response = self.client.get(self.url + '?filter=')
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url + "?filter=")
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'],
-                         "invalid query parameter: filter")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"], "invalid query parameter: filter"
+        )
 
     def test_filter_no_brackets_equal(self):
         """
         test for `filter` with missing filter[association] name and =value
         """
-        response = self.client.get(self.url + '?filter')
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url + "?filter")
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'],
-                         "invalid query parameter: filter")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"], "invalid query parameter: filter"
+        )
 
     def test_filter_malformed_left_bracket(self):
         """
         test for filter with invalid filter syntax
         """
-        response = self.client.get(self.url, data={'filter[': 'foobar'})
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"filter[": "foobar"})
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'], "invalid query parameter: filter[")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"], "invalid query parameter: filter["
+        )
 
     def test_filter_missing_rvalue(self):
         """
@@ -318,24 +375,30 @@ def test_filter_missing_rvalue(self):
         this should probably be an error rather than ignoring the filter:
         https://django-filter.readthedocs.io/en/latest/guide/tips.html#filtering-by-an-empty-string
         """
-        response = self.client.get(self.url, data={'filter[headline]': ''})
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"filter[headline]": ""})
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'],
-                         "missing value for query parameter filter[headline]")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"],
+            "missing value for query parameter filter[headline]",
+        )
 
     def test_filter_missing_rvalue_equal(self):
         """
         test for filter with missing value to test against
         this should probably be an error rather than ignoring the filter:
         """
-        response = self.client.get(self.url + '?filter[headline]')
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url + "?filter[headline]")
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'],
-                         "missing value for query parameter filter[headline]")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"],
+            "missing value for query parameter filter[headline]",
+        )
 
     def test_filter_single_relation(self):
         """
@@ -343,13 +406,14 @@ def test_filter_single_relation(self):
         e.g. filterset-entries?filter[authors.id]=1
         looks for entries written by (at least) author.id=1
         """
-        response = self.client.get(self.fs_url, data={'filter[authors.id]': 1})
+        response = self.client.get(self.fs_url, data={"filter[authors.id]": 1})
 
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
 
-        ids = [k['id'] for k in dja_response['data']]
+        ids = [k["id"] for k in dja_response["data"]]
 
         expected_ids = [str(k.id) for k in self.entries.filter(authors__id=1)]
 
@@ -361,15 +425,18 @@ def test_filter_repeated_relations(self):
         e.g. filterset-entries?filter[authors.id]=1&filter[authors.id]=2
         looks for entries written by (at least) author.id=1 AND author.id=2
         """
-        response = self.client.get(self.fs_url, data={'filter[authors.id]': [1, 2]})
+        response = self.client.get(self.fs_url, data={"filter[authors.id]": [1, 2]})
 
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
 
-        ids = [k['id'] for k in dja_response['data']]
+        ids = [k["id"] for k in dja_response["data"]]
 
-        expected_ids = [str(k.id) for k in self.entries.filter(authors__id=1).filter(authors__id=2)]
+        expected_ids = [
+            str(k.id) for k in self.entries.filter(authors__id=1).filter(authors__id=2)
+        ]
 
         self.assertEqual(set(ids), set(expected_ids))
 
@@ -379,13 +446,14 @@ def test_filter_in(self):
         e.g. filterset-entries?filter[authors.id.in]=1,2
         looks for entries written by (at least) author.id=1 OR author.id=2
         """
-        response = self.client.get(self.fs_url, data={'filter[authors.id.in]': '1,2'})
+        response = self.client.get(self.fs_url, data={"filter[authors.id.in]": "1,2"})
 
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
 
-        ids = [k['id'] for k in dja_response['data']]
+        ids = [k["id"] for k in dja_response["data"]]
 
         expected_ids = [str(k.id) for k in self.entries.filter(authors__id__in=[1, 2])]
 
@@ -396,82 +464,71 @@ def test_search_keywords(self):
         test for `filter[search]="keywords"` where some of the keywords are in the entry and
         others are in the related blog.
         """
-        response = self.client.get(self.url, data={'filter[search]': 'barnard field research'})
+        response = self.client.get(
+            self.url, data={"filter[search]": "barnard field research"}
+        )
         expected_result = {
-            'data': [
+            "data": [
                 {
-                    'type': 'posts',
-                    'id': '7',
-                    'attributes': {
-                        'headline': 'ANTH3868X',
-                        'bodyText': 'ETHNOGRAPHIC FIELD RESEARCH IN NYC',
-                        'pubDate': None,
-                        'modDate': None},
-                    'relationships': {
-                        'blog': {
-                            'data': {
-                                'type': 'blogs',
-                                'id': '1'
+                    "type": "entries",
+                    "id": "7",
+                    "attributes": {
+                        "headline": "ANTH3868X",
+                        "bodyText": "ETHNOGRAPHIC FIELD RESEARCH IN NYC",
+                        "pubDate": None,
+                        "modDate": None,
+                    },
+                    "relationships": {
+                        "blog": {"data": {"type": "blogs", "id": "1"}},
+                        "blogHyperlinked": {
+                            "links": {
+                                "self": "http://testserver/entries/7/relationships/blog_hyperlinked",  # noqa: B950
+                                "related": "http://testserver/entries/7/blog",
                             }
                         },
-                        'blogHyperlinked': {
-                            'links': {
-                                'self': 'http://testserver/entries/7/relationships/blog_hyperlinked',  # noqa: E501
-                                'related': 'http://testserver/entries/7/blog'}
-                        },
-                        'authors': {
-                            'meta': {
-                                'count': 0
-                            },
-                            'data': []
-                        },
-                        'comments': {
-                            'meta': {
-                                'count': 0
-                            },
-                            'data': []
-                        },
-                        'commentsHyperlinked': {
-                            'links': {
-                                'self': 'http://testserver/entries/7/relationships/comments_hyperlinked',  # noqa: E501
-                                'related': 'http://testserver/entries/7/comments'
+                        "authors": {"meta": {"count": 0}, "data": []},
+                        "comments": {"meta": {"count": 0}, "data": []},
+                        "commentsHyperlinked": {
+                            "links": {
+                                "self": "http://testserver/entries/7/relationships/comments_hyperlinked",  # noqa: B950
+                                "related": "http://testserver/entries/7/comments",
                             }
                         },
-                        'suggested': {
-                            'links': {
-                                'self': 'http://testserver/entries/7/relationships/suggested',
-                                'related': 'http://testserver/entries/7/suggested/'
+                        "suggested": {
+                            "links": {
+                                "self": "http://testserver/entries/7/relationships/suggested",
+                                "related": "http://testserver/entries/7/suggested/",
                             },
-                            'data': [
-                                {'type': 'entries', 'id': '1'},
-                                {'type': 'entries', 'id': '2'},
-                                {'type': 'entries', 'id': '3'},
-                                {'type': 'entries', 'id': '4'},
-                                {'type': 'entries', 'id': '5'},
-                                {'type': 'entries', 'id': '6'},
-                                {'type': 'entries', 'id': '8'},
-                                {'type': 'entries', 'id': '9'},
-                                {'type': 'entries', 'id': '10'},
-                                {'type': 'entries', 'id': '11'},
-                                {'type': 'entries', 'id': '12'}
-                            ]
+                            "data": [
+                                {"type": "entries", "id": "1"},
+                                {"type": "entries", "id": "2"},
+                                {"type": "entries", "id": "3"},
+                                {"type": "entries", "id": "4"},
+                                {"type": "entries", "id": "5"},
+                                {"type": "entries", "id": "6"},
+                                {"type": "entries", "id": "8"},
+                                {"type": "entries", "id": "9"},
+                                {"type": "entries", "id": "10"},
+                                {"type": "entries", "id": "11"},
+                                {"type": "entries", "id": "12"},
+                            ],
+                            "meta": {"count": 11},
                         },
-                        'suggestedHyperlinked': {
-                            'links': {
-                                'self': 'http://testserver/entries/7/relationships/suggested_hyperlinked',  # noqa: E501
-                                'related': 'http://testserver/entries/7/suggested/'}
+                        "suggestedHyperlinked": {
+                            "links": {
+                                "self": "http://testserver/entries/7/relationships/suggested_hyperlinked",  # noqa: B950
+                                "related": "http://testserver/entries/7/suggested/",
+                            }
                         },
-                        'tags': {'data': [], 'meta': {'count': 0}},
-                        'featuredHyperlinked': {
-                            'links': {
-                                'self': 'http://testserver/entries/7/relationships/featured_hyperlinked',  # noqa: E501
-                                'related': 'http://testserver/entries/7/featured'
+                        "tags": {"data": [], "meta": {"count": 0}},
+                        "featuredHyperlinked": {
+                            "links": {
+                                "self": "http://testserver/entries/7/relationships/featured_hyperlinked",  # noqa: B950
+                                "related": "http://testserver/entries/7/featured",
                             }
-                        }
+                        },
                     },
-                    'meta': {
-                        'bodyFormat': 'text'
-                    }
+                    "meta": {"bodyFormat": "text"},
                 }
             ]
         }
@@ -494,51 +551,71 @@ def test_search_multiple_keywords(self):
         1. For each keyword, search the 4 search_fields for a match and then get the result
            set which is the union of all results for the given keyword.
         2. Intersect those results sets such that *all* keywords are represented.
-        See `example/fixtures/blogentry.json` for the test content that the searches are based on.
+        See `example/fixtures/blogentry.json` for what test content the searches are based on.
         The searches test for both direct entries and related blogs across multiple fields.
         """
-        for searches in ("research", "chemistry", "nonesuch",
-                         "research seminar", "research nonesuch",
-                         "barnard classic", "barnard ethnographic field research"):
-            response = self.client.get(self.url, data={'filter[search]': searches})
-            self.assertEqual(response.status_code, 200, msg=response.content.decode("utf-8"))
+        for searches in (
+            "research",
+            "chemistry",
+            "nonesuch",
+            "research seminar",
+            "research nonesuch",
+            "barnard classic",
+            "barnard ethnographic field research",
+        ):
+            response = self.client.get(self.url, data={"filter[search]": searches})
+            self.assertEqual(
+                response.status_code, 200, msg=response.content.decode("utf-8")
+            )
             dja_response = response.json()
             keys = searches.split()
             # dicts keyed by the search keys for the 4 search_fields:
-            headline = {}      # list of entry ids where key is in entry__headline
-            body_text = {}     # list of entry ids where key is in entry__body_text
-            blog_name = {}     # list of entry ids where key is in entry__blog__name
+            headline = {}  # list of entry ids where key is in entry__headline
+            body_text = {}  # list of entry ids where key is in entry__body_text
+            blog_name = {}  # list of entry ids where key is in entry__blog__name
             blog_tagline = {}  # list of entry ids where key is in entry__blog__tagline
             for key in keys:
-                headline[key] = [str(k.id) for k in
-                                 self.entries.filter(headline__icontains=key)]
-                body_text[key] = [str(k.id) for k in
-                                  self.entries.filter(body_text__icontains=key)]
-                blog_name[key] = [str(k.id) for k in
-                                  self.entries.filter(blog__name__icontains=key)]
-                blog_tagline[key] = [str(k.id) for k in
-                                     self.entries.filter(blog__tagline__icontains=key)]
+                headline[key] = [
+                    str(k.id) for k in self.entries.filter(headline__icontains=key)
+                ]
+                body_text[key] = [
+                    str(k.id) for k in self.entries.filter(body_text__icontains=key)
+                ]
+                blog_name[key] = [
+                    str(k.id) for k in self.entries.filter(blog__name__icontains=key)
+                ]
+                blog_tagline[key] = [
+                    str(k.id) for k in self.entries.filter(blog__tagline__icontains=key)
+                ]
             union = []  # each list item is a set of entry ids matching the given key
             for key in keys:
-                union.append(set(headline[key] + body_text[key] +
-                                 blog_name[key] + blog_tagline[key]))
+                union.append(
+                    set(
+                        headline[key]
+                        + body_text[key]
+                        + blog_name[key]
+                        + blog_tagline[key]
+                    )
+                )
             # all keywords must be present: intersect the keyword sets
             expected_ids = set.intersection(*union)
             expected_len = len(expected_ids)
-            self.assertEqual(len(dja_response['data']), expected_len)
-            returned_ids = set([k['id'] for k in dja_response['data']])
+            self.assertEqual(len(dja_response["data"]), expected_len)
+            returned_ids = {k["id"] for k in dja_response["data"]}
             self.assertEqual(returned_ids, expected_ids)
 
     def test_param_invalid(self):
         """
         Test a "wrong" query parameter
         """
-        response = self.client.get(self.url, data={'garbage': 'foo'})
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.url, data={"garbage": "foo"})
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'],
-                         "invalid query parameter: garbage")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"], "invalid query parameter: garbage"
+        )
 
     def test_param_duplicate_sort(self):
         """
@@ -546,38 +623,48 @@ def test_param_duplicate_sort(self):
         `?sort=headline&page[size]=3&sort=bodyText` is not allowed.
         This is not so obvious when using a data dict....
         """
-        response = self.client.get(self.url,
-                                   data={'sort': ['headline', 'bodyText'],
-                                         'page[size]': 3}
-                                   )
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(
+            self.url, data={"sort": ["headline", "bodyText"], "page[size]": 3}
+        )
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'],
-                         "repeated query parameter not allowed: sort")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"],
+            "repeated query parameter not allowed: sort",
+        )
 
     def test_param_duplicate_page(self):
         """
         test a duplicated page[size] query parameter
         """
-        response = self.client.get(self.fs_url, data={'page[size]': [1, 2]})
-        self.assertEqual(response.status_code, 400,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(self.fs_url, data={"page[size]": [1, 2]})
+        self.assertEqual(
+            response.status_code, 400, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(dja_response['errors'][0]['detail'],
-                         "repeated query parameter not allowed: page[size]")
+        self.assertEqual(
+            dja_response["errors"][0]["detail"],
+            "repeated query parameter not allowed: page[size]",
+        )
 
     def test_many_params(self):
         """
         Test that filter params aren't ignored when many params are present
         """
-        response = self.client.get(self.url,
-                                   data={'filter[headline.regex]': '^A',
-                                         'filter[body_text.regex]': '^IN',
-                                         'filter[blog.name]': 'ANTB',
-                                         'page[size]': 3})
-        self.assertEqual(response.status_code, 200,
-                         msg=response.content.decode("utf-8"))
+        response = self.client.get(
+            self.url,
+            data={
+                "filter[headline.regex]": "^A",
+                "filter[body_text.regex]": "^IN",
+                "filter[blog.name]": "ANTB",
+                "page[size]": 3,
+            },
+        )
+        self.assertEqual(
+            response.status_code, 200, msg=response.content.decode("utf-8")
+        )
         dja_response = response.json()
-        self.assertEqual(len(dja_response['data']), 1)
-        self.assertEqual(dja_response['data'][0]['id'], '1')
+        self.assertEqual(len(dja_response["data"]), 1)
+        self.assertEqual(dja_response["data"][0]["id"], "1")
diff --git a/example/tests/test_format_keys.py b/example/tests/test_format_keys.py
deleted file mode 100644
index 0fd76c67..00000000
--- a/example/tests/test_format_keys.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from django.contrib.auth import get_user_model
-from django.urls import reverse
-from django.utils import encoding
-from rest_framework import status
-
-from example.tests import TestBase
-
-
-class FormatKeysSetTests(TestBase):
-    """
-    Test that camelization and underscoring of key names works if they are activated.
-    """
-    list_url = reverse('user-list')
-
-    def setUp(self):
-        super(FormatKeysSetTests, self).setUp()
-        self.detail_url = reverse('user-detail', kwargs={'pk': self.miles.pk})
-
-    def test_camelization(self):
-        """
-        Test that camelization works.
-        """
-        response = self.client.get(self.list_url)
-        self.assertEqual(response.status_code, 200)
-
-        user = get_user_model().objects.all()[0]
-        expected = {
-            'data': [
-                {
-                    'type': 'users',
-                    'id': encoding.force_str(user.pk),
-                    'attributes': {
-                        'firstName': user.first_name,
-                        'lastName': user.last_name,
-                        'email': user.email
-                    },
-                }
-            ],
-            'links': {
-                'first': 'http://testserver/identities?page%5Bnumber%5D=1',
-                'last': 'http://testserver/identities?page%5Bnumber%5D=2',
-                'next': 'http://testserver/identities?page%5Bnumber%5D=2',
-                'prev': None
-            },
-            'meta': {
-                'pagination': {
-                    'page': 1,
-                    'pages': 2,
-                    'count': 2
-                }
-            }
-        }
-
-        assert expected == response.json()
-
-
-def test_options_format_field_names(db, client):
-    response = client.options(reverse('author-list'))
-    assert response.status_code == status.HTTP_200_OK
-    data = response.json()['data']
-    expected_keys = {'name', 'email', 'bio', 'entries', 'firstEntry', 'type',
-                     'comments', 'secrets', 'defaults'}
-    assert expected_keys == data['actions']['POST'].keys()
diff --git a/example/tests/test_generic_validation.py b/example/tests/test_generic_validation.py
index ff2aff4b..1c3561e7 100644
--- a/example/tests/test_generic_validation.py
+++ b/example/tests/test_generic_validation.py
@@ -9,8 +9,8 @@ class GenericValidationTest(TestBase):
     """
 
     def setUp(self):
-        super(GenericValidationTest, self).setUp()
-        self.url = reverse('user-validation', kwargs={'pk': self.miles.pk})
+        super().setUp()
+        self.url = reverse("user-validation", kwargs={"pk": self.miles.pk})
 
     def test_generic_validation_error(self):
         """
@@ -20,14 +20,14 @@ def test_generic_validation_error(self):
         self.assertEqual(response.status_code, 400)
 
         expected = {
-            'errors': [{
-                'status': '400',
-                'source': {
-                    'pointer': '/data'
-                },
-                'detail': 'Oh nohs!',
-                'code': 'invalid',
-            }]
+            "errors": [
+                {
+                    "status": "400",
+                    "source": {"pointer": "/data"},
+                    "detail": "Oh nohs!",
+                    "code": "invalid",
+                }
+            ]
         }
 
         assert expected == response.json()
diff --git a/example/tests/test_generic_viewset.py b/example/tests/test_generic_viewset.py
index ba315740..40812d6e 100644
--- a/example/tests/test_generic_viewset.py
+++ b/example/tests/test_generic_viewset.py
@@ -13,17 +13,17 @@ def test_default_rest_framework_behavior(self):
         """
         This is more of an example really, showing default behavior
         """
-        url = reverse('user-default', kwargs={'pk': self.miles.pk})
+        url = reverse("user-default", kwargs={"pk": self.miles.pk})
 
         response = self.client.get(url)
 
         self.assertEqual(200, response.status_code)
 
         expected = {
-            'id': 2,
-            'first_name': 'Miles',
-            'last_name': 'Davis',
-            'email': 'miles@example.com'
+            "id": 2,
+            "first_name": "Miles",
+            "last_name": "Davis",
+            "email": "miles@example.com",
         }
 
         assert expected == response.json()
@@ -33,21 +33,21 @@ def test_ember_expected_renderer(self):
         The :class:`UserEmber` ViewSet has the ``resource_name`` of 'data'
         so that should be the key in the JSON response.
         """
-        url = reverse('user-manual-resource-name', kwargs={'pk': self.miles.pk})
+        url = reverse("user-manual-resource-name", kwargs={"pk": self.miles.pk})
 
-        with override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize'):
+        with override_settings(JSON_API_FORMAT_FIELD_NAMES="dasherize"):
             response = self.client.get(url)
         self.assertEqual(200, response.status_code)
 
         expected = {
-            'data': {
-                'type': 'data',
-                'id': '2',
-                'attributes': {
-                    'first-name': 'Miles',
-                    'last-name': 'Davis',
-                    'email': 'miles@example.com'
-                }
+            "data": {
+                "type": "data",
+                "id": "2",
+                "attributes": {
+                    "first-name": "Miles",
+                    "last-name": "Davis",
+                    "email": "miles@example.com",
+                },
             }
         }
 
@@ -55,37 +55,41 @@ def test_ember_expected_renderer(self):
 
     def test_default_validation_exceptions(self):
         """
-        Default validation exceptions should conform to json api spec
+        Default validation exceptions should conform to JSON:API spec
         """
         expected = {
-            'errors': [
+            "errors": [
                 {
-                    'status': '400',
-                    'source': {
-                        'pointer': '/data/attributes/email',
+                    "status": "400",
+                    "source": {
+                        "pointer": "/data/attributes/email",
                     },
-                    'detail': 'Enter a valid email address.',
-                    'code': 'invalid',
+                    "detail": "Enter a valid email address.",
+                    "code": "invalid",
                 },
                 {
-                    'status': '400',
-                    'source': {
-                        'pointer': '/data/attributes/first-name',
+                    "status": "400",
+                    "source": {
+                        "pointer": "/data/attributes/first-name",
                     },
-                    'detail': 'There\'s a problem with first name',
-                    'code': 'invalid',
-                }
+                    "detail": "There's a problem with first name",
+                    "code": "invalid",
+                },
             ]
         }
-        with override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize'):
-            response = self.client.post('/identities', {
-                'data': {
-                    'type': 'users',
-                    'attributes': {
-                        'email': 'bar', 'first_name': 'alajflajaljalajlfjafljalj'
+        with override_settings(JSON_API_FORMAT_FIELD_NAMES="dasherize"):
+            response = self.client.post(
+                "/identities",
+                {
+                    "data": {
+                        "type": "users",
+                        "attributes": {
+                            "email": "bar",
+                            "first_name": "alajflajaljalajlfjafljalj",
+                        },
                     }
-                }
-            })
+                },
+            )
 
         assert expected == response.json()
 
@@ -94,30 +98,66 @@ def test_custom_validation_exceptions(self):
         Exceptions should be able to be formatted manually
         """
         expected = {
-            'errors': [
+            "errors": [
                 {
-                    'status': '400',
-                    'source': {
-                        'pointer': '/data/attributes/email',
+                    "status": "400",
+                    "source": {
+                        "pointer": "/data/attributes/email",
                     },
-                    'detail': 'Enter a valid email address.',
-                    'code': 'invalid',
+                    "detail": "Enter a valid email address.",
+                    "code": "invalid",
                 },
                 {
-                    'id': 'armageddon101',
-                    'detail': 'Hey! You need a last name!',
-                    'meta': 'something',
-                    'source': {'pointer': '/data/attributes/lastName'}
+                    "id": "armageddon101",
+                    "detail": "Hey! You need a last name!",
+                    "meta": "something",
+                    "source": {"pointer": "/data/attributes/lastName"},
                 },
             ]
         }
-        response = self.client.post('/identities', {
-            'data': {
-                'type': 'users',
-                'attributes': {
-                    'email': 'bar', 'last_name': 'alajflajaljalajlfjafljalj'
+        response = self.client.post(
+            "/identities",
+            {
+                "data": {
+                    "type": "users",
+                    "attributes": {
+                        "email": "bar",
+                        "last_name": "alajflajaljalajlfjafljalj",
+                    },
                 }
-            }
-        })
+            },
+        )
+
+        assert expected == response.json()
+
+    def test_nonfield_validation_exceptions(self):
+        """
+        Non-field errors should be attributed to /data source.pointer.
+        """
+        expected = {
+            "errors": [
+                {
+                    "status": "400",
+                    "source": {
+                        "pointer": "/data",
+                    },
+                    "detail": "First name cannot be the same as last name!",
+                    "code": "invalid",
+                },
+            ]
+        }
+        response = self.client.post(
+            "/identities",
+            {
+                "data": {
+                    "type": "users",
+                    "attributes": {
+                        "email": "miles@example.com",
+                        "first_name": "Miles",
+                        "last_name": "Miles",
+                    },
+                }
+            },
+        )
 
         assert expected == response.json()
diff --git a/example/tests/test_model_viewsets.py b/example/tests/test_model_viewsets.py
index 1ce8336d..ce6c7ba5 100644
--- a/example/tests/test_model_viewsets.py
+++ b/example/tests/test_model_viewsets.py
@@ -1,4 +1,3 @@
-import pytest
 from django.contrib.auth import get_user_model
 from django.test import override_settings
 from django.urls import reverse
@@ -15,46 +14,41 @@ class ModelViewSetTests(TestBase):
     [<RegexURLPattern user-list ^identities/$>,
     <RegexURLPattern user-detail ^identities/(?P<pk>[^/]+)/$>]
     """
-    list_url = reverse('user-list')
+
+    list_url = reverse("user-list")
 
     def setUp(self):
-        super(ModelViewSetTests, self).setUp()
-        self.detail_url = reverse('user-detail', kwargs={'pk': self.miles.pk})
+        super().setUp()
+        self.detail_url = reverse("user-detail", kwargs={"pk": self.miles.pk})
 
     def test_key_in_list_result(self):
         """
         Ensure the result has a 'user' key since that is the name of the model
         """
-        with override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize'):
+        with override_settings(JSON_API_FORMAT_FIELD_NAMES="dasherize"):
             response = self.client.get(self.list_url)
         self.assertEqual(response.status_code, 200)
 
         user = get_user_model().objects.all()[0]
         expected = {
-            'data': [
+            "data": [
                 {
-                    'type': 'users',
-                    'id': encoding.force_str(user.pk),
-                    'attributes': {
-                        'first-name': user.first_name,
-                        'last-name': user.last_name,
-                        'email': user.email
+                    "type": "users",
+                    "id": encoding.force_str(user.pk),
+                    "attributes": {
+                        "first-name": user.first_name,
+                        "last-name": user.last_name,
+                        "email": user.email,
                     },
                 }
             ],
-            'links': {
-                'first': 'http://testserver/identities?page%5Bnumber%5D=1',
-                'last': 'http://testserver/identities?page%5Bnumber%5D=2',
-                'next': 'http://testserver/identities?page%5Bnumber%5D=2',
-                'prev': None
+            "links": {
+                "first": "http://testserver/identities?page%5Bnumber%5D=1",
+                "last": "http://testserver/identities?page%5Bnumber%5D=2",
+                "next": "http://testserver/identities?page%5Bnumber%5D=2",
+                "prev": None,
             },
-            'meta': {
-                'pagination': {
-                    'page': 1,
-                    'pages': 2,
-                    'count': 2
-                }
-            }
+            "meta": {"pagination": {"page": 1, "pages": 2, "count": 2}},
         }
 
         assert expected == response.json()
@@ -63,36 +57,30 @@ def test_page_two_in_list_result(self):
         """
         Ensure that the second page is reachable and is the correct data.
         """
-        with override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize'):
-            response = self.client.get(self.list_url, {'page[number]': 2})
+        with override_settings(JSON_API_FORMAT_FIELD_NAMES="dasherize"):
+            response = self.client.get(self.list_url, {"page[number]": 2})
         self.assertEqual(response.status_code, 200)
 
         user = get_user_model().objects.all()[1]
         expected = {
-            'data': [
+            "data": [
                 {
-                    'type': 'users',
-                    'id': encoding.force_str(user.pk),
-                    'attributes': {
-                        'first-name': user.first_name,
-                        'last-name': user.last_name,
-                        'email': user.email
+                    "type": "users",
+                    "id": encoding.force_str(user.pk),
+                    "attributes": {
+                        "first-name": user.first_name,
+                        "last-name": user.last_name,
+                        "email": user.email,
                     },
                 }
             ],
-            'links': {
-                'first': 'http://testserver/identities?page%5Bnumber%5D=1',
-                'last': 'http://testserver/identities?page%5Bnumber%5D=2',
-                'next': None,
-                'prev': 'http://testserver/identities?page%5Bnumber%5D=1'
+            "links": {
+                "first": "http://testserver/identities?page%5Bnumber%5D=1",
+                "last": "http://testserver/identities?page%5Bnumber%5D=2",
+                "next": None,
+                "prev": "http://testserver/identities?page%5Bnumber%5D=1",
             },
-            'meta': {
-                'pagination': {
-                    'page': 2,
-                    'pages': 2,
-                    'count': 2
-                }
-            }
+            "meta": {"pagination": {"page": 2, "pages": 2, "count": 2}},
         }
 
         assert expected == response.json()
@@ -103,45 +91,39 @@ def test_page_range_in_list_result(self):
         tests pluralization as two objects means it converts ``user`` to
         ``users``.
         """
-        with override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize'):
-            response = self.client.get(self.list_url, {'page[size]': 2})
+        with override_settings(JSON_API_FORMAT_FIELD_NAMES="dasherize"):
+            response = self.client.get(self.list_url, {"page[size]": 2})
         self.assertEqual(response.status_code, 200)
 
         users = get_user_model().objects.all()
         expected = {
-            'data': [
+            "data": [
                 {
-                    'type': 'users',
-                    'id': encoding.force_str(users[0].pk),
-                    'attributes': {
-                        'first-name': users[0].first_name,
-                        'last-name': users[0].last_name,
-                        'email': users[0].email
+                    "type": "users",
+                    "id": encoding.force_str(users[0].pk),
+                    "attributes": {
+                        "first-name": users[0].first_name,
+                        "last-name": users[0].last_name,
+                        "email": users[0].email,
                     },
                 },
                 {
-                    'type': 'users',
-                    'id': encoding.force_str(users[1].pk),
-                    'attributes': {
-                        'first-name': users[1].first_name,
-                        'last-name': users[1].last_name,
-                        'email': users[1].email
+                    "type": "users",
+                    "id": encoding.force_str(users[1].pk),
+                    "attributes": {
+                        "first-name": users[1].first_name,
+                        "last-name": users[1].last_name,
+                        "email": users[1].email,
                     },
-                }
+                },
             ],
-            'links': {
-                'first': 'http://testserver/identities?page%5Bnumber%5D=1&page%5Bsize%5D=2',
-                'last': 'http://testserver/identities?page%5Bnumber%5D=1&page%5Bsize%5D=2',
-                'next': None,
-                'prev': None
+            "links": {
+                "first": "http://testserver/identities?page%5Bnumber%5D=1&page%5Bsize%5D=2",
+                "last": "http://testserver/identities?page%5Bnumber%5D=1&page%5Bsize%5D=2",
+                "next": None,
+                "prev": None,
             },
-            'meta': {
-                'pagination': {
-                    'page': 1,
-                    'pages': 1,
-                    'count': 2
-                }
-            }
+            "meta": {"pagination": {"page": 1, "pages": 1, "count": 2}},
         }
 
         assert expected == response.json()
@@ -150,18 +132,18 @@ def test_key_in_detail_result(self):
         """
         Ensure the result has a 'user' key.
         """
-        with override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize'):
+        with override_settings(JSON_API_FORMAT_FIELD_NAMES="dasherize"):
             response = self.client.get(self.detail_url)
         self.assertEqual(response.status_code, 200)
 
         expected = {
-            'data': {
-                'type': 'users',
-                'id': encoding.force_str(self.miles.pk),
-                'attributes': {
-                    'first-name': self.miles.first_name,
-                    'last-name': self.miles.last_name,
-                    'email': self.miles.email
+            "data": {
+                "type": "users",
+                "id": encoding.force_str(self.miles.pk),
+                "attributes": {
+                    "first-name": self.miles.first_name,
+                    "last-name": self.miles.last_name,
+                    "email": self.miles.email,
                 },
             }
         }
@@ -173,12 +155,7 @@ def test_patch_requires_id(self):
         Verify that 'id' is required to be passed in an update request.
         """
         data = {
-            'data': {
-                'type': 'users',
-                'attributes': {
-                    'first-name': 'DifferentName'
-                }
-            }
+            "data": {"type": "users", "attributes": {"first-name": "DifferentName"}}
         }
 
         response = self.client.patch(self.detail_url, data=data)
@@ -190,12 +167,10 @@ def test_patch_requires_correct_id(self):
         Verify that 'id' is the same then in url
         """
         data = {
-            'data': {
-                'type': 'users',
-                'id': self.miles.pk + 1,
-                'attributes': {
-                    'first-name': 'DifferentName'
-                }
+            "data": {
+                "type": "users",
+                "id": self.miles.pk + 1,
+                "attributes": {"first-name": "DifferentName"},
             }
         }
 
@@ -207,64 +182,40 @@ def test_key_in_post(self):
         """
         Ensure a key is in the post.
         """
-        self.client.login(username='miles', password='pw')
+        self.client.login(username="miles", password="pw")
         data = {
-            'data': {
-                'type': 'users',
-                'id': encoding.force_str(self.miles.pk),
-                'attributes': {
-                    'first-name': self.miles.first_name,
-                    'last-name': self.miles.last_name,
-                    'email': 'miles@trumpet.org'
+            "data": {
+                "type": "users",
+                "id": encoding.force_str(self.miles.pk),
+                "attributes": {
+                    "first-name": self.miles.first_name,
+                    "last-name": self.miles.last_name,
+                    "email": "miles@trumpet.org",
                 },
             }
         }
 
-        with override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize'):
+        with override_settings(JSON_API_FORMAT_FIELD_NAMES="dasherize"):
             response = self.client.put(self.detail_url, data=data)
 
         assert data == response.json()
 
         # is it updated?
         self.assertEqual(
-            get_user_model().objects.get(pk=self.miles.pk).email,
-            'miles@trumpet.org')
+            get_user_model().objects.get(pk=self.miles.pk).email, "miles@trumpet.org"
+        )
 
     def test_404_error_pointer(self):
-        self.client.login(username='miles', password='pw')
-        not_found_url = reverse('user-detail', kwargs={'pk': 12345})
-        errors = {
-            'errors': [
-                {'detail': 'Not found.', 'status': '404', 'code': 'not_found'}
-            ]
-        }
+        self.client.login(username="miles", password="pw")
+        not_found_url = reverse("user-detail", kwargs={"pk": 12345})
 
         response = self.client.get(not_found_url)
         assert 404 == response.status_code
-        assert errors == response.json()
+        result = response.json()
 
+        # exact detail message differs between Python versions
+        # but only relevant is that there is a message
+        assert result["errors"][0].pop("detail") is not None
 
-@pytest.mark.django_db
-def test_patch_allow_field_type(author, author_type_factory, client):
-    """
-    Verify that type field may be updated.
-    """
-    author_type = author_type_factory()
-    url = reverse('author-detail', args=[author.id])
-
-    data = {
-        'data': {
-            'id': author.id,
-            'type': 'authors',
-            'relationships': {
-                'data': {
-                    'id': author_type.id,
-                    'type': 'author-type'
-                }
-            }
-        }
-    }
-
-    response = client.patch(url, data=data)
-
-    assert response.status_code == 200
+        errors = {"errors": [{"status": "404", "code": "not_found"}]}
+        assert errors == response.json()
diff --git a/example/tests/test_openapi.py b/example/tests/test_openapi.py
deleted file mode 100644
index e7a2b6ca..00000000
--- a/example/tests/test_openapi.py
+++ /dev/null
@@ -1,149 +0,0 @@
-# largely based on DRF's test_openapi
-import json
-
-from django.test import RequestFactory, override_settings
-from django.urls import re_path
-from rest_framework.request import Request
-
-from rest_framework_json_api.schemas.openapi import AutoSchema, SchemaGenerator
-
-from example import views
-
-
-def create_request(path):
-    factory = RequestFactory()
-    request = Request(factory.get(path))
-    return request
-
-
-def create_view_with_kw(view_cls, method, request, initkwargs):
-    generator = SchemaGenerator()
-    view = generator.create_view(view_cls.as_view(initkwargs), method, request)
-    return view
-
-
-def test_path_without_parameters(snapshot):
-    path = '/authors/'
-    method = 'GET'
-
-    view = create_view_with_kw(
-        views.AuthorViewSet,
-        method,
-        create_request(path),
-        {'get': 'list'}
-    )
-    inspector = AutoSchema()
-    inspector.view = view
-
-    operation = inspector.get_operation(path, method)
-    snapshot.assert_match(json.dumps(operation, indent=2, sort_keys=True))
-
-
-def test_path_with_id_parameter(snapshot):
-    path = '/authors/{id}/'
-    method = 'GET'
-
-    view = create_view_with_kw(
-        views.AuthorViewSet,
-        method,
-        create_request(path),
-        {'get': 'retrieve'}
-    )
-    inspector = AutoSchema()
-    inspector.view = view
-
-    operation = inspector.get_operation(path, method)
-    snapshot.assert_match(json.dumps(operation, indent=2, sort_keys=True))
-
-
-def test_post_request(snapshot):
-    method = 'POST'
-    path = '/authors/'
-
-    view = create_view_with_kw(
-        views.AuthorViewSet,
-        method,
-        create_request(path),
-        {'post': 'create'}
-    )
-    inspector = AutoSchema()
-    inspector.view = view
-
-    operation = inspector.get_operation(path, method)
-    snapshot.assert_match(json.dumps(operation, indent=2, sort_keys=True))
-
-
-def test_patch_request(snapshot):
-    method = 'PATCH'
-    path = '/authors/{id}'
-
-    view = create_view_with_kw(
-        views.AuthorViewSet,
-        method,
-        create_request(path),
-        {'patch': 'update'}
-    )
-    inspector = AutoSchema()
-    inspector.view = view
-
-    operation = inspector.get_operation(path, method)
-    snapshot.assert_match(json.dumps(operation, indent=2, sort_keys=True))
-
-
-def test_delete_request(snapshot):
-    method = 'DELETE'
-    path = '/authors/{id}'
-
-    view = create_view_with_kw(
-        views.AuthorViewSet,
-        method,
-        create_request(path),
-        {'delete': 'delete'}
-    )
-    inspector = AutoSchema()
-    inspector.view = view
-
-    operation = inspector.get_operation(path, method)
-    snapshot.assert_match(json.dumps(operation, indent=2, sort_keys=True))
-
-
-@override_settings(REST_FRAMEWORK={
-    'DEFAULT_SCHEMA_CLASS': 'rest_framework_json_api.schemas.openapi.AutoSchema'})
-def test_schema_construction():
-    """Construction of the top level dictionary."""
-    patterns = [
-        re_path('^authors/?$', views.AuthorViewSet.as_view({'get': 'list'})),
-    ]
-    generator = SchemaGenerator(patterns=patterns)
-
-    request = create_request('/')
-    schema = generator.get_schema(request=request)
-
-    assert 'openapi' in schema
-    assert 'info' in schema
-    assert 'paths' in schema
-    assert 'components' in schema
-
-
-def test_schema_related_serializers():
-    """
-    Confirm that paths are generated for related fields. For example:
-        /authors/{pk}/{related_field>}
-        /authors/{id}/comments/
-        /authors/{id}/entries/
-        /authors/{id}/first_entry/
-    and confirm that the schema for the related field is properly rendered
-    """
-    generator = SchemaGenerator()
-    request = create_request('/')
-    schema = generator.get_schema(request=request)
-    # make sure the path's relationship and related {related_field}'s got expanded
-    assert '/authors/{id}/relationships/{related_field}' in schema['paths']
-    assert '/authors/{id}/comments/' in schema['paths']
-    assert '/authors/{id}/entries/' in schema['paths']
-    assert '/authors/{id}/first_entry/' in schema['paths']
-    first_get = schema['paths']['/authors/{id}/first_entry/']['get']['responses']['200']
-    first_schema = first_get['content']['application/vnd.api+json']['schema']
-    first_props = first_schema['properties']['data']
-    assert '$ref' in first_props
-    assert first_props['$ref'] == '#/components/schemas/Entry'
diff --git a/example/tests/test_parsers.py b/example/tests/test_parsers.py
deleted file mode 100644
index 38d7c6d7..00000000
--- a/example/tests/test_parsers.py
+++ /dev/null
@@ -1,152 +0,0 @@
-import json
-from io import BytesIO
-
-from django.test import TestCase, override_settings
-from django.urls import path, reverse
-from rest_framework import status, views
-from rest_framework.exceptions import ParseError
-from rest_framework.response import Response
-from rest_framework.test import APITestCase
-
-from rest_framework_json_api import serializers
-from rest_framework_json_api.parsers import JSONParser
-from rest_framework_json_api.renderers import JSONRenderer
-
-
-class TestJSONParser(TestCase):
-
-    def setUp(self):
-        class MockRequest(object):
-
-            def __init__(self):
-                self.method = 'GET'
-
-        request = MockRequest()
-
-        self.parser_context = {'request': request, 'kwargs': {}, 'view': 'BlogViewSet'}
-
-        data = {
-            'data': {
-                'id': 123,
-                'type': 'Blog',
-                'attributes': {
-                    'json-value': {'JsonKey': 'JsonValue'}
-                },
-            },
-            'meta': {
-                'random_key': 'random_value'
-            }
-        }
-
-        self.string = json.dumps(data)
-
-    @override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize')
-    def test_parse_include_metadata_format_field_names(self):
-        parser = JSONParser()
-
-        stream = BytesIO(self.string.encode('utf-8'))
-        data = parser.parse(stream, None, self.parser_context)
-
-        self.assertEqual(data['_meta'], {'random_key': 'random_value'})
-        self.assertEqual(data['json_value'], {'JsonKey': 'JsonValue'})
-
-    def test_parse_invalid_data(self):
-        parser = JSONParser()
-
-        string = json.dumps([])
-        stream = BytesIO(string.encode('utf-8'))
-
-        with self.assertRaises(ParseError):
-            parser.parse(stream, None, self.parser_context)
-
-    def test_parse_invalid_data_key(self):
-        parser = JSONParser()
-
-        string = json.dumps({
-            'data': [{
-                'id': 123,
-                'type': 'Blog',
-                'attributes': {
-                    'json-value': {'JsonKey': 'JsonValue'}
-                },
-            }]
-        })
-        stream = BytesIO(string.encode('utf-8'))
-
-        with self.assertRaises(ParseError):
-            parser.parse(stream, None, self.parser_context)
-
-
-class DummyDTO:
-    def __init__(self, response_dict):
-        for k, v in response_dict.items():
-            setattr(self, k, v)
-
-    @property
-    def pk(self):
-        return self.id if hasattr(self, 'id') else None
-
-
-class DummySerializer(serializers.Serializer):
-    body = serializers.CharField()
-    id = serializers.IntegerField()
-
-
-class DummyAPIView(views.APIView):
-    parser_classes = [JSONParser]
-    renderer_classes = [JSONRenderer]
-    resource_name = 'dummy'
-
-    def patch(self, request, *args, **kwargs):
-        serializer = DummySerializer(DummyDTO(request.data))
-        return Response(status=status.HTTP_200_OK, data=serializer.data)
-
-
-urlpatterns = [
-    path('repeater', DummyAPIView.as_view(), name='repeater'),
-]
-
-
-class TestParserOnAPIView(APITestCase):
-
-    def setUp(self):
-        class MockRequest(object):
-            def __init__(self):
-                self.method = 'PATCH'
-
-        request = MockRequest()
-        # To be honest view string isn't resolved into actual view
-        self.parser_context = {'request': request, 'kwargs': {}, 'view': 'DummyAPIView'}
-
-        self.data = {
-            'data': {
-                'id': 123,
-                'type': 'strs',
-                'attributes': {
-                    'body': 'hello'
-                },
-            }
-        }
-
-        self.string = json.dumps(self.data)
-
-    def test_patch_doesnt_raise_attribute_error(self):
-        parser = JSONParser()
-
-        stream = BytesIO(self.string.encode('utf-8'))
-
-        data = parser.parse(stream, None, self.parser_context)
-
-        assert data['id'] == 123
-        assert data['body'] == 'hello'
-
-    @override_settings(ROOT_URLCONF=__name__)
-    def test_patch_request(self):
-        url = reverse('repeater')
-        data = self.data
-        data['data']['type'] = 'dummy'
-        response = self.client.patch(url, data=data)
-        data = response.json()
-
-        assert data['data']['id'] == str(123)
-        assert data['data']['attributes']['body'] == 'hello'
diff --git a/example/tests/test_performance.py b/example/tests/test_performance.py
index ac8b5956..ec4a81b2 100644
--- a/example/tests/test_performance.py
+++ b/example/tests/test_performance.py
@@ -1,51 +1,59 @@
+from datetime import date, timedelta
+from random import randint
+
 from django.utils import timezone
 from rest_framework.test import APITestCase
 
-from example.factories import CommentFactory
-from example.models import Author, Blog, Comment, Entry
+from example.factories import CommentFactory, EntryFactory
+from example.models import Author, Blog, Comment, Entry, LabResults, ResearchProject
 
 
 class PerformanceTestCase(APITestCase):
     def setUp(self):
-        self.author = Author.objects.create(name='Super powerful superhero', email='i.am@lost.com')
-        self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
-        self.other_blog = Blog.objects.create(name='Other blog', tagline="It's another blog")
+        self.author = Author.objects.create(
+            name="Super powerful superhero", email="i.am@lost.com"
+        )
+        self.blog = Blog.objects.create(name="Some Blog", tagline="It's a blog")
+        self.other_blog = Blog.objects.create(
+            name="Other blog", tagline="It's another blog"
+        )
         self.first_entry = Entry.objects.create(
             blog=self.blog,
-            headline='headline one',
-            body_text='body_text two',
+            headline="headline one",
+            body_text="body_text two",
             pub_date=timezone.now(),
             mod_date=timezone.now(),
             n_comments=0,
             n_pingbacks=0,
-            rating=3
+            rating=3,
         )
         self.second_entry = Entry.objects.create(
             blog=self.blog,
-            headline='headline two',
-            body_text='body_text one',
+            headline="headline two",
+            body_text="body_text one",
             pub_date=timezone.now(),
             mod_date=timezone.now(),
             n_comments=0,
             n_pingbacks=0,
-            rating=1
+            rating=1,
         )
         self.comment = Comment.objects.create(entry=self.first_entry)
         CommentFactory.create_batch(50)
+        EntryFactory.create_batch(50)
 
     def test_query_count_no_includes(self):
-        """ We expect a simple list view to issue only two queries.
+        """We expect a simple list view to issue only two queries.
 
         1. The number of results in the set (e.g. a COUNT query),
            only necessary because we're using PageNumberPagination
         2. The SELECT query for the set
         """
         with self.assertNumQueries(2):
-            response = self.client.get('/comments?page[size]=25')
-            self.assertEqual(len(response.data['results']), 25)
+            response = self.client.get("/comments?page[size]=25")
+            self.assertEqual(len(response.data["results"]), 25)
 
     def test_query_count_include_author(self):
-        """ We expect a list view with an include have three queries:
+        """We expect a list view with an include have five queries:
 
         1. Primary resource COUNT query
         2. Primary resource SELECT
@@ -54,15 +62,57 @@ def test_query_count_include_author(self):
         5. Entries prefetched
         """
         with self.assertNumQueries(5):
-            response = self.client.get('/comments?include=author&page[size]=25')
-            self.assertEqual(len(response.data['results']), 25)
+            response = self.client.get("/comments?include=author&page[size]=25")
+            self.assertEqual(len(response.data["results"]), 25)
 
     def test_query_select_related_entry(self):
-        """ We expect a list view with an include have two queries:
+        """We expect a list view with an include have two queries:
 
         1. Primary resource COUNT query
         2. Primary resource SELECT + SELECT RELATED writer(author) and bio
         """
         with self.assertNumQueries(2):
-            response = self.client.get('/comments?include=writer&page[size]=25')
-            self.assertEqual(len(response.data['results']), 25)
+            response = self.client.get("/comments?include=writer&page[size]=25")
+            self.assertEqual(len(response.data["results"]), 25)
+
+    def test_query_prefetch_uses_included_resources(self):
+        """We expect a list view with `included_resources` to have three queries:
+
+        1. Primary resource COUNT query
+        2. Primary resource SELECT
+        3. Comments prefetched
+        """
+        with self.assertNumQueries(3):
+            response = self.client.get(
+                "/entries?fields[entries]=comments&page[size]=25"
+            )
+            self.assertEqual(len(response.data["results"]), 25)
+
+    def test_query_prefetch_read_only(self):
+        """We expect a read only list view with an include have five queries:
+
+        1. Primary resource COUNT query
+        2. Primary resource SELECT
+        3. Authors prefetched
+        4. Author types prefetched
+        5. Entries prefetched
+        """
+        project = ResearchProject.objects.create(
+            topic="Mars Mission", supervisor="Elon Musk"
+        )
+
+        LabResults.objects.bulk_create(
+            [
+                LabResults(
+                    research_project=project,
+                    date=date.today() + timedelta(days=i),
+                    measurements=randint(0, 10000),
+                    author=self.author,
+                )
+                for i in range(20)
+            ]
+        )
+
+        with self.assertNumQueries(5):
+            response = self.client.get("/lab-results?include=author&page[size]=25")
+            self.assertEqual(len(response.data["results"]), 20)
diff --git a/example/tests/test_relations.py b/example/tests/test_relations.py
deleted file mode 100644
index ef1dfb02..00000000
--- a/example/tests/test_relations.py
+++ /dev/null
@@ -1,309 +0,0 @@
-from __future__ import absolute_import
-
-from django.test.client import RequestFactory
-from django.utils import timezone
-from rest_framework import serializers
-from rest_framework.fields import SkipField
-from rest_framework.reverse import reverse
-
-from rest_framework_json_api.exceptions import Conflict
-from rest_framework_json_api.relations import (
-    HyperlinkedRelatedField,
-    ResourceRelatedField,
-    SerializerMethodHyperlinkedRelatedField
-)
-from rest_framework_json_api.utils import format_resource_type
-
-from . import TestBase
-from example.models import Author, Blog, Comment, Entry
-from example.serializers import CommentSerializer
-from example.views import EntryViewSet
-
-
-class TestResourceRelatedField(TestBase):
-
-    def setUp(self):
-        super(TestResourceRelatedField, self).setUp()
-        self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
-        self.entry = Entry.objects.create(
-            blog=self.blog,
-            headline='headline',
-            body_text='body_text',
-            pub_date=timezone.now(),
-            mod_date=timezone.now(),
-            n_comments=0,
-            n_pingbacks=0,
-            rating=3
-        )
-        for i in range(1, 6):
-            name = 'some_author{}'.format(i)
-            self.entry.authors.add(
-                Author.objects.create(name=name, email='{}@example.org'.format(name))
-            )
-
-        self.comment = Comment.objects.create(
-            entry=self.entry,
-            body='testing one two three',
-            author=Author.objects.first()
-        )
-
-    def test_data_in_correct_format_when_instantiated_with_blog_object(self):
-        serializer = BlogFKSerializer(instance={'blog': self.blog})
-
-        expected_data = {
-            'type': format_resource_type('Blog'),
-            'id': str(self.blog.id)
-        }
-
-        actual_data = serializer.data['blog']
-
-        self.assertEqual(actual_data, expected_data)
-
-    def test_data_in_correct_format_when_instantiated_with_entry_object(self):
-        serializer = EntryFKSerializer(instance={'entry': self.entry})
-
-        expected_data = {
-            'type': format_resource_type('Entry'),
-            'id': str(self.entry.id)
-        }
-
-        actual_data = serializer.data['entry']
-
-        self.assertEqual(actual_data, expected_data)
-
-    def test_deserialize_primitive_data_blog(self):
-        serializer = BlogFKSerializer(data={
-            'blog': {
-                'type': format_resource_type('Blog'),
-                'id': str(self.blog.id)
-            }
-        }
-        )
-
-        self.assertTrue(serializer.is_valid())
-        self.assertEqual(serializer.validated_data['blog'], self.blog)
-
-    def test_validation_fails_for_wrong_type(self):
-        with self.assertRaises(Conflict) as cm:
-            serializer = BlogFKSerializer(data={
-                'blog': {
-                    'type': 'Entries',
-                    'id': str(self.blog.id)
-                }
-            }
-            )
-            serializer.is_valid()
-        the_exception = cm.exception
-        self.assertEqual(the_exception.status_code, 409)
-
-    def test_serialize_many_to_many_relation(self):
-        serializer = EntryModelSerializer(instance=self.entry)
-
-        type_string = format_resource_type('Author')
-        author_pks = Author.objects.values_list('pk', flat=True)
-        expected_data = [{'type': type_string, 'id': str(pk)} for pk in author_pks]
-
-        self.assertEqual(
-            serializer.data['authors'],
-            expected_data
-        )
-
-    def test_deserialize_many_to_many_relation(self):
-        type_string = format_resource_type('Author')
-        author_pks = Author.objects.values_list('pk', flat=True)
-        authors = [{'type': type_string, 'id': pk} for pk in author_pks]
-
-        serializer = EntryModelSerializer(data={'authors': authors, 'comments': []})
-
-        self.assertTrue(serializer.is_valid())
-        self.assertEqual(len(serializer.validated_data['authors']), Author.objects.count())
-        for author in serializer.validated_data['authors']:
-            self.assertIsInstance(author, Author)
-
-    def test_read_only(self):
-        serializer = EntryModelSerializer(
-            data={'authors': [], 'comments': [{'type': 'Comments', 'id': 2}]}
-        )
-        serializer.is_valid(raise_exception=True)
-        self.assertNotIn('comments', serializer.validated_data)
-
-    def test_invalid_resource_id_object(self):
-        comment = {'body': 'testing 123', 'entry': {'type': 'entry'}, 'author': {'id': '5'}}
-        serializer = CommentSerializer(data=comment)
-        assert not serializer.is_valid()
-        assert serializer.errors == {
-            'author': ["Invalid resource identifier object: missing 'type' attribute"],
-            'entry': ["Invalid resource identifier object: missing 'id' attribute"]
-        }
-
-
-class TestHyperlinkedFieldBase(TestBase):
-
-    def setUp(self):
-        super(TestHyperlinkedFieldBase, self).setUp()
-        self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
-        self.entry = Entry.objects.create(
-            blog=self.blog,
-            headline='headline',
-            body_text='body_text',
-            pub_date=timezone.now(),
-            mod_date=timezone.now(),
-            n_comments=0,
-            n_pingbacks=0,
-            rating=3
-        )
-        self.comment = Comment.objects.create(
-            entry=self.entry,
-            body='testing one two three',
-        )
-
-        self.request = RequestFactory().get(reverse('entry-detail', kwargs={'pk': self.entry.pk}))
-        self.view = EntryViewSet(request=self.request, kwargs={'entry_pk': self.entry.id})
-
-
-class TestHyperlinkedRelatedField(TestHyperlinkedFieldBase):
-
-    def test_single_hyperlinked_related_field(self):
-        field = HyperlinkedRelatedField(
-            related_link_view_name='entry-blog',
-            related_link_url_kwarg='entry_pk',
-            self_link_view_name='entry-relationships',
-            read_only=True,
-        )
-        field._context = {'request': self.request, 'view': self.view}
-        field.field_name = 'blog'
-
-        self.assertRaises(NotImplementedError, field.to_representation, self.entry)
-        self.assertRaises(SkipField, field.get_attribute, self.entry)
-
-        links_expected = {
-            'self': 'http://testserver/entries/{}/relationships/blog'.format(self.entry.pk),
-            'related': 'http://testserver/entries/{}/blog'.format(self.entry.pk)
-        }
-        got = field.get_links(self.entry)
-        self.assertEqual(got, links_expected)
-
-    def test_many_hyperlinked_related_field(self):
-        field = HyperlinkedRelatedField(
-            related_link_view_name='entry-comments',
-            related_link_url_kwarg='entry_pk',
-            self_link_view_name='entry-relationships',
-            read_only=True,
-            many=True
-        )
-        field._context = {'request': self.request, 'view': self.view}
-        field.field_name = 'comments'
-
-        self.assertRaises(NotImplementedError, field.to_representation, self.entry.comments.all())
-        self.assertRaises(SkipField, field.get_attribute, self.entry)
-
-        links_expected = {
-            'self': 'http://testserver/entries/{}/relationships/comments'.format(self.entry.pk),
-            'related': 'http://testserver/entries/{}/comments'.format(self.entry.pk)
-        }
-        got = field.child_relation.get_links(self.entry)
-        self.assertEqual(got, links_expected)
-
-
-class TestSerializerMethodHyperlinkedRelatedField(TestHyperlinkedFieldBase):
-
-    def test_single_serializer_method_hyperlinked_related_field(self):
-        serializer = EntryModelSerializerWithHyperLinks(
-            instance=self.entry,
-            context={
-                'request': self.request,
-                'view': self.view
-            }
-        )
-        field = serializer.fields['blog']
-
-        self.assertRaises(NotImplementedError, field.to_representation, self.entry)
-        self.assertRaises(SkipField, field.get_attribute, self.entry)
-
-        expected = {
-            'self': 'http://testserver/entries/{}/relationships/blog'.format(self.entry.pk),
-            'related': 'http://testserver/entries/{}/blog'.format(self.entry.pk)
-        }
-        got = field.get_links(self.entry)
-        self.assertEqual(got, expected)
-
-    def test_many_serializer_method_hyperlinked_related_field(self):
-        serializer = EntryModelSerializerWithHyperLinks(
-            instance=self.entry,
-            context={
-                'request': self.request,
-                'view': self.view
-            }
-        )
-        field = serializer.fields['comments']
-
-        self.assertRaises(NotImplementedError, field.to_representation, self.entry)
-        self.assertRaises(SkipField, field.get_attribute, self.entry)
-
-        expected = {
-            'self': 'http://testserver/entries/{}/relationships/comments'.format(self.entry.pk),
-            'related': 'http://testserver/entries/{}/comments'.format(self.entry.pk)
-        }
-        got = field.get_links(self.entry)
-        self.assertEqual(got, expected)
-
-    def test_get_blog(self):
-        serializer = EntryModelSerializerWithHyperLinks(instance=self.entry)
-        got = serializer.get_blog(self.entry)
-        expected = self.entry.blog
-
-        self.assertEqual(got, expected)
-
-    def test_get_comments(self):
-        serializer = EntryModelSerializerWithHyperLinks(instance=self.entry)
-        got = serializer.get_comments(self.entry)
-        expected = self.entry.comments.all()
-
-        self.assertListEqual(list(got), list(expected))
-
-
-class BlogResourceRelatedField(ResourceRelatedField):
-    def get_queryset(self):
-        return Blog.objects
-
-
-class BlogFKSerializer(serializers.Serializer):
-    blog = BlogResourceRelatedField()
-
-
-class EntryFKSerializer(serializers.Serializer):
-    entry = ResourceRelatedField(queryset=Entry.objects)
-
-
-class EntryModelSerializer(serializers.ModelSerializer):
-    authors = ResourceRelatedField(many=True, queryset=Author.objects)
-    comments = ResourceRelatedField(many=True, read_only=True)
-
-    class Meta:
-        model = Entry
-        fields = ('authors', 'comments')
-
-
-class EntryModelSerializerWithHyperLinks(serializers.ModelSerializer):
-    blog = SerializerMethodHyperlinkedRelatedField(
-        related_link_view_name='entry-blog',
-        related_link_url_kwarg='entry_pk',
-        self_link_view_name='entry-relationships',
-        many=True,
-    )
-    comments = SerializerMethodHyperlinkedRelatedField(
-        related_link_view_name='entry-comments',
-        related_link_url_kwarg='entry_pk',
-        self_link_view_name='entry-relationships',
-        many=True,
-    )
-
-    class Meta:
-        model = Entry
-        fields = ('blog', 'comments',)
-
-    def get_blog(self, obj):
-        return obj.blog
-
-    def get_comments(self, obj):
-        return obj.comments.all()
diff --git a/example/tests/test_serializers.py b/example/tests/test_serializers.py
index de97dcf8..9ad487e5 100644
--- a/example/tests/test_serializers.py
+++ b/example/tests/test_serializers.py
@@ -1,3 +1,4 @@
+from datetime import datetime
 from unittest import mock
 
 import pytest
@@ -11,7 +12,7 @@
     DateField,
     ModelSerializer,
     ResourceIdentifierObjectSerializer,
-    empty
+    empty,
 )
 from rest_framework_json_api.utils import format_resource_type
 
@@ -25,37 +26,40 @@
 
 class TestResourceIdentifierObjectSerializer(TestCase):
     def setUp(self):
-        self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
+        self.blog = Blog.objects.create(name="Some Blog", tagline="It's a blog")
         now = timezone.now()
 
         self.entry = Entry.objects.create(
             blog=self.blog,
-            headline='headline',
-            body_text='body_text',
+            headline="headline",
+            body_text="body_text",
             pub_date=now.date(),
             mod_date=now.date(),
             n_comments=0,
             n_pingbacks=0,
-            rating=3
+            rating=3,
         )
         for i in range(1, 6):
-            name = 'some_author{}'.format(i)
+            name = f"some_author{i}"
             self.entry.authors.add(
-                Author.objects.create(name=name, email='{}@example.org'.format(name))
+                Author.objects.create(name=name, email=f"{name}@example.org")
             )
 
     def test_forward_relationship_not_loaded_when_not_included(self):
-        to_representation_method = 'example.serializers.TaggedItemSerializer.to_representation'
+        to_representation_method = (
+            "example.serializers.TaggedItemSerializer.to_representation"
+        )
         with mock.patch(to_representation_method) as mocked_serializer:
+
             class EntrySerializer(ModelSerializer):
                 blog = BlogSerializer()
 
                 class Meta:
                     model = Entry
-                    fields = '__all__'
+                    fields = "__all__"
 
-            request_without_includes = Request(request_factory.get('/'))
-            serializer = EntrySerializer(context={'request': request_without_includes})
+            request_without_includes = Request(request_factory.get("/"))
+            serializer = EntrySerializer(context={"request": request_without_includes})
             serializer.to_representation(self.entry)
 
             mocked_serializer.assert_not_called()
@@ -66,62 +70,74 @@ class EntrySerializer(ModelSerializer):
 
             class Meta:
                 model = Entry
-                fields = '__all__'
+                fields = "__all__"
 
-        request_without_includes = Request(request_factory.get('/'))
-        serializer = EntrySerializer(context={'request': request_without_includes})
+        request_without_includes = Request(request_factory.get("/"))
+        serializer = EntrySerializer(context={"request": request_without_includes})
         result = serializer.to_representation(self.entry)
 
         # Remove non deterministic fields
-        result.pop('created_at')
-        result.pop('modified_at')
+        result.pop("created_at")
+        result.pop("modified_at")
 
         expected = dict(
             [
-                ('id', 1),
-                ('blog', dict([
-                    ('name', 'Some Blog'),
-                    ('tags', []),
-                    ('copyright', 2020),
-                    ('url', 'http://testserver/blogs/1')
-                ])),
-                ('headline', 'headline'),
-                ('body_text', 'body_text'),
-                ('pub_date', DateField().to_representation(self.entry.pub_date)),
-                ('mod_date', DateField().to_representation(self.entry.mod_date)),
-                ('n_comments', 0),
-                ('n_pingbacks', 0),
-                ('rating', 3),
-                ('authors',
+                ("id", 1),
+                (
+                    "blog",
+                    dict(
+                        [
+                            ("name", "Some Blog"),
+                            ("tags", []),
+                            ("copyright", datetime.now().year),
+                            ("url", "http://testserver/blogs/1"),
+                        ]
+                    ),
+                ),
+                ("headline", "headline"),
+                ("body_text", "body_text"),
+                ("pub_date", DateField().to_representation(self.entry.pub_date)),
+                ("mod_date", DateField().to_representation(self.entry.mod_date)),
+                ("n_comments", 0),
+                ("n_pingbacks", 0),
+                ("rating", 3),
+                (
+                    "authors",
                     [
-                        dict([('type', 'authors'), ('id', '1')]),
-                        dict([('type', 'authors'), ('id', '2')]),
-                        dict([('type', 'authors'), ('id', '3')]),
-                        dict([('type', 'authors'), ('id', '4')]),
-                        dict([('type', 'authors'), ('id', '5')])])])
+                        dict([("type", "authors"), ("id", "1")]),
+                        dict([("type", "authors"), ("id", "2")]),
+                        dict([("type", "authors"), ("id", "3")]),
+                        dict([("type", "authors"), ("id", "4")]),
+                        dict([("type", "authors"), ("id", "5")]),
+                    ],
+                ),
+            ]
+        )
 
         self.assertDictEqual(expected, result)
 
     def test_data_in_correct_format_when_instantiated_with_blog_object(self):
         serializer = ResourceIdentifierObjectSerializer(instance=self.blog)
 
-        expected_data = {'type': format_resource_type('Blog'), 'id': str(self.blog.id)}
+        expected_data = {"type": format_resource_type("Blog"), "id": str(self.blog.id)}
 
         assert serializer.data == expected_data
 
     def test_data_in_correct_format_when_instantiated_with_entry_object(self):
         serializer = ResourceIdentifierObjectSerializer(instance=self.entry)
 
-        expected_data = {'type': format_resource_type('Entry'), 'id': str(self.entry.id)}
+        expected_data = {
+            "type": format_resource_type("Entry"),
+            "id": str(self.entry.id),
+        }
 
         assert serializer.data == expected_data
 
     def test_deserialize_primitive_data_blog(self):
-        initial_data = {
-            'type': format_resource_type('Blog'),
-            'id': str(self.blog.id)
-        }
-        serializer = ResourceIdentifierObjectSerializer(data=initial_data, model_class=Blog)
+        initial_data = {"type": format_resource_type("Blog"), "id": str(self.blog.id)}
+        serializer = ResourceIdentifierObjectSerializer(
+            data=initial_data, model_class=Blog
+        )
 
         self.assertTrue(serializer.is_valid(), msg=serializer.errors)
         assert serializer.validated_data == self.blog
@@ -131,29 +147,28 @@ def test_deserialize_primitive_data_blog_with_unexisting_pk(self):
         self.blog.delete()
         assert not Blog.objects.filter(id=unexisting_pk).exists()
 
-        initial_data = {
-            'type': format_resource_type('Blog'),
-            'id': str(unexisting_pk)
-        }
-        serializer = ResourceIdentifierObjectSerializer(data=initial_data, model_class=Blog)
+        initial_data = {"type": format_resource_type("Blog"), "id": str(unexisting_pk)}
+        serializer = ResourceIdentifierObjectSerializer(
+            data=initial_data, model_class=Blog
+        )
 
         self.assertFalse(serializer.is_valid())
-        self.assertEqual(serializer.errors[0].code, 'does_not_exist')
+        self.assertEqual(serializer.errors[0].code, "does_not_exist")
 
     def test_data_in_correct_format_when_instantiated_with_queryset(self):
         qs = Author.objects.all()
         serializer = ResourceIdentifierObjectSerializer(instance=qs, many=True)
 
-        type_string = format_resource_type('Author')
-        author_pks = Author.objects.values_list('pk', flat=True)
-        expected_data = [{'type': type_string, 'id': str(pk)} for pk in author_pks]
+        type_string = format_resource_type("Author")
+        author_pks = Author.objects.values_list("pk", flat=True)
+        expected_data = [{"type": type_string, "id": str(pk)} for pk in author_pks]
 
         assert serializer.data == expected_data
 
     def test_deserialize_many(self):
-        type_string = format_resource_type('Author')
-        author_pks = Author.objects.values_list('pk', flat=True)
-        initial_data = [{'type': type_string, 'id': str(pk)} for pk in author_pks]
+        type_string = format_resource_type("Author")
+        author_pks = Author.objects.values_list("pk", flat=True)
+        initial_data = [{"type": type_string, "id": str(pk)} for pk in author_pks]
 
         serializer = ResourceIdentifierObjectSerializer(
             data=initial_data, model_class=Author, many=True
@@ -164,39 +179,80 @@ def test_deserialize_many(self):
         print(serializer.data)
 
 
-class TestModelSerializer(object):
+class TestModelSerializer:
     def test_model_serializer_with_implicit_fields(self, comment, client):
         expected = {
             "data": {
                 "type": "comments",
                 "id": str(comment.pk),
-                "attributes": {
-                    "body": comment.body
-                },
+                "attributes": {"body": comment.body},
                 "relationships": {
-                    "entry": {
-                        "data": {
-                            "type": "entries",
-                            "id": str(comment.entry.pk)
-                        }
-                    },
+                    "entry": {"data": {"type": "entries", "id": str(comment.entry.pk)}},
                     "author": {
-                        "data": {
-                            "type": "authors",
-                            "id": str(comment.author.pk)
-                        }
+                        "data": {"type": "authors", "id": str(comment.author.pk)}
                     },
                     "writer": {
-                        "data": {
-                            "type": "writers",
-                            "id": str(comment.author.pk)
+                        "data": {"type": "writers", "id": str(comment.author.pk)}
+                    },
+                },
+                "meta": {
+                    "modifiedDaysAgo": (datetime.now() - comment.modified_at).days
+                },
+            },
+            "included": [
+                {
+                    "attributes": {
+                        "email": comment.author.email,
+                        "name": comment.author.name,
+                    },
+                    "id": str(comment.author.pk),
+                    "relationships": {
+                        "bio": {
+                            "data": {
+                                "id": str(comment.author.bio.pk),
+                                "type": "authorBios",
+                            }
                         }
                     },
+                    "type": "writers",
                 }
-            }
+            ],
         }
 
-        response = client.get(reverse("comment-detail", kwargs={'pk': comment.pk}))
+        response = client.get(reverse("comment-detail", kwargs={"pk": comment.pk}))
+
+        assert response.status_code == 200
+        assert expected == response.json()
+
+    def test_model_serializer_with_subserializers(self, questionnaire, client):
+        expected = {
+            "data": {
+                "type": "questionnaires",
+                "id": str(questionnaire.pk),
+                "attributes": {
+                    "name": questionnaire.name,
+                    "questions": [
+                        {
+                            "text": "What is your name?",
+                            "required": True,
+                        },
+                        {
+                            "text": "What is your quest?",
+                            "required": False,
+                        },
+                        {
+                            "text": "What is the air-speed velocity of an unladen swallow?",
+                            "required": False,
+                        },
+                    ],
+                    "metadata": {"author": "Bridgekeeper", "producer": None},
+                },
+            },
+        }
+
+        response = client.get(
+            reverse("questionnaire-detail", kwargs={"pk": questionnaire.pk})
+        )
 
         assert response.status_code == 200
         assert expected == response.json()
diff --git a/example/tests/test_sideload_resources.py b/example/tests/test_sideload_resources.py
index 69641af7..51bfb841 100644
--- a/example/tests/test_sideload_resources.py
+++ b/example/tests/test_sideload_resources.py
@@ -1,6 +1,7 @@
 """
 Test sideloading resources
 """
+
 import json
 
 from django.urls import reverse
@@ -13,7 +14,8 @@ class SideloadResourceTest(TestBase):
     """
     Test that sideloading resources returns expected output.
     """
-    url = reverse('user-posts')
+
+    url = reverse("user-posts")
 
     def test_get_sideloaded_data(self):
         """
@@ -21,9 +23,9 @@ def test_get_sideloaded_data(self):
         do not return a single root key.
         """
         response = self.client.get(self.url)
-        content = json.loads(response.content.decode('utf8'))
+        content = json.loads(response.content.decode("utf8"))
 
         self.assertEqual(
             sorted(content.keys()),
-            [encoding.force_str('identities'),
-             encoding.force_str('posts')])
+            [encoding.force_str("identities"), encoding.force_str("posts")],
+        )
diff --git a/example/tests/test_utils.py b/example/tests/test_utils.py
deleted file mode 100644
index 4fff3225..00000000
--- a/example/tests/test_utils.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""
-Test rest_framework_json_api's utils functions.
-"""
-from rest_framework_json_api import utils
-
-from example.serializers import AuthorSerializer, EntrySerializer
-from example.tests import TestBase
-
-
-class GetRelatedResourceTests(TestBase):
-    """
-    Ensure the `get_related_resource_type` function returns correct types.
-    """
-
-    def test_reverse_relation(self):
-        """
-        Ensure reverse foreign keys have their types identified correctly.
-        """
-        serializer = EntrySerializer()
-        field = serializer.fields['comments']
-
-        self.assertEqual(utils.get_related_resource_type(field), 'comments')
-
-    def test_m2m_relation(self):
-        """
-        Ensure m2ms have their types identified correctly.
-        """
-        serializer = EntrySerializer()
-        field = serializer.fields['authors']
-
-        self.assertEqual(utils.get_related_resource_type(field), 'authors')
-
-    def test_m2m_reverse_relation(self):
-        """
-        Ensure reverse m2ms have their types identified correctly.
-        """
-        serializer = AuthorSerializer()
-        field = serializer.fields['entries']
-
-        self.assertEqual(utils.get_related_resource_type(field), 'entries')
diff --git a/example/tests/test_views.py b/example/tests/test_views.py
index 9cd493f7..47d4adb1 100644
--- a/example/tests/test_views.py
+++ b/example/tests/test_views.py
@@ -1,262 +1,326 @@
 import json
-from datetime import datetime
 
-from django.test import RequestFactory
+from django.test import RequestFactory, override_settings
 from django.utils import timezone
 from rest_framework import status
-from rest_framework.decorators import action
 from rest_framework.exceptions import NotFound
 from rest_framework.request import Request
-from rest_framework.response import Response
 from rest_framework.reverse import reverse
 from rest_framework.test import APIRequestFactory, APITestCase, force_authenticate
 
-from rest_framework_json_api import serializers, views
 from rest_framework_json_api.utils import format_resource_type
 
 from example.factories import AuthorFactory, CommentFactory, EntryFactory
 from example.models import Author, Blog, Comment, Entry
-from example.serializers import AuthorBioSerializer, AuthorTypeSerializer, EntrySerializer
+from example.serializers import (
+    AuthorBioSerializer,
+    AuthorTypeSerializer,
+    EntrySerializer,
+)
 from example.tests import TestBase
 from example.views import AuthorViewSet, BlogViewSet
 
 
 class TestRelationshipView(APITestCase):
     def setUp(self):
-        self.author = Author.objects.create(name='Super powerful superhero', email='i.am@lost.com')
-        self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
-        self.other_blog = Blog.objects.create(name='Other blog', tagline="It's another blog")
+        self.author = Author.objects.create(
+            name="Super powerful superhero", email="i.am@lost.com"
+        )
+        self.blog = Blog.objects.create(name="Some Blog", tagline="It's a blog")
+        self.other_blog = Blog.objects.create(
+            name="Other blog", tagline="It's another blog"
+        )
         self.first_entry = Entry.objects.create(
             blog=self.blog,
-            headline='headline one',
-            body_text='body_text two',
+            headline="headline one",
+            body_text="body_text two",
             pub_date=timezone.now(),
             mod_date=timezone.now(),
             n_comments=0,
             n_pingbacks=0,
-            rating=3
+            rating=3,
         )
         self.second_entry = Entry.objects.create(
             blog=self.blog,
-            headline='headline two',
-            body_text='body_text one',
+            headline="headline two",
+            body_text="body_text one",
             pub_date=timezone.now(),
             mod_date=timezone.now(),
             n_comments=0,
             n_pingbacks=0,
-            rating=1
+            rating=1,
         )
         self.first_comment = Comment.objects.create(
             entry=self.first_entry, body="This entry is cool", author=None
         )
         self.second_comment = Comment.objects.create(
-            entry=self.second_entry,
-            body="This entry is not cool",
-            author=self.author
+            entry=self.second_entry, body="This entry is not cool", author=self.author
         )
 
     def test_get_entry_relationship_blog(self):
         url = reverse(
-            'entry-relationships', kwargs={'pk': self.first_entry.id, 'related_field': 'blog'}
+            "entry-relationships",
+            kwargs={"pk": self.first_entry.id, "related_field": "blog"},
         )
         response = self.client.get(url)
-        expected_data = {'type': format_resource_type('Blog'), 'id': str(self.first_entry.blog.id)}
+        expected_data = {
+            "type": format_resource_type("Blog"),
+            "id": str(self.first_entry.blog.id),
+        }
 
         assert response.data == expected_data
 
     def test_get_entry_relationship_invalid_field(self):
         response = self.client.get(
-            '/entries/{}/relationships/invalid_field'.format(self.first_entry.id)
+            f"/entries/{self.first_entry.id}/relationships/invalid_field"
         )
 
         assert response.status_code == 404
 
     def test_get_blog_relationship_entry_set(self):
-        response = self.client.get('/blogs/{}/relationships/entry_set'.format(self.blog.id))
-        expected_data = [{'type': format_resource_type('Entry'), 'id': str(self.first_entry.id)},
-                         {'type': format_resource_type('Entry'), 'id': str(self.second_entry.id)}]
+        response = self.client.get(f"/blogs/{self.blog.id}/relationships/entry_set")
+        expected_data = [
+            {"type": format_resource_type("Entry"), "id": str(self.first_entry.id)},
+            {"type": format_resource_type("Entry"), "id": str(self.second_entry.id)},
+        ]
+
+        assert response.data == expected_data
+
+    @override_settings(JSON_API_FORMAT_RELATED_LINKS="dasherize")
+    def test_get_blog_relationship_entry_set_with_formatted_link(self):
+        response = self.client.get(f"/blogs/{self.blog.id}/relationships/entry-set")
+        expected_data = [
+            {"type": format_resource_type("Entry"), "id": str(self.first_entry.id)},
+            {"type": format_resource_type("Entry"), "id": str(self.second_entry.id)},
+        ]
 
         assert response.data == expected_data
 
     def test_put_entry_relationship_blog_returns_405(self):
-        url = '/entries/{}/relationships/blog'.format(self.first_entry.id)
+        url = f"/entries/{self.first_entry.id}/relationships/blog"
         response = self.client.put(url, data={})
         assert response.status_code == 405
 
     def test_patch_invalid_entry_relationship_blog_returns_400(self):
-        url = '/entries/{}/relationships/blog'.format(self.first_entry.id)
-        response = self.client.patch(url, data={'data': {'invalid': ''}})
+        url = f"/entries/{self.first_entry.id}/relationships/blog"
+        response = self.client.patch(url, data={"data": {"invalid": ""}})
         assert response.status_code == 400
 
     def test_relationship_view_errors_format(self):
-        url = '/entries/{}/relationships/blog'.format(self.first_entry.id)
-        response = self.client.patch(url, data={'data': {'invalid': ''}})
+        url = f"/entries/{self.first_entry.id}/relationships/blog"
+        response = self.client.patch(url, data={"data": {"invalid": ""}})
         assert response.status_code == 400
 
-        result = json.loads(response.content.decode('utf-8'))
+        result = json.loads(response.content.decode("utf-8"))
 
-        assert 'data' not in result
-        assert 'errors' in result
+        assert "data" not in result
+        assert "errors" in result
 
     def test_get_empty_to_one_relationship(self):
-        url = '/comments/{}/relationships/author'.format(self.first_entry.id)
+        url = f"/comments/{self.first_entry.id}/relationships/author"
         response = self.client.get(url)
         expected_data = None
 
         assert response.data == expected_data
 
     def test_get_to_many_relationship_self_link(self):
-        url = '/authors/{}/relationships/comments'.format(self.author.id)
+        url = f"/authors/{self.author.id}/relationships/comments"
 
         response = self.client.get(url)
         expected_data = {
-            'links': {'self': 'http://testserver/authors/1/relationships/comments'},
-            'data': [{'id': str(self.second_comment.id), 'type': format_resource_type('Comment')}]
+            "links": {"self": "http://testserver/authors/1/relationships/comments"},
+            "data": [
+                {
+                    "id": str(self.second_comment.id),
+                    "type": format_resource_type("Comment"),
+                }
+            ],
         }
-        assert json.loads(response.content.decode('utf-8')) == expected_data
+        assert json.loads(response.content.decode("utf-8")) == expected_data
 
     def test_patch_to_one_relationship(self):
-        url = '/entries/{}/relationships/blog'.format(self.first_entry.id)
+        url = f"/entries/{self.first_entry.id}/relationships/blog"
         request_data = {
-            'data': {'type': format_resource_type('Blog'), 'id': str(self.other_blog.id)}
+            "data": {
+                "type": format_resource_type("Blog"),
+                "id": str(self.other_blog.id),
+            }
         }
         response = self.client.patch(url, data=request_data)
         assert response.status_code == 200, response.content.decode()
-        assert response.data == request_data['data']
+        assert response.data == request_data["data"]
 
         response = self.client.get(url)
-        assert response.data == request_data['data']
+        assert response.data == request_data["data"]
 
     def test_patch_one_to_many_relationship(self):
-        url = '/blogs/{}/relationships/entry_set'.format(self.first_entry.id)
+        url = f"/blogs/{self.first_entry.id}/relationships/entry_set"
         request_data = {
-            'data': [{'type': format_resource_type('Entry'), 'id': str(self.first_entry.id)}, ]
+            "data": [
+                {"type": format_resource_type("Entry"), "id": str(self.first_entry.id)},
+            ]
         }
         response = self.client.patch(url, data=request_data)
         assert response.status_code == 200, response.content.decode()
-        assert response.data == request_data['data']
+        assert response.data == request_data["data"]
 
         response = self.client.get(url)
-        assert response.data == request_data['data']
+        assert response.data == request_data["data"]
 
         # retry a second time should end up with same result
         response = self.client.patch(url, data=request_data)
         assert response.status_code == 200, response.content.decode()
-        assert response.data == request_data['data']
+        assert response.data == request_data["data"]
 
         response = self.client.get(url)
-        assert response.data == request_data['data']
+        assert response.data == request_data["data"]
 
-    def test_patch_one_to_many_relaitonship_with_none(self):
-        url = '/blogs/{}/relationships/entry_set'.format(self.first_entry.id)
-        request_data = {
-            'data': None
-        }
+    def test_patch_one_to_many_relaitonship_with_empty(self):
+        url = f"/blogs/{self.first_entry.id}/relationships/entry_set"
+
+        request_data = {"data": []}
         response = self.client.patch(url, data=request_data)
-        assert response.status_code == 200, response.content.decode()
+        assert response.status_code == status.HTTP_200_OK
         assert response.data == []
 
         response = self.client.get(url)
         assert response.data == []
 
+    def test_patch_one_to_many_relaitonship_with_none(self):
+        """
+        None for a to many relationship is invalid and should return a request error.
+
+        see https://jsonapi.org/format/#crud-updating-to-many-relationships
+        """
+
+        url = f"/blogs/{self.first_entry.id}/relationships/entry_set"
+
+        request_data = {"data": None}
+        response = self.client.patch(url, data=request_data)
+        assert response.status_code == status.HTTP_400_BAD_REQUEST
+
     def test_patch_many_to_many_relationship(self):
-        url = '/entries/{}/relationships/authors'.format(self.first_entry.id)
+        url = f"/entries/{self.first_entry.id}/relationships/authors"
         request_data = {
-            'data': [
-                {
-                    'type': format_resource_type('Author'),
-                    'id': str(self.author.id)
-                },
+            "data": [
+                {"type": format_resource_type("Author"), "id": str(self.author.id)},
             ]
         }
         response = self.client.patch(url, data=request_data)
         assert response.status_code == 200, response.content.decode()
-        assert response.data == request_data['data']
+        assert response.data == request_data["data"]
 
         response = self.client.get(url)
-        assert response.data == request_data['data']
+        assert response.data == request_data["data"]
 
         # retry a second time should end up with same result
         response = self.client.patch(url, data=request_data)
         assert response.status_code == 200, response.content.decode()
-        assert response.data == request_data['data']
+        assert response.data == request_data["data"]
 
         response = self.client.get(url)
-        assert response.data == request_data['data']
+        assert response.data == request_data["data"]
 
     def test_post_to_one_relationship_should_fail(self):
-        url = '/entries/{}/relationships/blog'.format(self.first_entry.id)
+        url = f"/entries/{self.first_entry.id}/relationships/blog"
         request_data = {
-            'data': {'type': format_resource_type('Blog'), 'id': str(self.other_blog.id)}
+            "data": {
+                "type": format_resource_type("Blog"),
+                "id": str(self.other_blog.id),
+            }
         }
         response = self.client.post(url, data=request_data)
         assert response.status_code == 405, response.content.decode()
 
     def test_post_to_many_relationship_with_no_change(self):
-        url = '/entries/{}/relationships/comments'.format(self.first_entry.id)
+        url = f"/entries/{self.first_entry.id}/relationships/comments"
         request_data = {
-            'data': [{'type': format_resource_type('Comment'), 'id': str(self.first_comment.id)}, ]
+            "data": [
+                {
+                    "type": format_resource_type("Comment"),
+                    "id": str(self.first_comment.id),
+                },
+            ]
         }
         response = self.client.post(url, data=request_data)
         assert response.status_code == 204, response.content.decode()
         assert len(response.rendered_content) == 0, response.rendered_content.decode()
 
     def test_post_to_many_relationship_with_change(self):
-        url = '/entries/{}/relationships/comments'.format(self.first_entry.id)
+        url = f"/entries/{self.first_entry.id}/relationships/comments"
         request_data = {
-            'data': [{'type': format_resource_type('Comment'), 'id': str(self.second_comment.id)}, ]
+            "data": [
+                {
+                    "type": format_resource_type("Comment"),
+                    "id": str(self.second_comment.id),
+                },
+            ]
         }
         response = self.client.post(url, data=request_data)
         assert response.status_code == 200, response.content.decode()
 
-        assert request_data['data'][0] in response.data
+        assert request_data["data"][0] in response.data
 
     def test_delete_to_one_relationship_should_fail(self):
-        url = '/entries/{}/relationships/blog'.format(self.first_entry.id)
+        url = f"/entries/{self.first_entry.id}/relationships/blog"
         request_data = {
-            'data': {'type': format_resource_type('Blog'), 'id': str(self.other_blog.id)}
+            "data": {
+                "type": format_resource_type("Blog"),
+                "id": str(self.other_blog.id),
+            }
         }
         response = self.client.delete(url, data=request_data)
         assert response.status_code == 405, response.content.decode()
 
     def test_delete_relationship_overriding_with_none(self):
-        url = '/comments/{}'.format(self.second_comment.id)
+        url = f"/comments/{self.second_comment.id}"
         request_data = {
-            'data': {
-                'type': 'comments',
-                'id': self.second_comment.id,
-                'relationships': {
-                    'author': {
-                        'data': None
-                    }
-                }
+            "data": {
+                "type": "comments",
+                "id": self.second_comment.id,
+                "relationships": {"author": {"data": None}},
             }
         }
         response = self.client.patch(url, data=request_data)
         assert response.status_code == 200, response.content.decode()
-        assert response.data['author'] is None
+        assert response.data["author"] is None
 
     def test_delete_to_many_relationship_with_no_change(self):
-        url = '/entries/{}/relationships/comments'.format(self.first_entry.id)
+        url = f"/entries/{self.first_entry.id}/relationships/comments"
         request_data = {
-            'data': [{'type': format_resource_type('Comment'), 'id': str(self.second_comment.id)}, ]
+            "data": [
+                {
+                    "type": format_resource_type("Comment"),
+                    "id": str(self.second_comment.id),
+                },
+            ]
         }
         response = self.client.delete(url, data=request_data)
         assert response.status_code == 204, response.content.decode()
         assert len(response.rendered_content) == 0, response.rendered_content.decode()
 
     def test_delete_one_to_many_relationship_with_not_null_constraint(self):
-        url = '/entries/{}/relationships/comments'.format(self.first_entry.id)
+        url = f"/entries/{self.first_entry.id}/relationships/comments"
         request_data = {
-            'data': [{'type': format_resource_type('Comment'), 'id': str(self.first_comment.id)}, ]
+            "data": [
+                {
+                    "type": format_resource_type("Comment"),
+                    "id": str(self.first_comment.id),
+                },
+            ]
         }
         response = self.client.delete(url, data=request_data)
         assert response.status_code == 409, response.content.decode()
 
     def test_delete_to_many_relationship_with_change(self):
-        url = '/authors/{}/relationships/comments'.format(self.author.id)
+        url = f"/authors/{self.author.id}/relationships/comments"
         request_data = {
-            'data': [{'type': format_resource_type('Comment'), 'id': str(self.second_comment.id)}, ]
+            "data": [
+                {
+                    "type": format_resource_type("Comment"),
+                    "id": str(self.second_comment.id),
+                },
+            ]
         }
         response = self.client.delete(url, data=request_data)
         assert response.status_code == 200, response.content.decode()
@@ -265,21 +329,19 @@ def test_new_comment_data_patch_to_many_relationship(self):
         entry = EntryFactory(blog=self.blog, authors=(self.author,))
         comment = CommentFactory(entry=entry)
 
-        url = '/authors/{}/relationships/comments'.format(self.author.id)
+        url = f"/authors/{self.author.id}/relationships/comments"
         request_data = {
-            'data': [{'type': format_resource_type('Comment'), 'id': str(comment.id)}, ]
+            "data": [
+                {"type": format_resource_type("Comment"), "id": str(comment.id)},
+            ]
         }
         previous_response = {
-            'data': [
-                {'type': 'comments',
-                 'id': str(self.second_comment.id)
-                 }
-            ],
-            'links': {
-                'self': 'http://testserver/authors/{}/relationships/comments'.format(
+            "data": [{"type": "comments", "id": str(self.second_comment.id)}],
+            "links": {
+                "self": "http://testserver/authors/{}/relationships/comments".format(
                     self.author.id
                 )
-            }
+            },
         }
 
         response = self.client.get(url)
@@ -287,16 +349,12 @@ def test_new_comment_data_patch_to_many_relationship(self):
         assert response.json() == previous_response
 
         new_patched_response = {
-            'data': [
-                {'type': 'comments',
-                 'id': str(comment.id)
-                 }
-            ],
-            'links': {
-                'self': 'http://testserver/authors/{}/relationships/comments'.format(
+            "data": [{"type": "comments", "id": str(comment.id)}],
+            "links": {
+                "self": "http://testserver/authors/{}/relationships/comments".format(
                     self.author.id
                 )
-            }
+            },
         }
 
         response = self.client.patch(url, data=request_data)
@@ -307,21 +365,19 @@ def test_new_comment_data_patch_to_many_relationship(self):
 
     def test_options_entry_relationship_blog(self):
         url = reverse(
-            'entry-relationships', kwargs={'pk': self.first_entry.id, 'related_field': 'blog'}
+            "entry-relationships",
+            kwargs={"pk": self.first_entry.id, "related_field": "blog"},
         )
         response = self.client.options(url)
         expected_data = {
             "data": {
                 "name": "Entry Relationship",
                 "description": "",
-                "renders": [
-                    "application/vnd.api+json",
-                    "text/html"
-                ],
+                "renders": ["application/vnd.api+json", "text/html"],
                 "parses": [
                     "application/vnd.api+json",
                     "application/x-www-form-urlencoded",
-                    "multipart/form-data"
+                    "multipart/form-data",
                 ],
                 "allowed_methods": [
                     "GET",
@@ -329,99 +385,109 @@ def test_options_entry_relationship_blog(self):
                     "PATCH",
                     "DELETE",
                     "HEAD",
-                    "OPTIONS"
+                    "OPTIONS",
                 ],
-                "actions": {
-                    "POST": {}
-                }
+                "actions": {"POST": {}},
             }
         }
         assert response.json() == expected_data
 
 
 class TestRelatedMixin(APITestCase):
-
     def setUp(self):
         self.author = AuthorFactory()
 
     def _get_view(self, kwargs):
         factory = APIRequestFactory()
-        request = Request(factory.get('', content_type='application/vnd.api+json'))
+        request = Request(factory.get("", content_type="application/vnd.api+json"))
         return AuthorViewSet(request=request, kwargs=kwargs)
 
     def test_get_related_field_name(self):
-        kwargs = {'pk': self.author.id, 'related_field': 'bio'}
+        kwargs = {"pk": self.author.id, "related_field": "bio"}
         view = self._get_view(kwargs)
         got = view.get_related_field_name()
-        self.assertEqual(got, kwargs['related_field'])
+        self.assertEqual(got, kwargs["related_field"])
 
     def test_get_related_instance_serializer_field(self):
-        kwargs = {'pk': self.author.id, 'related_field': 'bio'}
+        kwargs = {"pk": self.author.id, "related_field": "bio"}
         view = self._get_view(kwargs)
         got = view.get_related_instance()
         self.assertEqual(got, self.author.bio)
 
     def test_get_related_instance_model_field(self):
-        kwargs = {'pk': self.author.id, 'related_field': 'id'}
+        kwargs = {"pk": self.author.id, "related_field": "id"}
         view = self._get_view(kwargs)
         got = view.get_related_instance()
         self.assertEqual(got, self.author.id)
 
-    def test_get_serializer_class(self):
-        kwargs = {'pk': self.author.id, 'related_field': 'bio'}
+    def test_get_related_serializer_class(self):
+        kwargs = {"pk": self.author.id, "related_field": "bio"}
         view = self._get_view(kwargs)
-        got = view.get_serializer_class()
+        got = view.get_related_serializer_class()
         self.assertEqual(got, AuthorBioSerializer)
 
-    def test_get_serializer_class_many(self):
-        kwargs = {'pk': self.author.id, 'related_field': 'entries'}
+    def test_get_related_serializer_class_many(self):
+        kwargs = {"pk": self.author.id, "related_field": "entries"}
         view = self._get_view(kwargs)
-        got = view.get_serializer_class()
+        got = view.get_related_serializer_class()
         self.assertEqual(got, EntrySerializer)
 
     def test_get_serializer_comes_from_included_serializers(self):
-        kwargs = {'pk': self.author.id, 'related_field': 'type'}
+        kwargs = {"pk": self.author.id, "related_field": "author_type"}
         view = self._get_view(kwargs)
-        related_serializers = view.serializer_class.related_serializers
-        delattr(view.serializer_class, 'related_serializers')
-        got = view.get_serializer_class()
+        related_serializers = view.get_serializer_class().related_serializers
+        delattr(view.get_serializer_class(), "related_serializers")
+        got = view.get_related_serializer_class()
         self.assertEqual(got, AuthorTypeSerializer)
+        view.get_serializer_class().related_serializers = related_serializers
 
-        view.serializer_class.related_serializers = related_serializers
-
-    def test_get_serializer_class_raises_error(self):
-        kwargs = {'pk': self.author.id, 'related_field': 'unknown'}
+    def test_get_related_serializer_class_raises_error(self):
+        kwargs = {"pk": self.author.id, "related_field": "unknown"}
         view = self._get_view(kwargs)
-        self.assertRaises(NotFound, view.get_serializer_class)
+        self.assertRaises(NotFound, view.get_related_serializer_class)
 
     def test_retrieve_related_single_reverse_lookup(self):
-        url = reverse('author-related', kwargs={'pk': self.author.pk, 'related_field': 'bio'})
-        resp = self.client.get(url)
+        url = reverse(
+            "author-related", kwargs={"pk": self.author.pk, "related_field": "bio"}
+        )
+        resp = self.client.get(url, data={"include": "metadata"})
         expected = {
-            'data': {
-                'type': 'authorBios', 'id': str(self.author.bio.id),
-                'relationships': {
-                    'author': {'data': {'type': 'authors', 'id': str(self.author.id)}},
-                    'metadata': {'data': {'id': str(self.author.bio.metadata.id),
-                                          'type': 'authorBioMetadata'}}
-                },
-                'attributes': {
-                    'body': str(self.author.bio.body)
+            "data": {
+                "type": "authorBios",
+                "id": str(self.author.bio.id),
+                "relationships": {
+                    "author": {"data": {"type": "authors", "id": str(self.author.id)}},
+                    "metadata": {
+                        "data": {
+                            "id": str(self.author.bio.metadata.id),
+                            "type": "authorBioMetadata",
+                        }
+                    },
                 },
-            }
+                "attributes": {"body": str(self.author.bio.body)},
+            },
+            "included": [
+                {
+                    "attributes": {"body": str(self.author.bio.metadata.body)},
+                    "id": str(self.author.bio.metadata.id),
+                    "type": "authorBioMetadata",
+                }
+            ],
         }
         self.assertEqual(resp.status_code, 200)
         self.assertEqual(resp.json(), expected)
 
     def test_retrieve_related_single(self):
-        url = reverse('author-related', kwargs={'pk': self.author.type.pk, 'related_field': 'type'})
+        url = reverse(
+            "author-related",
+            kwargs={"pk": self.author.author_type.pk, "related_field": "author_type"},
+        )
         resp = self.client.get(url)
         expected = {
-            'data': {
-                'type': 'authorTypes', 'id': str(self.author.type.id),
-                'attributes': {
-                    'name': str(self.author.type.name)
-                },
+            "data": {
+                "type": "authorTypes",
+                "id": str(self.author.author_type.id),
+                "attributes": {"name": str(self.author.author_type.name)},
             }
         }
         self.assertEqual(resp.status_code, 200)
@@ -429,288 +495,101 @@ def test_retrieve_related_single(self):
 
     def test_retrieve_related_many(self):
         entry = EntryFactory(authors=self.author)
-        url = reverse('author-related', kwargs={'pk': self.author.pk, 'related_field': 'entries'})
+        url = reverse(
+            "author-related", kwargs={"pk": self.author.pk, "related_field": "entries"}
+        )
         resp = self.client.get(url)
 
         self.assertEqual(resp.status_code, 200)
-        self.assertTrue(isinstance(resp.json()['data'], list))
-        self.assertEqual(len(resp.json()['data']), 1)
-        self.assertEqual(resp.json()['data'][0]['id'], str(entry.id))
+        self.assertTrue(isinstance(resp.json()["data"], list))
+        self.assertEqual(len(resp.json()["data"]), 1)
+        self.assertEqual(resp.json()["data"][0]["id"], str(entry.id))
 
     def test_retrieve_related_many_hyperlinked(self):
         comment = CommentFactory(author=self.author)
-        url = reverse('author-related', kwargs={'pk': self.author.pk, 'related_field': 'comments'})
+        url = reverse(
+            "author-related", kwargs={"pk": self.author.pk, "related_field": "comments"}
+        )
         resp = self.client.get(url)
 
         self.assertEqual(resp.status_code, 200)
-        self.assertTrue(isinstance(resp.json()['data'], list))
-        self.assertEqual(len(resp.json()['data']), 1)
-        self.assertEqual(resp.json()['data'][0]['id'], str(comment.id))
+        self.assertTrue(isinstance(resp.json()["data"], list))
+        self.assertEqual(len(resp.json()["data"]), 1)
+        self.assertEqual(resp.json()["data"][0]["id"], str(comment.id))
 
     def test_retrieve_related_None(self):
-        kwargs = {'pk': self.author.pk, 'related_field': 'first_entry'}
-        url = reverse('author-related', kwargs=kwargs)
+        kwargs = {"pk": self.author.pk, "related_field": "first_entry"}
+        url = reverse("author-related", kwargs=kwargs)
+        resp = self.client.get(url)
+
+        self.assertEqual(resp.status_code, 200)
+        self.assertEqual(resp.json(), {"data": None})
+
+    @override_settings(JSON_API_FORMAT_RELATED_LINKS="dasherize")
+    def test_retrieve_related_with_formatted_link(self):
+        first_entry = EntryFactory(authors=(self.author,))
+
+        kwargs = {"pk": self.author.pk, "related_field": "first-entry"}
+        url = reverse("author-related", kwargs=kwargs)
         resp = self.client.get(url)
 
         self.assertEqual(resp.status_code, 200)
-        self.assertEqual(resp.json(), {'data': None})
+        self.assertEqual(resp.json()["data"]["id"], str(first_entry.id))
 
 
 class TestValidationErrorResponses(TestBase):
     def test_if_returns_error_on_empty_post(self):
-        view = BlogViewSet.as_view({'post': 'create'})
+        view = BlogViewSet.as_view({"post": "create"})
         response = self._get_create_response("{}", view)
         self.assertEqual(400, response.status_code)
-        expected = [{
-            'detail': 'Received document does not contain primary data',
-            'status': '400',
-            'source': {'pointer': '/data'},
-            'code': 'parse_error',
-        }]
+        expected = [
+            {
+                "detail": "Received document does not contain primary data",
+                "status": "400",
+                "source": {"pointer": "/data"},
+                "code": "parse_error",
+            }
+        ]
         self.assertEqual(expected, response.data)
 
     def test_if_returns_error_on_missing_form_data_post(self):
-        view = BlogViewSet.as_view({'post': 'create'})
-        response = self._get_create_response('{"data":{"attributes":{},"type":"blogs"}}', view)
+        view = BlogViewSet.as_view({"post": "create"})
+        response = self._get_create_response(
+            '{"data":{"attributes":{},"type":"blogs"}}', view
+        )
         self.assertEqual(400, response.status_code)
-        expected = [{
-            'status': '400',
-            'detail': 'This field is required.',
-            'source': {'pointer': '/data/attributes/name'},
-            'code': 'required',
-        }]
+        expected = [
+            {
+                "status": "400",
+                "detail": "This field is required.",
+                "source": {"pointer": "/data/attributes/name"},
+                "code": "required",
+            }
+        ]
         self.assertEqual(expected, response.data)
 
     def test_if_returns_error_on_bad_endpoint_name(self):
-        view = BlogViewSet.as_view({'post': 'create'})
-        response = self._get_create_response('{"data":{"attributes":{},"type":"bad"}}', view)
+        view = BlogViewSet.as_view({"post": "create"})
+        response = self._get_create_response(
+            '{"data":{"attributes":{},"type":"bad"}}', view
+        )
         self.assertEqual(409, response.status_code)
-        expected = [{
-            'detail': (
-                "The resource object's type (bad) is not the type that constitute the collection "
-                "represented by the endpoint (blogs)."
-            ),
-            'source': {'pointer': '/data'},
-            'status': '409',
-            'code': 'error',
-        }]
+        expected = [
+            {
+                "detail": (
+                    "The resource object's type (bad) is not the type that constitute the "
+                    "collection represented by the endpoint (blogs)."
+                ),
+                "source": {"pointer": "/data"},
+                "status": "409",
+                "code": "error",
+            }
+        ]
         self.assertEqual(expected, response.data)
 
     def _get_create_response(self, data, view):
         factory = RequestFactory()
-        request = factory.post('/', data, content_type='application/vnd.api+json')
-        user = self.create_user('user', 'pass')
+        request = factory.post("/", data, content_type="application/vnd.api+json")
+        user = self.create_user("user", "pass")
         force_authenticate(request, user)
         return view(request)
-
-
-class TestModelViewSet(TestBase):
-    def setUp(self):
-        self.author = Author.objects.create(name='Super powerful superhero', email='i.am@lost.com')
-        self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
-
-    def test_no_content_response(self):
-        url = '/blogs/{}'.format(self.blog.pk)
-        response = self.client.delete(url)
-        assert response.status_code == 204, response.rendered_content.decode()
-        assert len(response.rendered_content) == 0, response.rendered_content.decode()
-
-
-class TestBlogViewSet(APITestCase):
-
-    def setUp(self):
-        self.blog = Blog.objects.create(
-            name='Some Blog',
-            tagline="It's a blog"
-        )
-        self.entry = Entry.objects.create(
-            blog=self.blog,
-            headline='headline one',
-            body_text='body_text two',
-        )
-
-    def test_get_object_gives_correct_blog(self):
-        url = reverse('entry-blog', kwargs={'entry_pk': self.entry.id})
-        resp = self.client.get(url)
-        expected = {
-            'data': {
-                'attributes': {'name': self.blog.name},
-                'id': '{}'.format(self.blog.id),
-                'links': {'self': 'http://testserver/blogs/{}'.format(self.blog.id)},
-                'meta': {'copyright': datetime.now().year},
-                'relationships': {'tags': {'data': [], 'meta': {'count': 0}}},
-                'type': 'blogs'
-            },
-            'meta': {'apiDocs': '/docs/api/blogs'}
-        }
-        got = resp.json()
-        self.assertEqual(got, expected)
-
-
-class TestEntryViewSet(APITestCase):
-
-    def setUp(self):
-        self.blog = Blog.objects.create(
-            name='Some Blog',
-            tagline="It's a blog"
-        )
-        self.first_entry = Entry.objects.create(
-            blog=self.blog,
-            headline='headline two',
-            body_text='body_text two',
-        )
-        self.second_entry = Entry.objects.create(
-            blog=self.blog,
-            headline='headline two',
-            body_text='body_text two',
-        )
-        self.maxDiff = None
-
-    def test_get_object_gives_correct_entry(self):
-        url = reverse('entry-featured', kwargs={'entry_pk': self.first_entry.id})
-        resp = self.client.get(url)
-        expected = {
-            'data': {
-                'attributes': {
-                    'bodyText': self.second_entry.body_text,
-                    'headline': self.second_entry.headline,
-                    'modDate': self.second_entry.mod_date,
-                    'pubDate': self.second_entry.pub_date
-                },
-                'id': '{}'.format(self.second_entry.id),
-                'meta': {'bodyFormat': 'text'},
-                'relationships': {
-                    'authors': {'data': [], 'meta': {'count': 0}},
-                    'blog': {
-                        'data': {
-                            'id': '{}'.format(self.second_entry.blog_id),
-                            'type': 'blogs'
-                        }
-                    },
-                    'blogHyperlinked': {
-                        'links': {
-                            'related': 'http://testserver/entries/{}'
-                                       '/blog'.format(self.second_entry.id),
-                            'self': 'http://testserver/entries/{}'
-                                    '/relationships/blog_hyperlinked'.format(self.second_entry.id)
-                        }
-                    },
-                    'comments': {
-                        'data': [],
-                        'meta': {'count': 0}
-                    },
-                    'commentsHyperlinked': {
-                        'links': {
-                            'related': 'http://testserver/entries/{}'
-                                       '/comments'.format(self.second_entry.id),
-                            'self': 'http://testserver/entries/{}/relationships'
-                                    '/comments_hyperlinked'.format(self.second_entry.id)
-                        }
-                    },
-                    'featuredHyperlinked': {
-                        'links': {
-                            'related': 'http://testserver/entries/{}'
-                                       '/featured'.format(self.second_entry.id),
-                            'self': 'http://testserver/entries/{}/relationships'
-                                    '/featured_hyperlinked'.format(self.second_entry.id)
-                        }
-                    },
-                    'suggested': {
-                        'data': [{'id': '1', 'type': 'entries'}],
-                        'links': {
-                            'related': 'http://testserver/entries/{}'
-                                       '/suggested/'.format(self.second_entry.id),
-                            'self': 'http://testserver/entries/{}'
-                                    '/relationships/suggested'.format(self.second_entry.id)
-                        }
-                    },
-                    'suggestedHyperlinked': {
-                        'links': {
-                            'related': 'http://testserver/entries/{}'
-                                       '/suggested/'.format(self.second_entry.id),
-                            'self': 'http://testserver/entries/{}/relationships'
-                                    '/suggested_hyperlinked'.format(self.second_entry.id)
-                        }
-                    },
-                    'tags': {'data': [], 'meta': {'count': 0}}},
-                'type': 'posts'
-            }
-        }
-        got = resp.json()
-        self.assertEqual(got, expected)
-
-
-class BasicAuthorSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = Author
-        fields = ('name',)
-
-
-class ReadOnlyViewSetWithCustomActions(views.ReadOnlyModelViewSet):
-    queryset = Author.objects.all()
-    serializer_class = BasicAuthorSerializer
-
-    @action(detail=False, methods=['get', 'post', 'patch', 'delete'])
-    def group_action(self, request):
-        return Response(status=status.HTTP_204_NO_CONTENT)
-
-    @action(detail=True, methods=['get', 'post', 'patch', 'delete'])
-    def item_action(self, request, pk):
-        return Response(status=status.HTTP_204_NO_CONTENT)
-
-
-class TestReadonlyModelViewSet(TestBase):
-    """
-        Test if ReadOnlyModelViewSet allows to have custom actions with POST, PATCH, DELETE methods
-    """
-    factory = RequestFactory()
-    viewset_class = ReadOnlyViewSetWithCustomActions
-    media_type = 'application/vnd.api+json'
-
-    def test_group_action_allows_get(self):
-        view = self.viewset_class.as_view({'get': 'group_action'})
-        request = self.factory.get('/')
-        response = view(request)
-        self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
-
-    def test_group_action_allows_post(self):
-        view = self.viewset_class.as_view({'post': 'group_action'})
-        request = self.factory.post('/', '{}', content_type=self.media_type)
-        response = view(request)
-        self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
-
-    def test_group_action_allows_patch(self):
-        view = self.viewset_class.as_view({'patch': 'group_action'})
-        request = self.factory.patch('/', '{}', content_type=self.media_type)
-        response = view(request)
-        self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
-
-    def test_group_action_allows_delete(self):
-        view = self.viewset_class.as_view({'delete': 'group_action'})
-        request = self.factory.delete('/', '{}', content_type=self.media_type)
-        response = view(request)
-        self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
-
-    def test_item_action_allows_get(self):
-        view = self.viewset_class.as_view({'get': 'item_action'})
-        request = self.factory.get('/')
-        response = view(request, pk='1')
-        self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
-
-    def test_item_action_allows_post(self):
-        view = self.viewset_class.as_view({'post': 'item_action'})
-        request = self.factory.post('/', '{}', content_type=self.media_type)
-        response = view(request, pk='1')
-        self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
-
-    def test_item_action_allows_patch(self):
-        view = self.viewset_class.as_view({'patch': 'item_action'})
-        request = self.factory.patch('/', '{}', content_type=self.media_type)
-        response = view(request, pk='1')
-        self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
-
-    def test_item_action_allows_delete(self):
-        view = self.viewset_class.as_view({'delete': 'item_action'})
-        request = self.factory.delete('/', '{}', content_type=self.media_type)
-        response = view(request, pk='1')
-        self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
diff --git a/example/tests/unit/test_default_drf_serializers.py b/example/tests/unit/test_default_drf_serializers.py
index 3233ce84..87231896 100644
--- a/example/tests/unit/test_default_drf_serializers.py
+++ b/example/tests/unit/test_default_drf_serializers.py
@@ -15,7 +15,7 @@
 class RelatedModelSerializer(ModelSerializer):
     class Meta:
         model = Comment
-        fields = ('id',)
+        fields = ("id",)
 
 
 class DummyTestSerializer(ModelSerializer):
@@ -23,16 +23,19 @@ class DummyTestSerializer(ModelSerializer):
     This serializer is a simple compound document serializer which includes only
     a single embedded relation
     """
-    related_models = RelatedModelSerializer(source='comments', many=True, read_only=True)
+
+    related_models = RelatedModelSerializer(
+        source="comments", many=True, read_only=True
+    )
 
     json_field = SerializerMethodField()
 
     def get_json_field(self, entry):
-        return {'JsonKey': 'JsonValue'}
+        return {"JsonKey": "JsonValue"}
 
     class Meta:
         model = Entry
-        fields = ('related_models', 'json_field')
+        fields = ("related_models", "json_field")
 
 
 # views
@@ -41,45 +44,38 @@ class DummyTestViewSet(viewsets.ModelViewSet):
     serializer_class = DummyTestSerializer
 
 
-def render_dummy_test_serialized_view(view_class):
-    serializer = DummyTestSerializer(instance=Entry())
+def render_dummy_test_serialized_view(view_class, entry):
+    serializer = DummyTestSerializer(instance=entry)
     renderer = JSONRenderer()
-    return renderer.render(
-        serializer.data,
-        renderer_context={'view': view_class()})
+    return renderer.render(serializer.data, renderer_context={"view": view_class()})
 
 
 # tests
-def test_simple_reverse_relation_included_renderer():
+def test_simple_reverse_relation_included_renderer(db, entry):
     """
     Test renderer when a single reverse fk relation is passed.
     """
-    rendered = render_dummy_test_serialized_view(
-        DummyTestViewSet)
+    rendered = render_dummy_test_serialized_view(DummyTestViewSet, entry)
 
     assert rendered
 
 
-def test_render_format_field_names(settings):
+def test_render_format_field_names(db, settings, entry):
     """Test that json field is kept untouched."""
-    settings.JSON_API_FORMAT_FIELD_NAMES = 'dasherize'
-    rendered = render_dummy_test_serialized_view(DummyTestViewSet)
+    settings.JSON_API_FORMAT_FIELD_NAMES = "dasherize"
+    rendered = render_dummy_test_serialized_view(DummyTestViewSet, entry)
 
     result = json.loads(rendered.decode())
-    assert result['data']['attributes']['json-field'] == {'JsonKey': 'JsonValue'}
+    assert result["data"]["attributes"]["json-field"] == {"JsonKey": "JsonValue"}
 
 
 @pytest.mark.django_db
 def test_blog_create(client):
-
-    url = reverse('drf-entry-blog-list')
+    url = reverse("drf-entry-blog-list")
     name = "Dummy Name"
 
     request_data = {
-        'data': {
-            'attributes': {'name': name},
-            'type': 'blogs'
-        },
+        "data": {"attributes": {"name": name}, "type": "blogs"},
     }
 
     resp = client.post(url, request_data)
@@ -94,14 +90,14 @@ def test_blog_create(client):
     blog = blog.first()
 
     expected = {
-        'data': {
-            'attributes': {'name': blog.name, 'tags': []},
-            'id': '{}'.format(blog.id),
-            'links': {'self': 'http://testserver/blogs/{}'.format(blog.id)},
-            'meta': {'copyright': datetime.now().year},
-            'type': 'blogs'
+        "data": {
+            "attributes": {"name": blog.name, "tags": []},
+            "id": f"{blog.id}",
+            "links": {"self": f"http://testserver/blogs/{blog.id}"},
+            "meta": {"copyright": datetime.now().year},
+            "type": "blogs",
         },
-        'meta': {'apiDocs': '/docs/api/blogs'}
+        "meta": {"apiDocs": "/docs/api/blogs"},
     }
 
     assert resp.status_code == 201
@@ -110,18 +106,17 @@ def test_blog_create(client):
 
 @pytest.mark.django_db
 def test_get_object_gives_correct_blog(client, blog, entry):
-
-    url = reverse('drf-entry-blog-detail', kwargs={'entry_pk': entry.id})
+    url = reverse("drf-entry-blog-detail", kwargs={"entry_pk": entry.id})
     resp = client.get(url)
     expected = {
-        'data': {
-            'attributes': {'name': blog.name, 'tags': []},
-            'id': '{}'.format(blog.id),
-            'links': {'self': 'http://testserver/blogs/{}'.format(blog.id)},
-            'meta': {'copyright': datetime.now().year},
-            'type': 'blogs'
+        "data": {
+            "attributes": {"name": blog.name, "tags": []},
+            "id": f"{blog.id}",
+            "links": {"self": f"http://testserver/blogs/{blog.id}"},
+            "meta": {"copyright": datetime.now().year},
+            "type": "blogs",
         },
-        'meta': {'apiDocs': '/docs/api/blogs'}
+        "meta": {"apiDocs": "/docs/api/blogs"},
     }
     got = resp.json()
     assert got == expected
@@ -129,21 +124,20 @@ def test_get_object_gives_correct_blog(client, blog, entry):
 
 @pytest.mark.django_db
 def test_get_object_patches_correct_blog(client, blog, entry):
-
-    url = reverse('drf-entry-blog-detail', kwargs={'entry_pk': entry.id})
+    url = reverse("drf-entry-blog-detail", kwargs={"entry_pk": entry.id})
     new_name = blog.name + " update"
     assert not new_name == blog.name
 
     request_data = {
-        'data': {
-            'attributes': {'name': new_name},
-            'id': '{}'.format(blog.id),
-            'links': {'self': 'http://testserver/blogs/{}'.format(blog.id)},
-            'meta': {'copyright': datetime.now().year},
-            'relationships': {'tags': {'data': []}},
-            'type': 'blogs'
+        "data": {
+            "attributes": {"name": new_name},
+            "id": f"{blog.id}",
+            "links": {"self": f"http://testserver/blogs/{blog.id}"},
+            "meta": {"copyright": datetime.now().year},
+            "relationships": {"tags": {"data": []}},
+            "type": "blogs",
         },
-        'meta': {'apiDocs': '/docs/api/blogs'}
+        "meta": {"apiDocs": "/docs/api/blogs"},
     }
 
     resp = client.patch(url, data=request_data)
@@ -151,14 +145,14 @@ def test_get_object_patches_correct_blog(client, blog, entry):
     assert resp.status_code == 200
 
     expected = {
-        'data': {
-            'attributes': {'name': new_name, 'tags': []},
-            'id': '{}'.format(blog.id),
-            'links': {'self': 'http://testserver/blogs/{}'.format(blog.id)},
-            'meta': {'copyright': datetime.now().year},
-            'type': 'blogs'
+        "data": {
+            "attributes": {"name": new_name, "tags": []},
+            "id": f"{blog.id}",
+            "links": {"self": f"http://testserver/blogs/{blog.id}"},
+            "meta": {"copyright": datetime.now().year},
+            "type": "blogs",
         },
-        'meta': {'apiDocs': '/docs/api/blogs'}
+        "meta": {"apiDocs": "/docs/api/blogs"},
     }
     got = resp.json()
     assert got == expected
@@ -166,8 +160,7 @@ def test_get_object_patches_correct_blog(client, blog, entry):
 
 @pytest.mark.django_db
 def test_get_object_deletes_correct_blog(client, entry):
-
-    url = reverse('drf-entry-blog-detail', kwargs={'entry_pk': entry.id})
+    url = reverse("drf-entry-blog-detail", kwargs={"entry_pk": entry.id})
 
     resp = client.delete(url)
 
@@ -176,37 +169,29 @@ def test_get_object_deletes_correct_blog(client, entry):
 
 @pytest.mark.django_db
 def test_get_entry_list_with_blogs(client, entry):
-    url = reverse('drf-entry-suggested', kwargs={'entry_pk': entry.id})
+    url = reverse("drf-entry-suggested", kwargs={"entry_pk": entry.id})
     resp = client.get(url)
 
     got = resp.json()
 
     expected = {
-        'links': {
-            'first': 'http://testserver/drf-entries/1/suggested/?page%5Bnumber%5D=1',
-            'last': 'http://testserver/drf-entries/1/suggested/?page%5Bnumber%5D=1',
-            'next': None,
-            'prev': None,
+        "links": {
+            "first": "http://testserver/drf-entries/1/suggested/?page%5Bnumber%5D=1",
+            "last": "http://testserver/drf-entries/1/suggested/?page%5Bnumber%5D=1",
+            "next": None,
+            "prev": None,
         },
-        'data': [
+        "data": [
             {
-                'type': 'entries',
-                'id': '1',
-                'attributes': {
-                    'tags': [],
+                "type": "entries",
+                "id": "1",
+                "attributes": {
+                    "tags": [],
                 },
-                'links': {
-                    'self': 'http://testserver/drf-blogs/1'
-                }
+                "links": {"self": "http://testserver/drf-blogs/1"},
             }
         ],
-        'meta': {
-            'pagination': {
-                'page': 1,
-                'pages': 1,
-                'count': 1
-            }
-        }
+        "meta": {"pagination": {"page": 1, "pages": 1, "count": 1}},
     }
 
     assert resp.status_code == 200
diff --git a/example/tests/unit/test_factories.py b/example/tests/unit/test_factories.py
deleted file mode 100644
index 8acc9e74..00000000
--- a/example/tests/unit/test_factories.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import pytest
-
-from example.factories import BlogFactory
-from example.models import Blog
-
-pytestmark = pytest.mark.django_db
-
-
-def test_factory_instance(blog_factory):
-
-    assert blog_factory == BlogFactory
-
-
-def test_model_instance(blog):
-
-    assert isinstance(blog, Blog)
-
-
-def test_multiple_blog(blog_factory):
-    another_blog = blog_factory(name='Cool Blog')
-    new_blog = blog_factory(name='Awesome Blog')
-
-    assert another_blog.name == 'Cool Blog'
-    assert new_blog.name == 'Awesome Blog'
-
-
-def test_factories_with_relations(author_factory, entry_factory):
-
-    author = author_factory(name="Joel Spolsky")
-    entry = entry_factory(
-        headline=("The Absolute Minimum Every Software Developer"
-                  "Absolutely, Positively Must Know About Unicode "
-                  "and Character Sets (No Excuses!)"),
-        blog__name='Joel on Software', authors=(author, ))
-
-    assert entry.blog.name == 'Joel on Software'
-    assert entry.headline == ("The Absolute Minimum Every Software Developer"
-                              "Absolutely, Positively Must Know About Unicode "
-                              "and Character Sets (No Excuses!)")
-    assert entry.authors.all().count() == 1
-    assert entry.authors.all()[0].name == 'Joel Spolsky'
diff --git a/example/tests/unit/test_filter_schema_params.py b/example/tests/unit/test_filter_schema_params.py
deleted file mode 100644
index 2044c467..00000000
--- a/example/tests/unit/test_filter_schema_params.py
+++ /dev/null
@@ -1,77 +0,0 @@
-from rest_framework import filters as drf_filters
-
-from rest_framework_json_api import filters as dja_filters
-from rest_framework_json_api.django_filters import backends
-
-from example.views import EntryViewSet
-
-
-class DummyEntryViewSet(EntryViewSet):
-    filter_backends = (dja_filters.QueryParameterValidationFilter, dja_filters.OrderingFilter,
-                       backends.DjangoFilterBackend, drf_filters.SearchFilter)
-    filterset_fields = {
-        'id': ('exact',),
-        'headline': ('exact', 'contains'),
-        'blog__name': ('contains', ),
-    }
-
-    def __init__(self, **kwargs):
-        # dummy up self.request since PreloadIncludesMixin expects it to be defined
-        self.request = None
-        super(DummyEntryViewSet, self).__init__(**kwargs)
-
-
-def test_filters_get_schema_params():
-    """
-    test all my filters for `get_schema_operation_parameters()`
-    """
-    # list of tuples: (filter, expected result)
-    filters = [
-        (dja_filters.QueryParameterValidationFilter, []),
-        (backends.DjangoFilterBackend, [
-            {
-                'name': 'filter[id]', 'required': False, 'in': 'query',
-                'description': 'id', 'schema': {'type': 'string'}
-            },
-            {
-                'name': 'filter[headline]', 'required': False, 'in': 'query',
-                'description': 'headline', 'schema': {'type': 'string'}
-            },
-            {
-                'name': 'filter[headline.contains]', 'required': False, 'in': 'query',
-                'description': 'headline__contains', 'schema': {'type': 'string'}
-            },
-            {
-                'name': 'filter[blog.name.contains]', 'required': False, 'in': 'query',
-                'description': 'blog__name__contains', 'schema': {'type': 'string'}
-            },
-        ]),
-        (dja_filters.OrderingFilter, [
-            {
-                'name': 'sort', 'required': False, 'in': 'query',
-                'description': 'Which field to use when ordering the results.',
-                'schema': {'type': 'string'}
-            }
-        ]),
-        (drf_filters.SearchFilter, [
-            {
-                'name': 'filter[search]', 'required': False, 'in': 'query',
-                'description': 'A search term.',
-                'schema': {'type': 'string'}
-            }
-        ]),
-    ]
-    view = DummyEntryViewSet()
-
-    for c, expected in filters:
-        f = c()
-        result = f.get_schema_operation_parameters(view)
-        assert len(result) == len(expected)
-        if len(result) == 0:
-            continue
-        # py35: the result list/dict ordering isn't guaranteed
-        for res_item in result:
-            assert 'name' in res_item
-            for exp_item in expected:
-                if res_item['name'] == exp_item['name']:
-                    assert res_item == exp_item
diff --git a/example/tests/unit/test_pagination.py b/example/tests/unit/test_pagination.py
deleted file mode 100644
index aeb5f87e..00000000
--- a/example/tests/unit/test_pagination.py
+++ /dev/null
@@ -1,78 +0,0 @@
-from collections import OrderedDict
-
-from rest_framework.request import Request
-from rest_framework.test import APIRequestFactory
-from rest_framework.utils.urls import replace_query_param
-
-from rest_framework_json_api import pagination
-
-factory = APIRequestFactory()
-
-
-class TestLimitOffset:
-    """
-    Unit tests for `pagination.JsonApiLimitOffsetPagination`.
-    """
-
-    def setup(self):
-        class ExamplePagination(pagination.JsonApiLimitOffsetPagination):
-            default_limit = 10
-            max_limit = 15
-
-        self.pagination = ExamplePagination()
-        self.queryset = range(1, 101)
-        self.base_url = 'http://testserver/'
-
-    def paginate_queryset(self, request):
-        return list(self.pagination.paginate_queryset(self.queryset, request))
-
-    def get_paginated_content(self, queryset):
-        response = self.pagination.get_paginated_response(queryset)
-        return response.data
-
-    def get_test_request(self, arguments):
-        return Request(factory.get('/', arguments))
-
-    def test_valid_offset_limit(self):
-        """
-        Basic test, assumes offset and limit are given.
-        """
-        offset = 10
-        limit = 5
-        count = len(self.queryset)
-        last_offset = (count // limit) * limit
-        next_offset = 15
-        prev_offset = 5
-
-        request = self.get_test_request({
-            self.pagination.limit_query_param: limit,
-            self.pagination.offset_query_param: offset
-        })
-        base_url = replace_query_param(self.base_url, self.pagination.limit_query_param, limit)
-        last_url = replace_query_param(base_url, self.pagination.offset_query_param, last_offset)
-        first_url = base_url
-        next_url = replace_query_param(base_url, self.pagination.offset_query_param, next_offset)
-        prev_url = replace_query_param(base_url, self.pagination.offset_query_param, prev_offset)
-        queryset = self.paginate_queryset(request)
-        content = self.get_paginated_content(queryset)
-        next_offset = offset + limit
-
-        expected_content = {
-            'results': list(range(offset + 1, next_offset + 1)),
-            'links': OrderedDict([
-                ('first', first_url),
-                ('last', last_url),
-                ('next', next_url),
-                ('prev', prev_url),
-            ]),
-            'meta': {
-                'pagination': OrderedDict([
-                    ('count', count),
-                    ('limit', limit),
-                    ('offset', offset),
-                ])
-            }
-        }
-
-        assert queryset == list(range(offset + 1, next_offset + 1))
-        assert content == expected_content
diff --git a/example/tests/unit/test_renderer_class_methods.py b/example/tests/unit/test_renderer_class_methods.py
index 7a9230d3..838f6819 100644
--- a/example/tests/unit/test_renderer_class_methods.py
+++ b/example/tests/unit/test_renderer_class_methods.py
@@ -3,6 +3,7 @@
 
 from rest_framework_json_api import serializers
 from rest_framework_json_api.renderers import JSONRenderer
+from rest_framework_json_api.utils import get_serializer_fields
 
 pytestmark = pytest.mark.django_db
 
@@ -11,55 +12,55 @@ class ResourceSerializer(serializers.ModelSerializer):
     version = serializers.SerializerMethodField()
 
     def get_version(self, obj):
-        return '1.0.0'
+        return "1.0.0"
 
     class Meta:
-        fields = ('username',)
-        meta_fields = ('version',)
+        fields = ("username",)
+        meta_fields = ("version",)
         model = get_user_model()
 
 
 def test_build_json_resource_obj():
-    resource = {
-        'pk': 1,
-        'username': 'Alice',
-    }
+    resource = {"username": "Alice", "version": "1.0.0"}
 
-    serializer = ResourceSerializer(data={'username': 'Alice'})
+    serializer = ResourceSerializer(data={"username": "Alice"})
     serializer.is_valid()
     resource_instance = serializer.save()
 
     output = {
-        'type': 'user',
-        'id': '1',
-        'attributes': {
-            'username': 'Alice'
-        },
+        "type": "user",
+        "id": "1",
+        "attributes": {"username": "Alice"},
+        "meta": {"version": "1.0.0"},
     }
 
-    assert JSONRenderer.build_json_resource_obj(
-        serializer.fields, resource, resource_instance, 'user') == output
+    assert (
+        JSONRenderer.build_json_resource_obj(
+            get_serializer_fields(serializer),
+            resource,
+            resource_instance,
+            "user",
+            serializer,
+        )
+        == output
+    )
 
 
 def test_can_override_methods():
     """
     Make sure extract_attributes and extract_relationships can be overriden.
     """
-    resource = {
-        'pk': 1,
-        'username': 'Alice',
-    }
 
-    serializer = ResourceSerializer(data={'username': 'Alice'})
+    resource = {"username": "Alice", "version": "1.0.0"}
+    serializer = ResourceSerializer(data={"username": "Alice"})
     serializer.is_valid()
     resource_instance = serializer.save()
 
     output = {
-        'type': 'user',
-        'id': '1',
-        'attributes': {
-            'username': 'Alice'
-        },
+        "type": "user",
+        "id": "1",
+        "attributes": {"username": "Alice"},
+        "meta": {"version": "1.0.0"},
     }
 
     class CustomRenderer(JSONRenderer):
@@ -69,44 +70,48 @@ class CustomRenderer(JSONRenderer):
         @classmethod
         def extract_attributes(cls, fields, resource):
             cls.extract_attributes_was_overriden = True
-            return super(CustomRenderer, cls).extract_attributes(fields, resource)
+            return super().extract_attributes(fields, resource)
 
         @classmethod
         def extract_relationships(cls, fields, resource, resource_instance):
             cls.extract_relationships_was_overriden = True
-            return super(CustomRenderer, cls).extract_relationships(
-                fields, resource, resource_instance
-            )
-
-    assert CustomRenderer.build_json_resource_obj(
-        serializer.fields, resource, resource_instance, 'user') == output
+            return super().extract_relationships(fields, resource, resource_instance)
+
+    assert (
+        CustomRenderer.build_json_resource_obj(
+            get_serializer_fields(serializer),
+            resource,
+            resource_instance,
+            "user",
+            serializer,
+        )
+        == output
+    )
     assert CustomRenderer.extract_attributes_was_overriden
     assert CustomRenderer.extract_relationships_was_overriden
 
 
 def test_extract_attributes():
     fields = {
-        'id': serializers.Field(),
-        'username': serializers.Field(),
-        'deleted': serializers.ReadOnlyField(),
+        "id": serializers.Field(),
+        "username": serializers.Field(),
+        "deleted": serializers.ReadOnlyField(),
     }
-    resource = {'id': 1, 'deleted': None, 'username': 'jerel'}
-    expected = {
-        'username': 'jerel',
-        'deleted': None
-    }
-    assert sorted(JSONRenderer.extract_attributes(fields, resource)) == sorted(expected), (
-        'Regular fields should be extracted'
-    )
-    assert sorted(JSONRenderer.extract_attributes(fields, {})) == sorted(
-        {'username': ''}), 'Should not extract read_only fields on empty serializer'
+    resource = {"id": 1, "deleted": None, "username": "jerel"}
+    expected = {"username": "jerel", "deleted": None}
+    assert sorted(JSONRenderer.extract_attributes(fields, resource)) == sorted(
+        expected
+    ), "Regular fields should be extracted"
+    assert (
+        JSONRenderer.extract_attributes(fields, {}) == {}
+    ), "Should not extract read_only fields on empty serializer"
 
 
 def test_extract_meta():
-    serializer = ResourceSerializer(data={'username': 'jerel', 'version': '1.0.0'})
+    serializer = ResourceSerializer(data={"username": "jerel", "version": "1.0.0"})
     serializer.is_valid()
     expected = {
-        'version': '1.0.0',
+        "version": "1.0.0",
     }
     assert JSONRenderer.extract_meta(serializer, serializer.data) == expected
 
@@ -114,33 +119,27 @@ def test_extract_meta():
 class ExtractRootMetaResourceSerializer(ResourceSerializer):
     def get_root_meta(self, resource, many):
         if many:
-            return {
-                'foo': 'meta-many-value'
-            }
+            return {"foo": "meta-many-value"}
         else:
-            return {
-                'foo': 'meta-value'
-            }
+            return {"foo": "meta-value"}
 
 
 class InvalidExtractRootMetaResourceSerializer(ResourceSerializer):
     def get_root_meta(self, resource, many):
-        return 'not a dict'
+        return "not a dict"
 
 
 def test_extract_root_meta():
     serializer = ExtractRootMetaResourceSerializer()
     expected = {
-        'foo': 'meta-value',
+        "foo": "meta-value",
     }
     assert JSONRenderer.extract_root_meta(serializer, {}) == expected
 
 
 def test_extract_root_meta_many():
     serializer = ExtractRootMetaResourceSerializer(many=True)
-    expected = {
-        'foo': 'meta-many-value'
-    }
+    expected = {"foo": "meta-many-value"}
     assert JSONRenderer.extract_root_meta(serializer, {}) == expected
 
 
diff --git a/example/tests/unit/test_renderers.py b/example/tests/unit/test_renderers.py
index 034cc45f..f34ce8d6 100644
--- a/example/tests/unit/test_renderers.py
+++ b/example/tests/unit/test_renderers.py
@@ -11,40 +11,41 @@
 
 # serializers
 class RelatedModelSerializer(serializers.ModelSerializer):
-    blog = serializers.ReadOnlyField(source='entry.blog')
+    blog = serializers.ReadOnlyField(source="entry.blog")
 
     class Meta:
         model = Comment
-        fields = ('id', 'blog')
+        fields = ("id", "blog")
 
 
 class DummyTestSerializer(serializers.ModelSerializer):
-    '''
+    """
     This serializer is a simple compound document serializer which includes only
     a single embedded relation
-    '''
+    """
+
     related_models = RelatedModelSerializer(
-        source='comments', many=True, read_only=True)
+        source="comments", many=True, read_only=True
+    )
 
     json_field = serializers.SerializerMethodField()
 
     def get_json_field(self, entry):
-        return {'JsonKey': 'JsonValue'}
+        return {"JsonKey": "JsonValue"}
 
     class Meta:
         model = Entry
-        fields = ('related_models', 'json_field')
+        fields = ("related_models", "json_field")
 
     class JSONAPIMeta:
-        included_resources = ('related_models',)
+        included_resources = ("related_models",)
 
 
 class EntryDRFSerializers(serializers.ModelSerializer):
-
     class Meta:
         model = Entry
-        fields = ('headline', 'body_text')
-        read_only_fields = ('tags',)
+        fields = ("headline", "body_text")
+        read_only_fields = ("tags",)
 
 
 class CommentWithNestedFieldsSerializer(serializers.ModelSerializer):
@@ -52,7 +53,7 @@ class CommentWithNestedFieldsSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = Comment
-        exclude = ('created_at', 'modified_at', 'author')
+        exclude = ("created_at", "modified_at", "author")
         # fields = ('entry', 'body', 'author',)
 
 
@@ -61,7 +62,7 @@ class AuthorWithNestedFieldsSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = Author
-        fields = ('name', 'email', 'comments')
+        fields = ("name", "email", "comments")
 
 
 # views
@@ -78,59 +79,54 @@ class ReadOnlyDummyTestViewSet(views.ReadOnlyModelViewSet):
 class AuthorWithNestedFieldsViewSet(views.ModelViewSet):
     queryset = Author.objects.all()
     serializer_class = AuthorWithNestedFieldsSerializer
-    resource_name = 'authors'
+    resource_name = "authors"
 
 
 def render_dummy_test_serialized_view(view_class, instance):
     serializer = view_class.serializer_class(instance=instance)
     renderer = JSONRenderer()
-    return renderer.render(
-        serializer.data,
-        renderer_context={'view': view_class()})
+    return renderer.render(serializer.data, renderer_context={"view": view_class()})
 
 
-def test_simple_reverse_relation_included_renderer():
-    '''
+def test_simple_reverse_relation_included_renderer(db, entry):
+    """
     Test renderer when a single reverse fk relation is passed.
-    '''
-    rendered = render_dummy_test_serialized_view(
-        DummyTestViewSet, Entry())
+    """
+    rendered = render_dummy_test_serialized_view(DummyTestViewSet, entry)
 
     assert rendered
 
 
-def test_simple_reverse_relation_included_read_only_viewset():
-    rendered = render_dummy_test_serialized_view(
-        ReadOnlyDummyTestViewSet, Entry())
+def test_simple_reverse_relation_included_read_only_viewset(db, entry):
+    rendered = render_dummy_test_serialized_view(ReadOnlyDummyTestViewSet, entry)
 
     assert rendered
 
 
-def test_render_format_field_names(settings):
+def test_render_format_field_names(db, entry, settings):
     """Test that json field is kept untouched."""
-    settings.JSON_API_FORMAT_FIELD_NAMES = 'dasherize'
-    rendered = render_dummy_test_serialized_view(DummyTestViewSet, Entry())
+    settings.JSON_API_FORMAT_FIELD_NAMES = "dasherize"
+    rendered = render_dummy_test_serialized_view(DummyTestViewSet, entry)
 
     result = json.loads(rendered.decode())
-    assert result['data']['attributes']['json-field'] == {'JsonKey': 'JsonValue'}
+    assert result["data"]["attributes"]["json-field"] == {"JsonKey": "JsonValue"}
 
 
 def test_writeonly_not_in_response():
     """Test that writeonly fields are not shown in list response"""
 
     class WriteonlyTestSerializer(serializers.ModelSerializer):
-        '''Serializer for testing the absence of write_only fields'''
+        """Serializer for testing the absence of write_only fields"""
+
         comments = serializers.ResourceRelatedField(
-            many=True,
-            write_only=True,
-            queryset=Comment.objects.all()
+            many=True, write_only=True, queryset=Comment.objects.all()
         )
 
         rating = serializers.IntegerField(write_only=True)
 
         class Meta:
             model = Entry
-            fields = ('comments', 'rating')
+            fields = ("headline", "comments", "rating")
 
     class WriteOnlyDummyTestViewSet(views.ReadOnlyModelViewSet):
         queryset = Entry.objects.all()
@@ -139,8 +135,9 @@ class WriteOnlyDummyTestViewSet(views.ReadOnlyModelViewSet):
     rendered = render_dummy_test_serialized_view(WriteOnlyDummyTestViewSet, Entry())
     result = json.loads(rendered.decode())
 
-    assert 'rating' not in result['data']['attributes']
-    assert 'relationships' not in result['data']
+    assert "rating" not in result["data"]["attributes"]
+    assert "headline" in result["data"]["attributes"]
+    assert "relationships" not in result["data"]
 
 
 def test_render_empty_relationship_reverse_lookup():
@@ -149,7 +146,7 @@ def test_render_empty_relationship_reverse_lookup():
     class EmptyRelationshipSerializer(serializers.ModelSerializer):
         class Meta:
             model = Author
-            fields = ('bio', )
+            fields = ("bio",)
 
     class EmptyRelationshipViewSet(views.ReadOnlyModelViewSet):
         queryset = Author.objects.all()
@@ -157,9 +154,10 @@ class EmptyRelationshipViewSet(views.ReadOnlyModelViewSet):
 
     rendered = render_dummy_test_serialized_view(EmptyRelationshipViewSet, Author())
     result = json.loads(rendered.decode())
-    assert 'relationships' in result['data']
-    assert 'bio' in result['data']['relationships']
-    assert result['data']['relationships']['bio'] == {'data': None}
+    assert "attributes" not in result["data"]
+    assert "relationships" in result["data"]
+    assert "bio" in result["data"]["relationships"]
+    assert result["data"]["relationships"]["bio"] == {"data": None}
 
 
 @pytest.mark.django_db
@@ -167,32 +165,30 @@ def test_extract_relation_instance(comment):
     serializer = RelatedModelSerializer(instance=comment)
 
     got = JSONRenderer.extract_relation_instance(
-        field=serializer.fields['blog'], resource_instance=comment
+        field=serializer.fields["blog"], resource_instance=comment
     )
     assert got == comment.entry.blog
 
 
 def test_render_serializer_as_attribute(db):
     # setting up
-    blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
+    blog = Blog.objects.create(name="Some Blog", tagline="It's a blog")
     entry = Entry.objects.create(
         blog=blog,
-        headline='headline',
-        body_text='body_text',
+        headline="headline",
+        body_text="body_text",
         pub_date=timezone.now(),
         mod_date=timezone.now(),
         n_comments=0,
         n_pingbacks=0,
-        rating=3
+        rating=3,
     )
 
-    author = Author.objects.create(name='some_author', email='some_author@example.org')
+    author = Author.objects.create(name="some_author", email="some_author@example.org")
     entry.authors.add(author)
 
     Comment.objects.create(
-        entry=entry,
-        body='testing one two three',
-        author=Author.objects.first()
+        entry=entry, body="testing one two three", author=Author.objects.first()
     )
 
     rendered = render_dummy_test_serialized_view(AuthorWithNestedFieldsViewSet, author)
@@ -209,13 +205,13 @@ def test_render_serializer_as_attribute(db):
                     {
                         "id": 1,
                         "entry": {
-                            'headline': 'headline',
-                            'body_text': 'body_text',
+                            "headline": "headline",
+                            "body_text": "body_text",
                         },
-                        "body": "testing one two three"
+                        "body": "testing one two three",
                     }
-                ]
-            }
+                ],
+            },
         }
     }
     assert expected == result
diff --git a/example/tests/unit/test_serializer_method_field.py b/example/tests/unit/test_serializer_method_field.py
index 89e18295..1be56f47 100644
--- a/example/tests/unit/test_serializer_method_field.py
+++ b/example/tests/unit/test_serializer_method_field.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from rest_framework import serializers
 
 from rest_framework_json_api.relations import SerializerMethodResourceRelatedField
@@ -13,28 +11,27 @@ class BlogSerializer(serializers.ModelSerializer):
 
         class Meta:
             model = Blog
-            fields = ['one_entry']
+            fields = ["one_entry"]
 
         def get_one_entry(self, instance):
             return Entry(id=100)
 
     serializer = BlogSerializer(instance=Blog())
-    assert serializer.data['one_entry']['id'] == '100'
+    assert serializer.data["one_entry"]["id"] == "100"
 
 
 def test_method_name_custom():
     class BlogSerializer(serializers.ModelSerializer):
         one_entry = SerializerMethodResourceRelatedField(
-            model=Entry,
-            method_name='get_custom_entry'
+            model=Entry, method_name="get_custom_entry"
         )
 
         class Meta:
             model = Blog
-            fields = ['one_entry']
+            fields = ["one_entry"]
 
         def get_custom_entry(self, instance):
             return Entry(id=100)
 
     serializer = BlogSerializer(instance=Blog())
-    assert serializer.data['one_entry']['id'] == '100'
+    assert serializer.data["one_entry"]["id"] == "100"
diff --git a/example/tests/unit/test_utils.py b/example/tests/unit/test_utils.py
deleted file mode 100644
index f2822f2b..00000000
--- a/example/tests/unit/test_utils.py
+++ /dev/null
@@ -1,129 +0,0 @@
-import pytest
-from django.contrib.auth import get_user_model
-from django.test import override_settings
-from rest_framework import serializers
-from rest_framework.generics import GenericAPIView
-from rest_framework.response import Response
-from rest_framework.views import APIView
-
-from rest_framework_json_api import utils
-from rest_framework_json_api.utils import get_included_serializers
-
-from example.serializers import AuthorSerializer, BlogSerializer, CommentSerializer, EntrySerializer
-
-pytestmark = pytest.mark.django_db
-
-
-class NonModelResourceSerializer(serializers.Serializer):
-    class Meta:
-        resource_name = 'users'
-
-
-class ResourceSerializer(serializers.ModelSerializer):
-    class Meta:
-        fields = ('username',)
-        model = get_user_model()
-
-
-def test_get_resource_name():
-    view = APIView()
-    context = {'view': view}
-    with override_settings(JSON_API_FORMAT_TYPES=None):
-        assert 'APIViews' == utils.get_resource_name(context), 'not formatted'
-
-    context = {'view': view}
-    with override_settings(JSON_API_FORMAT_TYPES='dasherize'):
-        assert 'api-views' == utils.get_resource_name(context), 'derived from view'
-
-    view.model = get_user_model()
-    assert 'users' == utils.get_resource_name(context), 'derived from view model'
-
-    view.resource_name = 'custom'
-    assert 'custom' == utils.get_resource_name(context), 'manually set on view'
-
-    view.response = Response(status=403)
-    assert 'errors' == utils.get_resource_name(context), 'handles 4xx error'
-
-    view.response = Response(status=500)
-    assert 'errors' == utils.get_resource_name(context), 'handles 500 error'
-
-    view = GenericAPIView()
-    view.serializer_class = ResourceSerializer
-    context = {'view': view}
-    assert 'users' == utils.get_resource_name(context), 'derived from serializer'
-
-    view.serializer_class.Meta.resource_name = 'rcustom'
-    assert 'rcustom' == utils.get_resource_name(context), 'set on serializer'
-
-    view = GenericAPIView()
-    view.serializer_class = NonModelResourceSerializer
-    context = {'view': view}
-    assert 'users' == utils.get_resource_name(context), 'derived from non-model serializer'
-
-
-@pytest.mark.parametrize("format_type,output", [
-    ('camelize', {'fullName': {'last-name': 'a', 'first-name': 'b'}}),
-    ('capitalize', {'FullName': {'last-name': 'a', 'first-name': 'b'}}),
-    ('dasherize', {'full-name': {'last-name': 'a', 'first-name': 'b'}}),
-    ('underscore', {'full_name': {'last-name': 'a', 'first-name': 'b'}}),
-])
-def test_format_field_names(settings, format_type, output):
-    settings.JSON_API_FORMAT_FIELD_NAMES = format_type
-
-    value = {'full_name': {'last-name': 'a', 'first-name': 'b'}}
-    assert utils.format_field_names(value, format_type) == output
-
-
-def test_format_value():
-    assert utils.format_value('first_name', 'camelize') == 'firstName'
-    assert utils.format_value('first_name', 'capitalize') == 'FirstName'
-    assert utils.format_value('first_name', 'dasherize') == 'first-name'
-    assert utils.format_value('first-name', 'underscore') == 'first_name'
-
-
-def test_format_resource_type():
-    assert utils.format_resource_type('first_name', 'capitalize') == 'FirstNames'
-    assert utils.format_resource_type('first_name', 'camelize') == 'firstNames'
-
-
-class SerializerWithIncludedSerializers(EntrySerializer):
-    included_serializers = {
-        'blog': BlogSerializer,
-        'authors': 'example.serializers.AuthorSerializer',
-        'comments': 'example.serializers.CommentSerializer',
-        'self': 'self'  # this wouldn't make sense in practice (and would be prohibited by
-        # IncludedResourcesValidationMixin) but it's useful for the test
-    }
-
-
-def test_get_included_serializers_against_class():
-    klass = SerializerWithIncludedSerializers
-    included_serializers = get_included_serializers(klass)
-    expected_included_serializers = {
-        'blog': BlogSerializer,
-        'authors': AuthorSerializer,
-        'comments': CommentSerializer,
-        'self': klass
-    }
-    assert included_serializers.keys() == klass.included_serializers.keys(), (
-        'the keys must be preserved'
-    )
-
-    assert included_serializers == expected_included_serializers
-
-
-def test_get_included_serializers_against_instance():
-    klass = SerializerWithIncludedSerializers
-    instance = klass()
-    included_serializers = get_included_serializers(instance)
-    expected_included_serializers = {
-        'blog': BlogSerializer,
-        'authors': AuthorSerializer,
-        'comments': CommentSerializer,
-        'self': klass
-    }
-    assert included_serializers.keys() == klass.included_serializers.keys(), (
-        'the keys must be preserved'
-    )
-
-    assert included_serializers == expected_included_serializers
diff --git a/example/urls.py b/example/urls.py
index 9b882ce5..471fbe81 100644
--- a/example/urls.py
+++ b/example/urls.py
@@ -1,11 +1,5 @@
-from django.conf import settings
-from django.conf.urls import include, url
-from django.urls import path
-from django.views.generic import TemplateView
+from django.urls import include, path, re_path
 from rest_framework import routers
-from rest_framework.schemas import get_schema_view
-
-from rest_framework_json_api.schemas.openapi import SchemaGenerator
 
 from example.views import (
     AuthorRelationshipView,
@@ -17,72 +11,76 @@
     CompanyViewset,
     EntryRelationshipView,
     EntryViewSet,
+    LabResultViewSet,
     NonPaginatedEntryViewSet,
     ProjectTypeViewset,
-    ProjectViewset
+    ProjectViewset,
+    QuestionnaireViewset,
 )
 
 router = routers.DefaultRouter(trailing_slash=False)
 
-router.register(r'blogs', BlogViewSet)
-router.register(r'entries', EntryViewSet)
-router.register(r'nopage-entries', NonPaginatedEntryViewSet, 'nopage-entry')
-router.register(r'authors', AuthorViewSet)
-router.register(r'comments', CommentViewSet)
-router.register(r'companies', CompanyViewset)
-router.register(r'projects', ProjectViewset)
-router.register(r'project-types', ProjectTypeViewset)
+router.register(r"blogs", BlogViewSet)
+router.register(r"entries", EntryViewSet)
+router.register(r"nopage-entries", NonPaginatedEntryViewSet, "nopage-entry")
+router.register(r"authors", AuthorViewSet)
+router.register(r"comments", CommentViewSet)
+router.register(r"companies", CompanyViewset)
+router.register(r"projects", ProjectViewset)
+router.register(r"project-types", ProjectTypeViewset)
+router.register(r"lab-results", LabResultViewSet)
+router.register(r"questionnaires", QuestionnaireViewset)
 
 urlpatterns = [
-    url(r'^', include(router.urls)),
-    url(r'^entries/(?P<entry_pk>[^/.]+)/suggested/$',
-        EntryViewSet.as_view({'get': 'list'}),
-        name='entry-suggested'
-        ),
-    url(r'entries/(?P<entry_pk>[^/.]+)/blog$',
-        BlogViewSet.as_view({'get': 'retrieve'}),
-        name='entry-blog'),
-    url(r'entries/(?P<entry_pk>[^/.]+)/comments$',
-        CommentViewSet.as_view({'get': 'list'}),
-        name='entry-comments'),
-    url(r'entries/(?P<entry_pk>[^/.]+)/authors$',
-        AuthorViewSet.as_view({'get': 'list'}),
-        name='entry-authors'),
-    url(r'entries/(?P<entry_pk>[^/.]+)/featured$',
-        EntryViewSet.as_view({'get': 'retrieve'}),
-        name='entry-featured'),
-
-    url(r'^authors/(?P<pk>[^/.]+)/(?P<related_field>\w+)/$',
-        AuthorViewSet.as_view({'get': 'retrieve_related'}),
-        name='author-related'),
-
-    url(r'^entries/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$',
+    path("", include(router.urls)),
+    re_path(
+        r"^entries/(?P<entry_pk>[^/.]+)/suggested/$",
+        EntryViewSet.as_view({"get": "list"}),
+        name="entry-suggested",
+    ),
+    re_path(
+        r"entries/(?P<entry_pk>[^/.]+)/blog$",
+        BlogViewSet.as_view({"get": "retrieve"}),
+        name="entry-blog",
+    ),
+    re_path(
+        r"entries/(?P<entry_pk>[^/.]+)/comments$",
+        CommentViewSet.as_view({"get": "list"}),
+        name="entry-comments",
+    ),
+    re_path(
+        r"entries/(?P<entry_pk>[^/.]+)/authors$",
+        AuthorViewSet.as_view({"get": "list"}),
+        name="entry-authors",
+    ),
+    re_path(
+        r"entries/(?P<entry_pk>[^/.]+)/featured$",
+        EntryViewSet.as_view({"get": "retrieve"}),
+        name="entry-featured",
+    ),
+    re_path(
+        r"^authors/(?P<pk>[^/.]+)/(?P<related_field>\w+)/$",
+        AuthorViewSet.as_view({"get": "retrieve_related"}),
+        name="author-related",
+    ),
+    re_path(
+        r"^entries/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$",
         EntryRelationshipView.as_view(),
-        name='entry-relationships'),
-    url(r'^blogs/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$',
+        name="entry-relationships",
+    ),
+    re_path(
+        r"^blogs/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$",
         BlogRelationshipView.as_view(),
-        name='blog-relationships'),
-    url(r'^comments/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$',
+        name="blog-relationships",
+    ),
+    re_path(
+        r"^comments/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$",
         CommentRelationshipView.as_view(),
-        name='comment-relationships'),
-    url(r'^authors/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$',
+        name="comment-relationships",
+    ),
+    re_path(
+        r"^authors/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$",
         AuthorRelationshipView.as_view(),
-        name='author-relationships'),
-    path('openapi', get_schema_view(
-        title="Example API",
-        description="API for all things …",
-        version="1.0.0",
-        generator_class=SchemaGenerator
-    ), name='openapi-schema'),
-    path('swagger-ui/', TemplateView.as_view(
-        template_name='swagger-ui.html',
-        extra_context={'schema_url': 'openapi-schema'}
-    ), name='swagger-ui'),
+        name="author-relationships",
+    ),
 ]
-
-if settings.DEBUG:
-    import debug_toolbar
-
-    urlpatterns = [
-        url(r'^__debug__/', include(debug_toolbar.urls)),
-    ] + urlpatterns
diff --git a/example/urls_test.py b/example/urls_test.py
index 44ae6f58..26d4db21 100644
--- a/example/urls_test.py
+++ b/example/urls_test.py
@@ -1,4 +1,4 @@
-from django.conf.urls import re_path
+from django.urls import path, re_path
 from rest_framework import routers
 
 from .api.resources.identity import GenericIdentity, Identity
@@ -15,76 +15,97 @@
     EntryRelationshipView,
     EntryViewSet,
     FiltersetEntryViewSet,
+    LabResultViewSet,
     NoFiltersetEntryViewSet,
     NonPaginatedEntryViewSet,
     ProjectTypeViewset,
-    ProjectViewset
+    ProjectViewset,
+    QuestionnaireViewset,
 )
 
 router = routers.DefaultRouter(trailing_slash=False)
 
-router.register(r'blogs', BlogViewSet)
+router.register(r"blogs", BlogViewSet)
 # router to test default DRF blog functionalities
-router.register(r'drf-blogs', DRFBlogViewSet, 'drf-entry-blog')
-router.register(r'entries', EntryViewSet)
+router.register(r"drf-blogs", DRFBlogViewSet, "drf-entry-blog")
+router.register(r"entries", EntryViewSet)
 # these "flavors" of entries are used for various tests:
-router.register(r'nopage-entries', NonPaginatedEntryViewSet, 'nopage-entry')
-router.register(r'filterset-entries', FiltersetEntryViewSet, 'filterset-entry')
-router.register(r'nofilterset-entries', NoFiltersetEntryViewSet, 'nofilterset-entry')
-router.register(r'authors', AuthorViewSet)
-router.register(r'comments', CommentViewSet)
-router.register(r'companies', CompanyViewset)
-router.register(r'projects', ProjectViewset)
-router.register(r'project-types', ProjectTypeViewset)
+router.register(r"nopage-entries", NonPaginatedEntryViewSet, "nopage-entry")
+router.register(r"filterset-entries", FiltersetEntryViewSet, "filterset-entry")
+router.register(r"nofilterset-entries", NoFiltersetEntryViewSet, "nofilterset-entry")
+router.register(r"authors", AuthorViewSet)
+router.register(r"comments", CommentViewSet)
+router.register(r"companies", CompanyViewset)
+router.register(r"projects", ProjectViewset)
+router.register(r"project-types", ProjectTypeViewset)
+router.register(r"lab-results", LabResultViewSet)
+router.register(r"questionnaires", QuestionnaireViewset)
 
 # for the old tests
-router.register(r'identities', Identity)
+router.register(r"identities", Identity)
 
 urlpatterns = [
     # old tests
-    re_path(r'identities/default/(?P<pk>\d+)$',
-            GenericIdentity.as_view(), name='user-default'),
-
-
-    re_path(r'^entries/(?P<entry_pk>[^/.]+)/blog$',
-            BlogViewSet.as_view({'get': 'retrieve'}),
-            name='entry-blog'
-            ),
-    re_path(r'^entries/(?P<entry_pk>[^/.]+)/comments$',
-            CommentViewSet.as_view({'get': 'list'}),
-            name='entry-comments'
-            ),
-    re_path(r'^entries/(?P<entry_pk>[^/.]+)/suggested/$',
-            EntryViewSet.as_view({'get': 'list'}),
-            name='entry-suggested'
-            ),
-    re_path(r'^drf-entries/(?P<entry_pk>[^/.]+)/suggested/$',
-            DRFEntryViewSet.as_view({'get': 'list'}),
-            name='drf-entry-suggested'
-            ),
-    re_path(r'entries/(?P<entry_pk>[^/.]+)/authors$',
-            AuthorViewSet.as_view({'get': 'list'}),
-            name='entry-authors'),
-    re_path(r'entries/(?P<entry_pk>[^/.]+)/featured$',
-            EntryViewSet.as_view({'get': 'retrieve'}),
-            name='entry-featured'),
-
-    re_path(r'^authors/(?P<pk>[^/.]+)/(?P<related_field>\w+)/$',
-            AuthorViewSet.as_view({'get': 'retrieve_related'}),
-            name='author-related'),
-
-    re_path(r'^entries/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$',
-            EntryRelationshipView.as_view(),
-            name='entry-relationships'),
-    re_path(r'^blogs/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$',
-            BlogRelationshipView.as_view(),
-            name='blog-relationships'),
-    re_path(r'^comments/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$',
-            CommentRelationshipView.as_view(),
-            name='comment-relationships'),
-    re_path(r'^authors/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$',
-            AuthorRelationshipView.as_view(),
-            name='author-relationships'),
+    path(
+        "identities/default/<int:pk>",
+        GenericIdentity.as_view(),
+        name="user-default",
+    ),
+    re_path(
+        r"^entries/(?P<entry_pk>[^/.]+)/blog$",
+        BlogViewSet.as_view({"get": "retrieve"}),
+        name="entry-blog",
+    ),
+    re_path(
+        r"^entries/(?P<entry_pk>[^/.]+)/comments$",
+        CommentViewSet.as_view({"get": "list"}),
+        name="entry-comments",
+    ),
+    re_path(
+        r"^entries/(?P<entry_pk>[^/.]+)/suggested/$",
+        EntryViewSet.as_view({"get": "list"}),
+        name="entry-suggested",
+    ),
+    re_path(
+        r"^drf-entries/(?P<entry_pk>[^/.]+)/suggested/$",
+        DRFEntryViewSet.as_view({"get": "list"}),
+        name="drf-entry-suggested",
+    ),
+    re_path(
+        r"entries/(?P<entry_pk>[^/.]+)/authors$",
+        AuthorViewSet.as_view({"get": "list"}),
+        name="entry-authors",
+    ),
+    re_path(
+        r"entries/(?P<entry_pk>[^/.]+)/featured$",
+        EntryViewSet.as_view({"get": "retrieve"}),
+        name="entry-featured",
+    ),
+    re_path(
+        r"^authors/(?P<pk>[^/.]+)/(?P<related_field>[-\w]+)/$",
+        AuthorViewSet.as_view({"get": "retrieve_related"}),
+        name="author-related",
+    ),
+    re_path(
+        r"^entries/(?P<pk>[^/.]+)/relationships/(?P<related_field>[\-\w]+)$",
+        EntryRelationshipView.as_view(),
+        name="entry-relationships",
+    ),
+    re_path(
+        r"^blogs/(?P<pk>[^/.]+)/relationships/(?P<related_field>[^/.]+)$",
+        BlogRelationshipView.as_view(),
+        name="blog-relationships",
+    ),
+    re_path(
+        r"^comments/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$",
+        CommentRelationshipView.as_view(),
+        name="comment-relationships",
+    ),
+    re_path(
+        r"^authors/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)$",
+        AuthorRelationshipView.as_view(),
+        name="author-relationships",
+    ),
 ]
 
 urlpatterns += router.urls
diff --git a/example/utils.py b/example/utils.py
index 65403038..1c9ef459 100644
--- a/example/utils.py
+++ b/example/utils.py
@@ -6,7 +6,7 @@ class BrowsableAPIRendererWithoutForms(BrowsableAPIRenderer):
 
     def get_context(self, *args, **kwargs):
         ctx = super().get_context(*args, **kwargs)
-        ctx['display_edit_forms'] = False
+        ctx["display_edit_forms"] = False
         return ctx
 
     def show_form_for_method(self, view, method, request, obj):
diff --git a/example/views.py b/example/views.py
index 8c80d145..da171698 100644
--- a/example/views.py
+++ b/example/views.py
@@ -8,13 +8,32 @@
 import rest_framework_json_api.parsers
 import rest_framework_json_api.renderers
 from rest_framework_json_api.django_filters import DjangoFilterBackend
-from rest_framework_json_api.filters import OrderingFilter, QueryParameterValidationFilter
+from rest_framework_json_api.filters import (
+    OrderingFilter,
+    QueryParameterValidationFilter,
+)
 from rest_framework_json_api.pagination import JsonApiPageNumberPagination
 from rest_framework_json_api.utils import format_drf_errors
-from rest_framework_json_api.views import ModelViewSet, RelationshipView
+from rest_framework_json_api.views import (
+    ModelViewSet,
+    ReadOnlyModelViewSet,
+    RelationshipView,
+)
 
-from example.models import Author, Blog, Comment, Company, Entry, Project, ProjectType
+from example.models import (
+    Author,
+    Blog,
+    Comment,
+    Company,
+    Entry,
+    LabResults,
+    Project,
+    ProjectType,
+    Questionnaire,
+)
 from example.serializers import (
+    AuthorDetailSerializer,
+    AuthorListSerializer,
     AuthorSerializer,
     BlogDRFSerializer,
     BlogSerializer,
@@ -22,8 +41,10 @@
     CompanySerializer,
     EntryDRFSerializers,
     EntrySerializer,
+    LabResultsSerializer,
     ProjectSerializer,
-    ProjectTypeSerializer
+    ProjectTypeSerializer,
+    QuestionnaireSerializer,
 )
 
 HTTP_422_UNPROCESSABLE_ENTITY = 422
@@ -34,24 +55,24 @@ class BlogViewSet(ModelViewSet):
     serializer_class = BlogSerializer
 
     def get_object(self):
-        entry_pk = self.kwargs.get('entry_pk', None)
+        entry_pk = self.kwargs.get("entry_pk", None)
         if entry_pk is not None:
             return Entry.objects.get(id=entry_pk).blog
 
-        return super(BlogViewSet, self).get_object()
+        return super().get_object()
 
 
 class DRFBlogViewSet(ModelViewSet):
     queryset = Blog.objects.all()
     serializer_class = BlogDRFSerializer
-    lookup_url_kwarg = 'entry_pk'
+    lookup_url_kwarg = "entry_pk"
 
     def get_object(self):
         entry_pk = self.kwargs.get(self.lookup_url_kwarg, None)
         if entry_pk is not None:
             return Entry.objects.get(id=entry_pk).blog
 
-        return super(DRFBlogViewSet, self).get_object()
+        return super().get_object()
 
 
 class JsonApiViewSet(ModelViewSet):
@@ -60,6 +81,7 @@ class JsonApiViewSet(ModelViewSet):
     within a class. It allows using DRF-jsonapi alongside
     vanilla DRF API views.
     """
+
     parser_classes = [
         rest_framework_json_api.parsers.JSONParser,
         rest_framework.parsers.FormParser,
@@ -78,7 +100,7 @@ def handle_exception(self, exc):
             exc.status_code = HTTP_422_UNPROCESSABLE_ENTITY
         # exception handler can't be set on class so you have to
         # override the error response in this method
-        response = super(JsonApiViewSet, self).handle_exception(exc)
+        response = super().handle_exception(exc)
         context = self.get_exception_handler_context()
         return format_drf_errors(response, context, exc)
 
@@ -90,24 +112,27 @@ class BlogCustomViewSet(JsonApiViewSet):
 
 class EntryViewSet(ModelViewSet):
     queryset = Entry.objects.all()
-    resource_name = 'posts'
+    # TODO it should not be supported to overwrite resource name
+    # of endpoints with serializers as includes and sparse fields
+    # cannot be aware of it
+    # resource_name = "posts"
 
     def get_serializer_class(self):
         return EntrySerializer
 
     def get_object(self):
         # Handle featured
-        entry_pk = self.kwargs.get('entry_pk', None)
+        entry_pk = self.kwargs.get("entry_pk", None)
         if entry_pk is not None:
             return Entry.objects.exclude(pk=entry_pk).first()
 
-        return super(EntryViewSet, self).get_object()
+        return super().get_object()
 
 
 class DRFEntryViewSet(ModelViewSet):
     queryset = Entry.objects.all()
     serializer_class = EntryDRFSerializers
-    lookup_url_kwarg = 'entry_pk'
+    lookup_url_kwarg = "entry_pk"
 
     def get_object(self):
         # Handle featured
@@ -115,7 +140,7 @@ def get_object(self):
         if entry_pk is not None:
             return Entry.objects.exclude(pk=entry_pk).first()
 
-        return super(DRFEntryViewSet, self).get_object()
+        return super().get_object()
 
 
 class NoPagination(JsonApiPageNumberPagination):
@@ -124,32 +149,44 @@ class NoPagination(JsonApiPageNumberPagination):
 
 class NonPaginatedEntryViewSet(EntryViewSet):
     pagination_class = NoPagination
-    # override the default filter backends in order to test QueryParameterValidationFilter without
-    # breaking older usage of non-standard query params like `page_size`.
-    filter_backends = (QueryParameterValidationFilter, OrderingFilter,
-                       DjangoFilterBackend, SearchFilter)
-    ordering_fields = ('headline', 'body_text', 'blog__name', 'blog__id')
-    rels = ('exact', 'iexact',
-            'contains', 'icontains',
-            'gt', 'gte', 'lt', 'lte',
-            'in', 'regex', 'isnull',)
+    # override the default filter backends in order to test QueryParameterValidationFilter
+    # without breaking older usage of non-standard query params like `page_size`.
+    filter_backends = (
+        QueryParameterValidationFilter,
+        OrderingFilter,
+        DjangoFilterBackend,
+        SearchFilter,
+    )
+    ordering_fields = ("headline", "body_text", "blog__name", "blog__id")
+    rels = (
+        "exact",
+        "iexact",
+        "contains",
+        "icontains",
+        "gt",
+        "gte",
+        "lt",
+        "lte",
+        "in",
+        "regex",
+        "isnull",
+    )
     filterset_fields = {
-        'id': ('exact', 'in'),
-        'headline': rels,
-        'body_text': rels,
-        'blog__name': rels,
-        'blog__tagline': rels,
+        "id": ("exact", "in"),
+        "headline": rels,
+        "body_text": rels,
+        "blog__name": rels,
+        "blog__tagline": rels,
     }
-    search_fields = ('headline', 'body_text', 'blog__name', 'blog__tagline')
+    search_fields = ("headline", "body_text", "blog__name", "blog__tagline")
 
 
 class EntryFilter(filters.FilterSet):
-    bname = filters.CharFilter(field_name="blog__name",
-                               lookup_expr="exact")
+    bname = filters.CharFilter(field_name="blog__name", lookup_expr="exact")
 
     authors__id = filters.ModelMultipleChoiceFilter(
-        field_name='authors',
-        to_field_name='id',
+        field_name="authors",
+        to_field_name="id",
         conjoined=True,  # to "and" the ids
         queryset=Author.objects.all(),
     )
@@ -157,10 +194,10 @@ class EntryFilter(filters.FilterSet):
     class Meta:
         model = Entry
         fields = {
-            'id': ('exact',),
-            'headline': ('exact',),
-            'body_text': ('exact',),
-            'authors__id': ('in',),
+            "id": ("exact",),
+            "headline": ("exact",),
+            "body_text": ("exact",),
+            "authors__id": ("in",),
         }
 
 
@@ -168,16 +205,21 @@ class FiltersetEntryViewSet(EntryViewSet):
     """
     like above but use filterset_class instead of filterset_fields
     """
+
     pagination_class = NoPagination
     filterset_fields = None
     filterset_class = EntryFilter
-    filter_backends = (QueryParameterValidationFilter, DjangoFilterBackend,)
+    filter_backends = (
+        QueryParameterValidationFilter,
+        DjangoFilterBackend,
+    )
 
 
 class NoFiltersetEntryViewSet(EntryViewSet):
     """
     like above but no filtersets
     """
+
     pagination_class = NoPagination
     filterset_fields = None
     filterset_class = None
@@ -185,26 +227,35 @@ class NoFiltersetEntryViewSet(EntryViewSet):
 
 class AuthorViewSet(ModelViewSet):
     queryset = Author.objects.all()
-    serializer_class = AuthorSerializer
+    filterset_fields = ("author_type", "name")
+
+    def get_serializer_class(self):
+        serializer_classes = {
+            "list": AuthorListSerializer,
+            "retrieve": AuthorDetailSerializer,
+        }
+
+        action = getattr(self, "action", "")
+        return serializer_classes.get(action, AuthorSerializer)
 
 
 class CommentViewSet(ModelViewSet):
     queryset = Comment.objects.all()
     serializer_class = CommentSerializer
-    select_for_includes = {
-        'writer': ['author__bio']
-    }
+    select_for_includes = {"writer": ["author__bio"]}
     prefetch_for_includes = {
-        '__all__': [],
-        'author': ['author__bio', 'author__entries'],
+        "__all__": [],
+        "author": ["author__bio", "author__entries"],
     }
 
     def get_queryset(self, *args, **kwargs):
-        entry_pk = self.kwargs.get('entry_pk', None)
+        queryset = super().get_queryset()
+
+        entry_pk = self.kwargs.get("entry_pk", None)
         if entry_pk is not None:
-            return self.queryset.filter(entry_id=entry_pk)
+            queryset = queryset.filter(entry_id=entry_pk)
 
-        return super(CommentViewSet, self).get_queryset()
+        return queryset
 
 
 class CompanyViewset(ModelViewSet):
@@ -213,7 +264,7 @@ class CompanyViewset(ModelViewSet):
 
 
 class ProjectViewset(ModelViewSet):
-    queryset = Project.objects.all().order_by('pk')
+    queryset = Project.objects.all().order_by("pk")
     serializer_class = ProjectSerializer
 
 
@@ -236,4 +287,18 @@ class CommentRelationshipView(RelationshipView):
 
 class AuthorRelationshipView(RelationshipView):
     queryset = Author.objects.all()
-    self_link_view_name = 'author-relationships'
+    self_link_view_name = "author-relationships"
+
+
+class LabResultViewSet(ReadOnlyModelViewSet):
+    queryset = LabResults.objects.all()
+    serializer_class = LabResultsSerializer
+    prefetch_for_includes = {
+        "__all__": [],
+        "author": ["author__bio", "author__entries"],
+    }
+
+
+class QuestionnaireViewset(ModelViewSet):
+    queryset = Questionnaire.objects.all()
+    serializer_class = QuestionnaireSerializer
diff --git a/requirements.txt b/requirements.txt
index f1ba742f..ba98a85d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-# The base set of requirements for Django REST framework JSON API is actually
+# The base set of requirements for Django REST framework JSON:API is actually
 # fairly small, but for the purposes of development and testing
 # there are a number of packages that are useful to install.
 
diff --git a/requirements/requirements-codestyle.txt b/requirements/requirements-codestyle.txt
index c144f975..707bddb9 100644
--- a/requirements/requirements-codestyle.txt
+++ b/requirements/requirements-codestyle.txt
@@ -1,3 +1,5 @@
-flake8==3.8.4
-flake8-isort==4.0.0
-isort==5.6.4
+black==25.1.0
+flake8==7.3.0
+flake8-bugbear==24.12.12
+flake8-isort==6.1.2
+isort==6.0.1
diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt
index e8378b09..76f3bf9a 100644
--- a/requirements/requirements-documentation.txt
+++ b/requirements/requirements-documentation.txt
@@ -1,3 +1,3 @@
-recommonmark==0.6.0
-Sphinx==3.3.0
-sphinx_rtd_theme==0.5.0
+recommonmark==0.7.1
+Sphinx==8.1.3
+sphinx_rtd_theme==3.0.2
diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt
index e5ceb15f..4eb517ff 100644
--- a/requirements/requirements-optionals.txt
+++ b/requirements/requirements-optionals.txt
@@ -1,4 +1,2 @@
-django-filter==2.4.0
-django-polymorphic==3.0.0
-pyyaml==5.3.1
-uritemplate==3.0.1
+django-filter==25.1
+django-polymorphic==4.1.0
diff --git a/requirements/requirements-packaging.txt b/requirements/requirements-packaging.txt
index 7bc36909..54882779 100644
--- a/requirements/requirements-packaging.txt
+++ b/requirements/requirements-packaging.txt
@@ -1 +1 @@
-twine==3.2.0
+twine==6.1.0
diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt
index 9508b0a3..8fe1e366 100644
--- a/requirements/requirements-testing.txt
+++ b/requirements/requirements-testing.txt
@@ -1,8 +1,7 @@
-django-debug-toolbar==3.1.1
-factory-boy==3.1.0
-Faker==4.14.0
-pytest==6.1.2
-pytest-cov==2.10.1
-pytest-django==4.1.0
-pytest-factoryboy==2.0.3
-snapshottest==0.6.0
+factory-boy==3.3.3
+Faker==37.5.3
+pytest==8.4.1
+pytest-cov==6.2.1
+pytest-django==4.11.1
+pytest-factoryboy==2.8.1
+syrupy==4.9.1
diff --git a/rest_framework_json_api/__init__.py b/rest_framework_json_api/__init__.py
index f059f020..23f6b8f9 100644
--- a/rest_framework_json_api/__init__.py
+++ b/rest_framework_json_api/__init__.py
@@ -1,10 +1,8 @@
-# -*- coding: utf-8 -*-
-
-__title__ = 'djangorestframework-jsonapi'
-__version__ = '4.0.0'
-__author__ = ''
-__license__ = 'BSD'
-__copyright__ = ''
+__title__ = "djangorestframework-jsonapi"
+__version__ = "8.0.0"
+__author__ = ""
+__license__ = "BSD"
+__copyright__ = ""
 
 # Version synonym
 VERSION = __version__
diff --git a/example/tests/snapshots/__init__.py b/rest_framework_json_api/compat.py
similarity index 100%
rename from example/tests/snapshots/__init__.py
rename to rest_framework_json_api/compat.py
diff --git a/rest_framework_json_api/django_filters/backends.py b/rest_framework_json_api/django_filters/backends.py
index 814a79f3..70e543c1 100644
--- a/rest_framework_json_api/django_filters/backends.py
+++ b/rest_framework_json_api/django_filters/backends.py
@@ -4,18 +4,18 @@
 from rest_framework.exceptions import ValidationError
 from rest_framework.settings import api_settings
 
-from rest_framework_json_api.utils import format_value
+from rest_framework_json_api.utils import undo_format_field_name
 
 
 class DjangoFilterBackend(DjangoFilterBackend):
     """
     A Django-style ORM filter implementation, using `django-filter`.
 
-    This is not part of the jsonapi standard per-se, other than the requirement
+    This is not part of the JSON:API standard per-se, other than the requirement
     to use the `filter` keyword: This is an optional implementation of style of
     filtering in which each filter is an ORM expression as implemented by
     DjangoFilterBackend and seems to be in alignment with an interpretation of
-    http://jsonapi.org/recommendations/#filtering, including relationship
+    https://jsonapi.org/recommendations/#filtering, including relationship
     chaining. It also returns a 400 error for invalid filters.
 
     Filters can be:
@@ -44,26 +44,31 @@ class DjangoFilterBackend(DjangoFilterBackend):
     - A related resource path
       can be used:
 
-      ``?filter[inventory.item.partNum]=123456`` (where `inventory.item` is the relationship path)
+      ``?filter[inventory.item.partNum]=123456``
+
+      where `inventory.item` is the relationship path.
 
     If you are also using rest_framework.filters.SearchFilter you'll want to customize
     the name of the query parameter for searching to make sure it doesn't conflict
     with a field name defined in the filterset.
     The recommended value is: `search_param="filter[search]"` but just make sure it's
-    `filter[<something>]` to comply with the jsonapi spec requirement to use the filter
+    `filter[<something>]` to comply with the JSON:API spec requirement to use the filter
     keyword. The default is "search" unless overriden but it's used here just to make sure
     we don't complain about it being an invalid filter.
     """
+
     search_param = api_settings.SEARCH_PARAM
 
     # Make this regex check for 'filter' as well as 'filter[...]'
-    # See http://jsonapi.org/format/#document-member-names for allowed characters
-    # and http://jsonapi.org/format/#document-member-names-reserved-characters for reserved
+    # See https://jsonapi.org/format/#document-member-names for allowed characters
+    # and https://jsonapi.org/format/#document-member-names-reserved-characters for reserved
     # characters (for use in paths, lists or as delimiters).
     # regex `\w` matches [a-zA-Z0-9_].
     # TODO: U+0080 and above allowed but not recommended. Leave them out for now.e
     #       Also, ' ' (space) is allowed within a member name but not recommended.
-    filter_regex = re.compile(r'^filter(?P<ldelim>\[?)(?P<assoc>[\w\.\-]*)(?P<rdelim>\]?$)')
+    filter_regex = re.compile(
+        r"^filter(?P<ldelim>\[?)(?P<assoc>[\w\.\-]*)(?P<rdelim>\]?$)"
+    )
 
     def _validate_filter(self, keys, filterset_class):
         """
@@ -74,8 +79,8 @@ def _validate_filter(self, keys, filterset_class):
         :raises ValidationError: if key not in FilterSet keys or no FilterSet.
         """
         for k in keys:
-            if ((not filterset_class) or (k not in filterset_class.base_filters)):
-                raise ValidationError("invalid filter[{}]".format(k))
+            if (not filterset_class) or (k not in filterset_class.base_filters):
+                raise ValidationError(f"invalid filter[{k}]")
 
     def get_filterset(self, request, queryset, view):
         """
@@ -86,7 +91,7 @@ def get_filterset(self, request, queryset, view):
         # TODO: .base_filters vs. .filters attr (not always present)
         filterset_class = self.get_filterset_class(view, queryset)
         kwargs = self.get_filterset_kwargs(request, queryset, view)
-        self._validate_filter(kwargs.pop('filter_keys'), filterset_class)
+        self._validate_filter(kwargs.pop("filter_keys"), filterset_class)
         if filterset_class is None:
             return None
         return filterset_class(**kwargs)
@@ -103,36 +108,24 @@ def get_filterset_kwargs(self, request, queryset, view):
         data = request.query_params.copy()
         for qp, val in request.query_params.lists():
             m = self.filter_regex.match(qp)
-            if m and (not m.groupdict()['assoc'] or
-                      m.groupdict()['ldelim'] != '[' or m.groupdict()['rdelim'] != ']'):
-                raise ValidationError("invalid query parameter: {}".format(qp))
+            if m and (
+                not m.groupdict()["assoc"]
+                or m.groupdict()["ldelim"] != "["
+                or m.groupdict()["rdelim"] != "]"
+            ):
+                raise ValidationError(f"invalid query parameter: {qp}")
             if m and qp != self.search_param:
                 if not all(val):
-                    raise ValidationError("missing value for query parameter {}".format(qp))
-                # convert jsonapi relationship path to Django ORM's __ notation
-                key = m.groupdict()['assoc'].replace('.', '__')
-                # undo JSON_API_FORMAT_FIELD_NAMES conversion:
-                key = format_value(key, 'underscore')
+                    raise ValidationError(f"missing value for query parameter {qp}")
+                # convert JSON:API relationship path to Django ORM's __ notation
+                key = m.groupdict()["assoc"].replace(".", "__")
+                key = undo_format_field_name(key)
                 data.setlist(key, val)
                 filter_keys.append(key)
                 del data[qp]
         return {
-            'data': data,
-            'queryset': queryset,
-            'request': request,
-            'filter_keys': filter_keys,
+            "data": data,
+            "queryset": queryset,
+            "request": request,
+            "filter_keys": filter_keys,
         }
-
-    def get_schema_operation_parameters(self, view):
-        """
-        Convert backend filter `name` to JSON:API-style `filter[name]`.
-        For filters that are relationship paths, rewrite ORM-style `__` to our preferred `.`.
-        For example: `blog__name__contains` becomes `filter[blog.name.contains]`.
-
-        This is basically the reverse of `get_filterset_kwargs` above.
-        """
-        result = super(DjangoFilterBackend, self).get_schema_operation_parameters(view)
-        for res in result:
-            if 'name' in res:
-                res['name'] = 'filter[{}]'.format(res['name']).replace('__', '.')
-        return result
diff --git a/rest_framework_json_api/exceptions.py b/rest_framework_json_api/exceptions.py
index 938a0c77..1c7acbdb 100644
--- a/rest_framework_json_api/exceptions.py
+++ b/rest_framework_json_api/exceptions.py
@@ -8,7 +8,8 @@
 
 def rendered_with_json_api(view):
     from rest_framework_json_api.renderers import JSONRenderer
-    for renderer_class in getattr(view, 'renderer_classes', []):
+
+    for renderer_class in getattr(view, "renderer_classes", []):
         if issubclass(renderer_class, JSONRenderer):
             return True
     return False
@@ -17,8 +18,9 @@ def rendered_with_json_api(view):
 def exception_handler(exc, context):
     # Import this here to avoid potential edge-case circular imports, which
     # crashes with:
-    # "ImportError: Could not import 'rest_framework_json_api.parsers.JSONParser' for API setting
-    # 'DEFAULT_PARSER_CLASSES'. ImportError: cannot import name 'exceptions'.'"
+    # "ImportError: Could not import 'rest_framework_json_api.parsers.JSONParser' for
+    # API setting 'DEFAULT_PARSER_CLASSES'.
+    # ImportError: cannot import name 'exceptions'.'"
     #
     # Also see: https://github.com/django-json-api/django-rest-framework-json-api/issues/158
     from rest_framework.views import exception_handler as drf_exception_handler
@@ -28,16 +30,16 @@ def exception_handler(exc, context):
     if not response:
         return response
 
-    # Use regular DRF format if not rendered by DRF JSON API and not uniform
-    is_json_api_view = rendered_with_json_api(context['view'])
+    # Use regular DRF format if not rendered by DRF JSON:API and not uniform
+    is_json_api_view = rendered_with_json_api(context["view"])
     is_uniform = json_api_settings.UNIFORM_EXCEPTIONS
     if not is_json_api_view and not is_uniform:
         return response
 
-    # Convert to DRF JSON API error format
+    # Convert to DRF JSON:API error format
     response = utils.format_drf_errors(response, context, exc)
 
-    # Add top-level 'errors' object when not rendered by DRF JSON API
+    # Add top-level 'errors' object when not rendered by DRF JSON:API
     if not is_json_api_view:
         response.data = utils.format_errors(response.data)
 
@@ -46,4 +48,4 @@ def exception_handler(exc, context):
 
 class Conflict(exceptions.APIException):
     status_code = status.HTTP_409_CONFLICT
-    default_detail = _('Conflict.')
+    default_detail = _("Conflict.")
diff --git a/rest_framework_json_api/filters.py b/rest_framework_json_api/filters.py
index eafcdb76..ead6f84b 100644
--- a/rest_framework_json_api/filters.py
+++ b/rest_framework_json_api/filters.py
@@ -3,24 +3,28 @@
 from rest_framework.exceptions import ValidationError
 from rest_framework.filters import BaseFilterBackend, OrderingFilter
 
-from rest_framework_json_api.utils import format_value
+from rest_framework_json_api.utils import undo_format_field_name
 
 
 class OrderingFilter(OrderingFilter):
     """
-    A backend filter that implements http://jsonapi.org/format/#fetching-sorting and
+    A backend filter that implements https://jsonapi.org/format/#fetching-sorting and
     raises a 400 error if any sort field is invalid.
 
     If you prefer *not* to report 400 errors for invalid sort fields, just use
     :py:class:`rest_framework.filters.OrderingFilter` with
     :py:attr:`~rest_framework.filters.OrderingFilter.ordering_param` = "sort"
 
-    Also applies DJA format_value() to convert (e.g. camelcase) to underscore.
+    Also supports undo of field name formatting
     (See JSON_API_FORMAT_FIELD_NAMES in docs/usage.md)
     """
+
     #: override :py:attr:`rest_framework.filters.OrderingFilter.ordering_param`
     #: with JSON:API-compliant query parameter name.
-    ordering_param = 'sort'
+    ordering_param = "sort"
+    ordering_description = (
+        "[list of fields to sort by]" "(https://jsonapi.org/format/#fetching-sorting)"
+    )
 
     def remove_invalid_fields(self, queryset, fields, view, request):
         """
@@ -31,16 +35,21 @@ def remove_invalid_fields(self, queryset, fields, view, request):
         :raises ValidationError: if a sort field is invalid.
         """
         valid_fields = [
-            item[0] for item in self.get_valid_fields(queryset, view,
-                                                      {'request': request})
+            item[0]
+            for item in self.get_valid_fields(queryset, view, {"request": request})
         ]
         bad_terms = [
-            term for term in fields
-            if format_value(term.replace(".", "__").lstrip('-'), "underscore") not in valid_fields
+            term
+            for term in fields
+            if undo_format_field_name(term.replace(".", "__").lstrip("-"))
+            not in valid_fields
         ]
         if bad_terms:
-            raise ValidationError('invalid sort parameter{}: {}'.format(
-                ('s' if len(bad_terms) > 1 else ''), ','.join(bad_terms)))
+            raise ValidationError(
+                "invalid sort parameter{}: {}".format(
+                    ("s" if len(bad_terms) > 1 else ""), ",".join(bad_terms)
+                )
+            )
         # this looks like it duplicates code above, but we want the ValidationError to report
         # the actual parameter supplied while we want the fields passed to the super() to
         # be correctly rewritten.
@@ -48,14 +57,14 @@ def remove_invalid_fields(self, queryset, fields, view, request):
         underscore_fields = []
         for item in fields:
             item_rewritten = item.replace(".", "__")
-            if item_rewritten.startswith('-'):
+            if item_rewritten.startswith("-"):
                 underscore_fields.append(
-                    '-' + format_value(item_rewritten.lstrip('-'), "underscore"))
+                    "-" + undo_format_field_name(item_rewritten.lstrip("-"))
+                )
             else:
-                underscore_fields.append(format_value(item_rewritten, "underscore"))
+                underscore_fields.append(undo_format_field_name(item_rewritten))
 
-        return super(OrderingFilter, self).remove_invalid_fields(
-            queryset, underscore_fields, view, request)
+        return super().remove_invalid_fields(queryset, underscore_fields, view, request)
 
 
 class QueryParameterValidationFilter(BaseFilterBackend):
@@ -66,11 +75,14 @@ class QueryParameterValidationFilter(BaseFilterBackend):
 
     If you want to add some additional non-standard query parameters,
     override :py:attr:`query_regex` adding the new parameters. Make sure to comply with
-    the rules at http://jsonapi.org/format/#query-parameters.
+    the rules at https://jsonapi.org/format/#query-parameters.
     """
-    #: compiled regex that matches the allowed http://jsonapi.org/format/#query-parameters:
+
+    #: compiled regex that matches the allowed https://jsonapi.org/format/#query-parameters:
     #: `sort` and `include` stand alone; `filter`, `fields`, and `page` have []'s
-    query_regex = re.compile(r'^(sort|include)$|^(?P<type>filter|fields|page)(\[[\w\.\-]+\])?$')
+    query_regex = re.compile(
+        r"^(sort|include)$|^(?P<type>filter|fields|page)(\[[\w\.\-]+\])?$"
+    )
 
     def validate_query_params(self, request):
         """
@@ -79,15 +91,17 @@ def validate_query_params(self, request):
 
         :raises ValidationError: if not.
         """
-        # TODO: For jsonapi error object conformance, must set jsonapi errors "parameter" for
+        # TODO: For JSON:API error object conformance, must set JSON:API errors "parameter" for
         # the ValidationError. This requires extending DRF/DJA Exceptions.
         for qp in request.query_params.keys():
             m = self.query_regex.match(qp)
             if not m:
-                raise ValidationError('invalid query parameter: {}'.format(qp))
-            if not m.group('type') == 'filter' and len(request.query_params.getlist(qp)) > 1:
-                raise ValidationError(
-                    'repeated query parameter not allowed: {}'.format(qp))
+                raise ValidationError(f"invalid query parameter: {qp}")
+            if (
+                not m.group("type") == "filter"
+                and len(request.query_params.getlist(qp)) > 1
+            ):
+                raise ValidationError(f"repeated query parameter not allowed: {qp}")
 
     def filter_queryset(self, request, queryset, view):
         """
diff --git a/rest_framework_json_api/metadata.py b/rest_framework_json_api/metadata.py
index ef3356fe..e761e919 100644
--- a/rest_framework_json_api/metadata.py
+++ b/rest_framework_json_api/metadata.py
@@ -1,5 +1,3 @@
-from collections import OrderedDict
-
 from django.db.models.fields import related
 from django.utils.encoding import force_str
 from rest_framework import serializers
@@ -7,7 +5,7 @@
 from rest_framework.settings import api_settings
 from rest_framework.utils.field_mapping import ClassLookupDict
 
-from rest_framework_json_api.utils import format_value, get_related_resource_type
+from rest_framework_json_api.utils import format_field_name, get_related_resource_type
 
 
 class JSONAPIMetadata(SimpleMetadata):
@@ -17,56 +15,64 @@ class JSONAPIMetadata(SimpleMetadata):
     There are not any formalized standards for `OPTIONS` responses
     for us to base this on.
     """
-    type_lookup = ClassLookupDict({
-        serializers.Field: 'GenericField',
-        serializers.RelatedField: 'Relationship',
-        serializers.BooleanField: 'Boolean',
-        serializers.NullBooleanField: 'Boolean',
-        serializers.CharField: 'String',
-        serializers.URLField: 'URL',
-        serializers.EmailField: 'Email',
-        serializers.RegexField: 'Regex',
-        serializers.SlugField: 'Slug',
-        serializers.IntegerField: 'Integer',
-        serializers.FloatField: 'Float',
-        serializers.DecimalField: 'Decimal',
-        serializers.DateField: 'Date',
-        serializers.DateTimeField: 'DateTime',
-        serializers.TimeField: 'Time',
-        serializers.ChoiceField: 'Choice',
-        serializers.MultipleChoiceField: 'MultipleChoice',
-        serializers.FileField: 'File',
-        serializers.ImageField: 'Image',
-        serializers.ListField: 'List',
-        serializers.DictField: 'Dict',
-        serializers.Serializer: 'Serializer',
-    })
+
+    type_lookup = ClassLookupDict(
+        {
+            serializers.Field: "GenericField",
+            serializers.RelatedField: "Relationship",
+            serializers.BooleanField: "Boolean",
+            serializers.CharField: "String",
+            serializers.URLField: "URL",
+            serializers.EmailField: "Email",
+            serializers.RegexField: "Regex",
+            serializers.SlugField: "Slug",
+            serializers.IntegerField: "Integer",
+            serializers.FloatField: "Float",
+            serializers.DecimalField: "Decimal",
+            serializers.DateField: "Date",
+            serializers.DateTimeField: "DateTime",
+            serializers.TimeField: "Time",
+            serializers.ChoiceField: "Choice",
+            serializers.MultipleChoiceField: "MultipleChoice",
+            serializers.FileField: "File",
+            serializers.ImageField: "Image",
+            serializers.ListField: "List",
+            serializers.DictField: "Dict",
+            serializers.Serializer: "Serializer",
+        }
+    )
 
     try:
-        relation_type_lookup = ClassLookupDict({
-            related.ManyToManyDescriptor: 'ManyToMany',
-            related.ReverseManyToOneDescriptor: 'OneToMany',
-            related.ForwardManyToOneDescriptor: 'ManyToOne',
-        })
+        relation_type_lookup = ClassLookupDict(
+            {
+                related.ManyToManyDescriptor: "ManyToMany",
+                related.ReverseManyToOneDescriptor: "OneToMany",
+                related.ForwardManyToOneDescriptor: "ManyToOne",
+            }
+        )
     except AttributeError:
-        relation_type_lookup = ClassLookupDict({
-            related.ManyRelatedObjectsDescriptor: 'ManyToMany',
-            related.ReverseManyRelatedObjectsDescriptor: 'ManyToMany',
-            related.ForeignRelatedObjectsDescriptor: 'OneToMany',
-            related.ReverseSingleRelatedObjectDescriptor: 'ManyToOne',
-        })
+        relation_type_lookup = ClassLookupDict(
+            {
+                related.ManyRelatedObjectsDescriptor: "ManyToMany",
+                related.ReverseManyRelatedObjectsDescriptor: "ManyToMany",
+                related.ForeignRelatedObjectsDescriptor: "OneToMany",
+                related.ReverseSingleRelatedObjectDescriptor: "ManyToOne",
+            }
+        )
 
     def determine_metadata(self, request, view):
-        metadata = OrderedDict()
-        metadata['name'] = view.get_view_name()
-        metadata['description'] = view.get_view_description()
-        metadata['renders'] = [renderer.media_type for renderer in view.renderer_classes]
-        metadata['parses'] = [parser.media_type for parser in view.parser_classes]
-        metadata['allowed_methods'] = view.allowed_methods
-        if hasattr(view, 'get_serializer'):
+        metadata = {}
+        metadata["name"] = view.get_view_name()
+        metadata["description"] = view.get_view_description()
+        metadata["renders"] = [
+            renderer.media_type for renderer in view.renderer_classes
+        ]
+        metadata["parses"] = [parser.media_type for parser in view.parser_classes]
+        metadata["allowed_methods"] = view.allowed_methods
+        if hasattr(view, "get_serializer"):
             actions = self.determine_actions(request, view)
             if actions:
-                metadata['actions'] = actions
+                metadata["actions"] = actions
         return metadata
 
     def get_serializer_info(self, serializer):
@@ -74,7 +80,7 @@ def get_serializer_info(self, serializer):
         Given an instance of a serializer, return a dictionary of metadata
         about its fields.
         """
-        if hasattr(serializer, 'child'):
+        if hasattr(serializer, "child"):
             # If this is a `ListSerializer` then we want to examine the
             # underlying child serializer instance instead.
             serializer = serializer.child
@@ -82,27 +88,27 @@ def get_serializer_info(self, serializer):
         # Remove the URL field if present
         serializer.fields.pop(api_settings.URL_FIELD_NAME, None)
 
-        return OrderedDict([
-            (format_value(field_name), self.get_field_info(field))
+        return {
+            format_field_name(field_name): self.get_field_info(field)
             for field_name, field in serializer.fields.items()
-        ])
+        }
 
     def get_field_info(self, field):
         """
         Given an instance of a serializer field, return a dictionary
         of metadata about it.
         """
-        field_info = OrderedDict()
+        field_info = {}
         serializer = field.parent
 
         if isinstance(field, serializers.ManyRelatedField):
-            field_info['type'] = self.type_lookup[field.child_relation]
+            field_info["type"] = self.type_lookup[field.child_relation]
         else:
-            field_info['type'] = self.type_lookup[field]
+            field_info["type"] = self.type_lookup[field]
 
         try:
-            serializer_model = getattr(serializer.Meta, 'model')
-            field_info['relationship_type'] = self.relation_type_lookup[
+            serializer_model = serializer.Meta.model
+            field_info["relationship_type"] = self.relation_type_lookup[
                 getattr(serializer_model, field.field_name)
             ]
         except KeyError:
@@ -110,40 +116,51 @@ def get_field_info(self, field):
         except AttributeError:
             pass
         else:
-            field_info['relationship_resource'] = get_related_resource_type(field)
+            field_info["relationship_resource"] = get_related_resource_type(field)
 
-        field_info['required'] = getattr(field, 'required', False)
+        field_info["required"] = getattr(field, "required", False)
 
         attrs = [
-            'read_only', 'write_only', 'label', 'help_text',
-            'min_length', 'max_length',
-            'min_value', 'max_value', 'initial'
+            "read_only",
+            "write_only",
+            "label",
+            "help_text",
+            "min_length",
+            "max_length",
+            "min_value",
+            "max_value",
+            "initial",
         ]
 
         for attr in attrs:
             value = getattr(field, attr, None)
-            if value is not None and value != '':
+            if value is not None and value != "":
                 field_info[attr] = force_str(value, strings_only=True)
 
-        if getattr(field, 'child', None):
-            field_info['child'] = self.get_field_info(field.child)
-        elif getattr(field, 'fields', None):
-            field_info['children'] = self.get_serializer_info(field)
+        if getattr(field, "child", None):
+            field_info["child"] = self.get_field_info(field.child)
+        elif getattr(field, "fields", None):
+            field_info["children"] = self.get_serializer_info(field)
 
         if (
-            not field_info.get('read_only') and
-            not field_info.get('relationship_resource') and
-            hasattr(field, 'choices')
+            not field_info.get("read_only")
+            and not field_info.get("relationship_resource")
+            and hasattr(field, "choices")
         ):
-            field_info['choices'] = [
+            field_info["choices"] = [
                 {
-                    'value': choice_value,
-                    'display_name': force_str(choice_name, strings_only=True)
+                    "value": choice_value,
+                    "display_name": force_str(choice_name, strings_only=True),
                 }
                 for choice_value, choice_name in field.choices.items()
             ]
 
-        if hasattr(serializer, 'included_serializers') and 'relationship_resource' in field_info:
-            field_info['allows_include'] = field.field_name in serializer.included_serializers
+        if (
+            hasattr(serializer, "included_serializers")
+            and "relationship_resource" in field_info
+        ):
+            field_info["allows_include"] = (
+                field.field_name in serializer.included_serializers
+            )
 
         return field_info
diff --git a/rest_framework_json_api/pagination.py b/rest_framework_json_api/pagination.py
index 2e57b937..0c0d3bb9 100644
--- a/rest_framework_json_api/pagination.py
+++ b/rest_framework_json_api/pagination.py
@@ -1,7 +1,6 @@
 """
 Pagination fields
 """
-from collections import OrderedDict
 
 from rest_framework.pagination import LimitOffsetPagination, PageNumberPagination
 from rest_framework.utils.urls import remove_query_param, replace_query_param
@@ -10,16 +9,17 @@
 
 class JsonApiPageNumberPagination(PageNumberPagination):
     """
-    A json-api compatible pagination format.
+    A JSON:API compatible pagination format.
     """
-    page_query_param = 'page[number]'
-    page_size_query_param = 'page[size]'
+
+    page_query_param = "page[number]"
+    page_size_query_param = "page[size]"
     max_page_size = 100
 
     def build_link(self, index):
         if not index:
             return None
-        url = self.request and self.request.build_absolute_uri() or ''
+        url = self.request and self.request.build_absolute_uri() or ""
         return replace_query_param(url, self.page_query_param, index)
 
     def get_paginated_response(self, data):
@@ -31,22 +31,24 @@ def get_paginated_response(self, data):
         if self.page.has_previous():
             previous = self.page.previous_page_number()
 
-        return Response({
-            'results': data,
-            'meta': {
-                'pagination': OrderedDict([
-                    ('page', self.page.number),
-                    ('pages', self.page.paginator.num_pages),
-                    ('count', self.page.paginator.count),
-                ])
-            },
-            'links': OrderedDict([
-                ('first', self.build_link(1)),
-                ('last', self.build_link(self.page.paginator.num_pages)),
-                ('next', self.build_link(next)),
-                ('prev', self.build_link(previous))
-            ])
-        })
+        return Response(
+            {
+                "results": data,
+                "meta": {
+                    "pagination": {
+                        "page": self.page.number,
+                        "pages": self.page.paginator.num_pages,
+                        "count": self.page.paginator.count,
+                    }
+                },
+                "links": {
+                    "first": self.build_link(1),
+                    "last": self.build_link(self.page.paginator.num_pages),
+                    "next": self.build_link(next),
+                    "prev": self.build_link(previous),
+                },
+            }
+        )
 
 
 class JsonApiLimitOffsetPagination(LimitOffsetPagination):
@@ -59,8 +61,9 @@ class JsonApiLimitOffsetPagination(LimitOffsetPagination):
         http://api.example.org/accounts/?page[offset]=400&page[limit]=100
 
     """
-    limit_query_param = 'page[limit]'
-    offset_query_param = 'page[offset]'
+
+    limit_query_param = "page[limit]"
+    offset_query_param = "page[offset]"
     max_limit = 100
 
     def get_last_link(self):
@@ -85,19 +88,21 @@ def get_first_link(self):
         return remove_query_param(url, self.offset_query_param)
 
     def get_paginated_response(self, data):
-        return Response({
-            'results': data,
-            'meta': {
-                'pagination': OrderedDict([
-                    ('count', self.count),
-                    ('limit', self.limit),
-                    ('offset', self.offset),
-                ])
-            },
-            'links': OrderedDict([
-                ('first', self.get_first_link()),
-                ('last', self.get_last_link()),
-                ('next', self.get_next_link()),
-                ('prev', self.get_previous_link())
-            ])
-        })
+        return Response(
+            {
+                "results": data,
+                "meta": {
+                    "pagination": {
+                        "count": self.count,
+                        "limit": self.limit,
+                        "offset": self.offset,
+                    }
+                },
+                "links": {
+                    "first": self.get_first_link(),
+                    "last": self.get_last_link(),
+                    "next": self.get_next_link(),
+                    "prev": self.get_previous_link(),
+                },
+            }
+        )
diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py
index 88c4f522..8940c653 100644
--- a/rest_framework_json_api/parsers.py
+++ b/rest_framework_json_api/parsers.py
@@ -1,11 +1,12 @@
 """
 Parsers
 """
+
 from rest_framework import parsers
 from rest_framework.exceptions import ParseError
 
-from . import exceptions, renderers, serializers, utils
-from .settings import json_api_settings
+from rest_framework_json_api import exceptions, renderers
+from rest_framework_json_api.utils import get_resource_name, undo_format_field_names
 
 
 class JSONParser(parsers.JSONParser):
@@ -13,7 +14,7 @@ class JSONParser(parsers.JSONParser):
     Similar to `JSONRenderer`, the `JSONParser` you may override the following methods if you
     need highly custom parsing control.
 
-    A JSON API client will send a payload that looks like this:
+    A JSON:API client will send a payload that looks like this:
 
     .. code:: json
 
@@ -31,41 +32,30 @@ class JSONParser(parsers.JSONParser):
 
     We extract the attributes so that DRF serializers can work as normal.
     """
-    media_type = 'application/vnd.api+json'
+
+    media_type = "application/vnd.api+json"
     renderer_class = renderers.JSONRenderer
 
     @staticmethod
     def parse_attributes(data):
-        attributes = data.get('attributes')
-        uses_format_translation = json_api_settings.FORMAT_FIELD_NAMES
-
-        if not attributes:
-            return dict()
-        elif uses_format_translation:
-            # convert back to python/rest_framework's preferred underscore format
-            return utils.format_field_names(attributes, 'underscore')
-        else:
-            return attributes
+        attributes = data.get("attributes") or dict()
+        return undo_format_field_names(attributes)
 
     @staticmethod
     def parse_relationships(data):
-        uses_format_translation = json_api_settings.FORMAT_FIELD_NAMES
-        relationships = data.get('relationships')
-
-        if not relationships:
-            relationships = dict()
-        elif uses_format_translation:
-            # convert back to python/rest_framework's preferred underscore format
-            relationships = utils.format_field_names(relationships, 'underscore')
+        relationships = data.get("relationships") or dict()
+        relationships = undo_format_field_names(relationships)
 
         # Parse the relationships
         parsed_relationships = dict()
         for field_name, field_data in relationships.items():
-            field_data = field_data.get('data')
+            field_data = field_data.get("data")
             if isinstance(field_data, dict) or field_data is None:
                 parsed_relationships[field_name] = field_data
             elif isinstance(field_data, list):
-                parsed_relationships[field_name] = list(relation for relation in field_data)
+                parsed_relationships[field_name] = list(
+                    relation for relation in field_data
+                )
         return parsed_relationships
 
     @staticmethod
@@ -75,94 +65,112 @@ def parse_metadata(result):
         it reads the `meta` content in the request body and returns it in a dictionary with
         a `_meta` top level key.
         """
-        metadata = result.get('meta')
+        metadata = result.get("meta")
         if metadata:
-            return {'_meta': metadata}
+            return {"_meta": metadata}
         else:
             return {}
 
-    def parse(self, stream, media_type=None, parser_context=None):
+    def parse_data(self, result, parser_context):
         """
-        Parses the incoming bytestream as JSON and returns the resulting data
+        Formats the output of calling JSONParser to match the JSON:API specification
+        and returns the result.
         """
-        result = super(JSONParser, self).parse(
-            stream, media_type=media_type, parser_context=parser_context
-        )
+        if not isinstance(result, dict) or "data" not in result:
+            raise ParseError("Received document does not contain primary data")
 
-        if not isinstance(result, dict) or 'data' not in result:
-            raise ParseError('Received document does not contain primary data')
-
-        data = result.get('data')
-        view = parser_context['view']
+        data = result.get("data")
+        parser_context = parser_context or {}
+        view = parser_context.get("view")
 
         from rest_framework_json_api.views import RelationshipView
+
         if isinstance(view, RelationshipView):
-            # We skip parsing the object as JSONAPI Resource Identifier Object and not a regular
-            # Resource Object
+            # We skip parsing the object as JSON:API Resource Identifier Object and not
+            # a regular Resource Object
             if isinstance(data, list):
                 for resource_identifier_object in data:
                     if not (
-                        resource_identifier_object.get('id') and
-                        resource_identifier_object.get('type')
+                        resource_identifier_object.get("id")
+                        and resource_identifier_object.get("type")
                     ):
                         raise ParseError(
-                            'Received data contains one or more malformed JSONAPI '
-                            'Resource Identifier Object(s)'
+                            "Received data contains one or more malformed JSON:API "
+                            "Resource Identifier Object(s)"
                         )
-            elif not (data.get('id') and data.get('type')):
-                raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object')
+            elif isinstance(data, dict) and not (data.get("id") and data.get("type")):
+                raise ParseError(
+                    "Received data is not a valid JSON:API Resource Identifier Object"
+                )
 
             return data
 
-        request = parser_context.get('request')
+        request = parser_context.get("request")
+        method = request and request.method
 
         # Sanity check
         if not isinstance(data, dict):
-            raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object')
+            raise ParseError(
+                "Received data is not a valid JSON:API Resource Identifier Object"
+            )
 
         # Check for inconsistencies
-        if request.method in ('PUT', 'POST', 'PATCH'):
-            resource_name = utils.get_resource_name(
-                parser_context, expand_polymorphic_types=True)
+        if method in ("PUT", "POST", "PATCH"):
+            resource_name = get_resource_name(
+                parser_context, expand_polymorphic_types=True
+            )
             if isinstance(resource_name, str):
-                if data.get('type') != resource_name:
+                if data.get("type") != resource_name:
                     raise exceptions.Conflict(
                         "The resource object's type ({data_type}) is not the type that "
                         "constitute the collection represented by the endpoint "
                         "({resource_type}).".format(
-                            data_type=data.get('type'),
-                            resource_type=resource_name))
+                            data_type=data.get("type"), resource_type=resource_name
+                        )
+                    )
             else:
-                if data.get('type') not in resource_name:
+                if data.get("type") not in resource_name:
                     raise exceptions.Conflict(
                         "The resource object's type ({data_type}) is not the type that "
                         "constitute the collection represented by the endpoint "
                         "(one of [{resource_types}]).".format(
-                            data_type=data.get('type'),
-                            resource_types=", ".join(resource_name)))
-        if not data.get('id') and request.method in ('PATCH', 'PUT'):
-            raise ParseError("The resource identifier object must contain an 'id' member")
-
-        if request.method in ('PATCH', 'PUT'):
-            lookup_url_kwarg = getattr(view, 'lookup_url_kwarg', None) or \
-                getattr(view, 'lookup_field', None)
-            if lookup_url_kwarg and str(data.get('id')) != str(view.kwargs[lookup_url_kwarg]):
+                            data_type=data.get("type"),
+                            resource_types=", ".join(resource_name),
+                        )
+                    )
+        if not data.get("id") and method in ("PATCH", "PUT"):
+            raise ParseError(
+                "The resource identifier object must contain an 'id' member"
+            )
+
+        if method in ("PATCH", "PUT"):
+            lookup_url_kwarg = getattr(view, "lookup_url_kwarg", None) or getattr(
+                view, "lookup_field", None
+            )
+            if lookup_url_kwarg and str(data.get("id")) != str(
+                view.kwargs[lookup_url_kwarg]
+            ):
                 raise exceptions.Conflict(
                     "The resource object's id ({data_id}) does not match url's "
                     "lookup id ({url_id})".format(
-                        data_id=data.get('id'),
-                        url_id=view.kwargs[lookup_url_kwarg]
+                        data_id=data.get("id"), url_id=view.kwargs[lookup_url_kwarg]
                     )
                 )
 
         # Construct the return data
-        serializer_class = getattr(view, 'serializer_class', None)
-        parsed_data = {'id': data.get('id')} if 'id' in data else {}
-        # `type` field needs to be allowed in none polymorphic serializers
-        if serializer_class is not None:
-            if issubclass(serializer_class, serializers.PolymorphicModelSerializer):
-                parsed_data['type'] = data.get('type')
+        parsed_data = {"id": data.get("id")} if "id" in data else {}
+        parsed_data["type"] = data.get("type")
         parsed_data.update(self.parse_attributes(data))
         parsed_data.update(self.parse_relationships(data))
         parsed_data.update(self.parse_metadata(result))
         return parsed_data
+
+    def parse(self, stream, media_type=None, parser_context=None):
+        """
+        Parses the incoming bytestream as JSON and returns the resulting data
+        """
+        result = super().parse(
+            stream, media_type=media_type, parser_context=parser_context
+        )
+
+        return self.parse_data(result, parser_context)
diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py
index 95df1d48..0742c1ec 100644
--- a/rest_framework_json_api/relations.py
+++ b/rest_framework_json_api/relations.py
@@ -1,5 +1,4 @@
 import json
-from collections import OrderedDict
 
 import inflection
 from django.core.exceptions import ImproperlyConfigured
@@ -15,28 +14,28 @@
 from rest_framework_json_api.exceptions import Conflict
 from rest_framework_json_api.utils import (
     Hyperlink,
-    get_included_serializers,
+    format_link_segment,
     get_resource_type_from_instance,
     get_resource_type_from_queryset,
-    get_resource_type_from_serializer
+    get_resource_type_from_serializer,
 )
 
 LINKS_PARAMS = [
-    'self_link_view_name',
-    'related_link_view_name',
-    'related_link_lookup_field',
-    'related_link_url_kwarg'
+    "self_link_view_name",
+    "related_link_view_name",
+    "related_link_lookup_field",
+    "related_link_url_kwarg",
 ]
 
 
-class SkipDataMixin(object):
+class SkipDataMixin:
     """
     This workaround skips "data" rendering for relationships
     in order to save some sql queries and improve performance
     """
 
     def __init__(self, *args, **kwargs):
-        super(SkipDataMixin, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
 
     def get_attribute(self, instance):
         raise SkipField
@@ -49,10 +48,10 @@ class ManyRelatedFieldWithNoData(SkipDataMixin, DRFManyRelatedField):
     pass
 
 
-class HyperlinkedMixin(object):
+class HyperlinkedMixin:
     self_link_view_name = None
     related_link_view_name = None
-    related_link_lookup_field = 'pk'
+    related_link_lookup_field = "pk"
 
     def __init__(self, self_link_view_name=None, related_link_view_name=None, **kwargs):
         if self_link_view_name is not None:
@@ -61,10 +60,10 @@ def __init__(self, self_link_view_name=None, related_link_view_name=None, **kwar
             self.related_link_view_name = related_link_view_name
 
         self.related_link_lookup_field = kwargs.pop(
-            'related_link_lookup_field', self.related_link_lookup_field
+            "related_link_lookup_field", self.related_link_lookup_field
         )
         self.related_link_url_kwarg = kwargs.pop(
-            'related_link_url_kwarg', self.related_link_lookup_field
+            "related_link_url_kwarg", self.related_link_lookup_field
         )
 
         # We include this simply for dependency injection in tests.
@@ -72,7 +71,7 @@ def __init__(self, self_link_view_name=None, related_link_view_name=None, **kwar
         # implicit `self` argument to be passed.
         self.reverse = reverse
 
-        super(HyperlinkedMixin, self).__init__(**kwargs)
+        super().__init__(**kwargs)
 
     def get_url(self, name, view_name, kwargs, request):
         """
@@ -91,7 +90,7 @@ def get_url(self, name, view_name, kwargs, request):
             url = self.reverse(view_name, kwargs=kwargs, request=request)
         except NoReverseMatch:
             msg = (
-                'Could not resolve URL for hyperlinked relationship using '
+                "Could not resolve URL for hyperlinked relationship using "
                 'view name "%s".'
             )
             raise ImproperlyConfigured(msg % view_name)
@@ -101,41 +100,48 @@ def get_url(self, name, view_name, kwargs, request):
 
         return Hyperlink(url, name)
 
-    def get_links(self, obj=None, lookup_field='pk'):
-        request = self.context.get('request', None)
-        view = self.context.get('view', None)
-        return_data = OrderedDict()
+    def get_links(self, obj=None, lookup_field="pk"):
+        request = self.context.get("request", None)
+        view = self.context.get("view", None)
+        return_data = {}
+
+        kwargs = {
+            lookup_field: (
+                getattr(obj, lookup_field) if obj else view.kwargs[lookup_field]
+            )
+        }
 
-        kwargs = {lookup_field: getattr(obj, lookup_field) if obj else view.kwargs[lookup_field]}
+        field_name = self.field_name if self.field_name else self.parent.field_name
 
         self_kwargs = kwargs.copy()
-        self_kwargs.update({
-            'related_field': self.field_name if self.field_name else self.parent.field_name
-        })
-        self_link = self.get_url('self', self.self_link_view_name, self_kwargs, request)
+        self_kwargs.update({"related_field": format_link_segment(field_name)})
+        self_link = self.get_url("self", self.self_link_view_name, self_kwargs, request)
 
         # Assuming RelatedField will be declared in two ways:
         # 1. url(r'^authors/(?P<pk>[^/.]+)/(?P<related_field>\w+)/$',
         #         AuthorViewSet.as_view({'get': 'retrieve_related'}))
         # 2. url(r'^authors/(?P<author_pk>[^/.]+)/bio/$',
         #         AuthorBioViewSet.as_view({'get': 'retrieve'}))
-        # So, if related_link_url_kwarg == 'pk' it will add 'related_field' parameter to reverse()
-        if self.related_link_url_kwarg == 'pk':
+        # So, if related_link_url_kwarg == 'pk' it adds 'related_field' parameter to reverse()
+        if self.related_link_url_kwarg == "pk":
             related_kwargs = self_kwargs
         else:
-            related_kwargs = {self.related_link_url_kwarg: kwargs[self.related_link_lookup_field]}
+            related_kwargs = {
+                self.related_link_url_kwarg: kwargs[self.related_link_lookup_field]
+            }
 
-        related_link = self.get_url('related', self.related_link_view_name, related_kwargs, request)
+        related_link = self.get_url(
+            "related", self.related_link_view_name, related_kwargs, request
+        )
 
         if self_link:
-            return_data.update({'self': self_link})
+            return_data.update({"self": self_link})
         if related_link:
-            return_data.update({'related': related_link})
+            return_data.update({"related": related_link})
         return return_data
 
 
 class HyperlinkedRelatedField(HyperlinkedMixin, SkipDataMixin, RelatedField):
-
     @classmethod
     def many_init(cls, *args, **kwargs):
         """
@@ -155,7 +161,7 @@ def many_init(cls, *args, **kwargs):
                 kwargs['child'] = cls()
                 return CustomManyRelatedField(*args, **kwargs)
         """
-        list_kwargs = {'child_relation': cls(*args, **kwargs)}
+        list_kwargs = {"child_relation": cls(*args, **kwargs)}
         for key in kwargs:
             if key in MANY_RELATION_KWARGS:
                 list_kwargs[key] = kwargs[key]
@@ -166,29 +172,31 @@ class ResourceRelatedField(HyperlinkedMixin, PrimaryKeyRelatedField):
     _skip_polymorphic_optimization = True
     self_link_view_name = None
     related_link_view_name = None
-    related_link_lookup_field = 'pk'
+    related_link_lookup_field = "pk"
 
     default_error_messages = {
-        'required': _('This field is required.'),
-        'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
-        'incorrect_type': _(
-            'Incorrect type. Expected resource identifier object, received {data_type}.'
+        "required": _("This field is required."),
+        "does_not_exist": _('Invalid pk "{pk_value}" - object does not exist.'),
+        "incorrect_type": _(
+            "Incorrect type. Expected resource identifier object, received {data_type}."
+        ),
+        "incorrect_relation_type": _(
+            "Incorrect relation type. Expected {relation_type}, received {received_type}."
         ),
-        'incorrect_relation_type': _(
-            'Incorrect relation type. Expected {relation_type}, received {received_type}.'
+        "missing_type": _(
+            "Invalid resource identifier object: missing 'type' attribute"
         ),
-        'missing_type': _('Invalid resource identifier object: missing \'type\' attribute'),
-        'missing_id': _('Invalid resource identifier object: missing \'id\' attribute'),
-        'no_match': _('Invalid hyperlink - No URL match.'),
+        "missing_id": _("Invalid resource identifier object: missing 'id' attribute"),
+        "no_match": _("Invalid hyperlink - No URL match."),
     }
 
     def __init__(self, **kwargs):
         # check for a model class that was passed in for the relation type
-        model = kwargs.pop('model', None)
+        model = kwargs.pop("model", None)
         if model:
             self.model = model
 
-        super(ResourceRelatedField, self).__init__(**kwargs)
+        super().__init__(**kwargs)
 
     def use_pk_only_optimization(self):
         # We need the real object to determine its type...
@@ -213,9 +221,9 @@ def to_internal_value(self, data):
                 data = json.loads(data)
             except ValueError:
                 # show a useful error if they send a `pk` instead of resource object
-                self.fail('incorrect_type', data_type=type(data).__name__)
+                self.fail("incorrect_type", data_type=type(data).__name__)
         if not isinstance(data, dict):
-            self.fail('incorrect_type', data_type=type(data).__name__)
+            self.fail("incorrect_type", data_type=type(data).__name__)
 
         expected_relation_type = get_resource_type_from_queryset(self.get_queryset())
         serializer_resource_type = self.get_resource_type_from_included_serializer()
@@ -223,32 +231,36 @@ def to_internal_value(self, data):
         if serializer_resource_type is not None:
             expected_relation_type = serializer_resource_type
 
-        if 'type' not in data:
-            self.fail('missing_type')
+        if "type" not in data:
+            self.fail("missing_type")
 
-        if 'id' not in data:
-            self.fail('missing_id')
+        if "id" not in data:
+            self.fail("missing_id")
 
-        if data['type'] != expected_relation_type:
+        if data["type"] != expected_relation_type:
             self.conflict(
-                'incorrect_relation_type',
+                "incorrect_relation_type",
                 relation_type=expected_relation_type,
-                received_type=data['type']
+                received_type=data["type"],
             )
 
-        return super(ResourceRelatedField, self).to_internal_value(data['id'])
+        return super().to_internal_value(data["id"])
 
     def to_representation(self, value):
-        if getattr(self, 'pk_field', None) is not None:
-            pk = self.pk_field.to_representation(value.pk)
-        else:
-            pk = value.pk
-
+        pk = self.get_resource_id(value)
         resource_type = self.get_resource_type_from_included_serializer()
         if resource_type is None or not self._skip_polymorphic_optimization:
             resource_type = get_resource_type_from_instance(value)
 
-        return OrderedDict([('type', resource_type), ('id', str(pk))])
+        return {"type": resource_type, "id": str(pk)}
+
+    def get_resource_id(self, value):
+        """
+        Get resource id of related field.
+
+        Per default pk of value is returned.
+        """
+        return super().to_representation(value)
 
     def get_resource_type_from_included_serializer(self):
         """
@@ -262,9 +274,9 @@ def get_resource_type_from_included_serializer(self):
             # accept both singular and plural versions of field_name
             field_names = [
                 inflection.singularize(field_name),
-                inflection.pluralize(field_name)
+                inflection.pluralize(field_name),
             ]
-            includes = get_included_serializers(parent)
+            includes = getattr(parent, "included_serializers", dict())
             for field in field_names:
                 if field in includes.keys():
                     return get_resource_type_from_serializer(includes[field])
@@ -272,7 +284,7 @@ def get_resource_type_from_included_serializer(self):
         return None
 
     def get_parent_serializer(self):
-        if hasattr(self.parent, 'parent') and self.is_serializer(self.parent.parent):
+        if hasattr(self.parent, "parent") and self.is_serializer(self.parent.parent):
             return self.parent.parent
         elif self.is_serializer(self.parent):
             return self.parent
@@ -292,13 +304,10 @@ def get_choices(self, cutoff=None):
         if cutoff is not None:
             queryset = queryset[:cutoff]
 
-        return OrderedDict([
-            (
-                json.dumps(self.to_representation(item)),
-                self.display_value(item)
-            )
+        return {
+            json.dumps(self.to_representation(item)): self.display_value(item)
             for item in queryset
-        ])
+        }
 
 
 class PolymorphicResourceRelatedField(ResourceRelatedField):
@@ -309,14 +318,19 @@ class PolymorphicResourceRelatedField(ResourceRelatedField):
     """
 
     _skip_polymorphic_optimization = False
-    default_error_messages = dict(ResourceRelatedField.default_error_messages, **{
-        'incorrect_relation_type': _('Incorrect relation type. Expected one of [{relation_type}], '
-                                     'received {received_type}.'),
-    })
+    default_error_messages = dict(
+        ResourceRelatedField.default_error_messages,
+        **{
+            "incorrect_relation_type": _(
+                "Incorrect relation type. Expected one of [{relation_type}], "
+                "received {received_type}."
+            ),
+        },
+    )
 
     def __init__(self, polymorphic_serializer, *args, **kwargs):
         self.polymorphic_serializer = polymorphic_serializer
-        super(PolymorphicResourceRelatedField, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
 
     def use_pk_only_optimization(self):
         return False
@@ -327,34 +341,37 @@ def to_internal_value(self, data):
                 data = json.loads(data)
             except ValueError:
                 # show a useful error if they send a `pk` instead of resource object
-                self.fail('incorrect_type', data_type=type(data).__name__)
+                self.fail("incorrect_type", data_type=type(data).__name__)
         if not isinstance(data, dict):
-            self.fail('incorrect_type', data_type=type(data).__name__)
+            self.fail("incorrect_type", data_type=type(data).__name__)
 
-        if 'type' not in data:
-            self.fail('missing_type')
+        if "type" not in data:
+            self.fail("missing_type")
 
-        if 'id' not in data:
-            self.fail('missing_id')
+        if "id" not in data:
+            self.fail("missing_id")
 
         expected_relation_types = self.polymorphic_serializer.get_polymorphic_types()
 
-        if data['type'] not in expected_relation_types:
-            self.conflict('incorrect_relation_type', relation_type=", ".join(
-                expected_relation_types), received_type=data['type'])
+        if data["type"] not in expected_relation_types:
+            self.conflict(
+                "incorrect_relation_type",
+                relation_type=", ".join(expected_relation_types),
+                received_type=data["type"],
+            )
 
-        return super(ResourceRelatedField, self).to_internal_value(data['id'])
+        return super(ResourceRelatedField, self).to_internal_value(data["id"])
 
 
 class SerializerMethodFieldBase(Field):
     def __init__(self, method_name=None, **kwargs):
         self.method_name = method_name
-        kwargs['source'] = '*'
-        kwargs['read_only'] = True
+        kwargs["source"] = "*"
+        kwargs["read_only"] = True
         super().__init__(**kwargs)
 
     def bind(self, field_name, parent):
-        default_method_name = 'get_{field_name}'.format(field_name=field_name)
+        default_method_name = f"get_{field_name}"
         if self.method_name is None:
             self.method_name = default_method_name
         super().bind(field_name, parent)
@@ -364,40 +381,46 @@ def get_attribute(self, instance):
         return serializer_method(instance)
 
 
-class ManySerializerMethodResourceRelatedField(SerializerMethodFieldBase, ResourceRelatedField):
+class ManySerializerMethodResourceRelatedField(
+    SerializerMethodFieldBase, ResourceRelatedField
+):
     def __init__(self, child_relation=None, *args, **kwargs):
-        assert child_relation is not None, '`child_relation` is a required argument.'
+        assert child_relation is not None, "`child_relation` is a required argument."
         self.child_relation = child_relation
         super().__init__(**kwargs)
-        self.child_relation.bind(field_name='', parent=self)
+        self.child_relation.bind(field_name="", parent=self)
 
     def to_representation(self, value):
         return [self.child_relation.to_representation(item) for item in value]
 
 
-class SerializerMethodResourceRelatedField(SerializerMethodFieldBase, ResourceRelatedField):
+class SerializerMethodResourceRelatedField(
+    SerializerMethodFieldBase, ResourceRelatedField
+):
     """
     Allows us to use serializer method RelatedFields
     with return querysets
     """
 
-    many_kwargs = [*MANY_RELATION_KWARGS, *LINKS_PARAMS, 'method_name', 'model']
+    many_kwargs = [*MANY_RELATION_KWARGS, *LINKS_PARAMS, "method_name", "model"]
     many_cls = ManySerializerMethodResourceRelatedField
 
     @classmethod
     def many_init(cls, *args, **kwargs):
-        list_kwargs = {'child_relation': cls(**kwargs)}
+        list_kwargs = {"child_relation": cls(**kwargs)}
         for key in kwargs:
             if key in cls.many_kwargs:
                 list_kwargs[key] = kwargs[key]
         return cls.many_cls(**list_kwargs)
 
 
-class ManySerializerMethodHyperlinkedRelatedField(SkipDataMixin,
-                                                  ManySerializerMethodResourceRelatedField):
+class ManySerializerMethodHyperlinkedRelatedField(
+    SkipDataMixin, ManySerializerMethodResourceRelatedField
+):
     pass
 
 
-class SerializerMethodHyperlinkedRelatedField(SkipDataMixin,
-                                              SerializerMethodResourceRelatedField):
+class SerializerMethodHyperlinkedRelatedField(
+    SkipDataMixin, SerializerMethodResourceRelatedField
+):
     many_cls = ManySerializerMethodHyperlinkedRelatedField
diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py
index b6f216be..b670338f 100644
--- a/rest_framework_json_api/renderers.py
+++ b/rest_framework_json_api/renderers.py
@@ -1,13 +1,14 @@
 """
 Renderers
 """
+
 import copy
-from collections import OrderedDict, defaultdict
+from collections import defaultdict
 from collections.abc import Iterable
 
-import inflection
 from django.db.models import Manager
-from django.utils import encoding
+from django.template import loader
+from django.utils.encoding import force_str
 from rest_framework import relations, renderers
 from rest_framework.fields import SkipField, get_attribute
 from rest_framework.relations import PKOnlyObject
@@ -15,8 +16,26 @@
 from rest_framework.settings import api_settings
 
 import rest_framework_json_api
-from rest_framework_json_api import utils
-from rest_framework_json_api.relations import HyperlinkedMixin, ResourceRelatedField, SkipDataMixin
+from rest_framework_json_api.relations import (
+    HyperlinkedMixin,
+    ManySerializerMethodResourceRelatedField,
+    ResourceRelatedField,
+    SkipDataMixin,
+)
+from rest_framework_json_api.utils import (
+    format_errors,
+    format_field_name,
+    format_field_names,
+    get_included_resources,
+    get_related_resource_type,
+    get_relation_instance,
+    get_resource_id,
+    get_resource_name,
+    get_resource_type_from_instance,
+    get_resource_type_from_serializer,
+    get_serializer_fields,
+    is_relationship_field,
+)
 
 
 class JSONRenderer(renderers.JSONRenderer):
@@ -24,7 +43,7 @@ class JSONRenderer(renderers.JSONRenderer):
     The `JSONRenderer` exposes a number of methods that you may override if you need highly
     custom rendering control.
 
-    Render a JSON response per the JSON API spec:
+    Render a JSON response per the JSON:API spec:
 
     .. code-block:: json
 
@@ -43,42 +62,25 @@ class JSONRenderer(renderers.JSONRenderer):
         }
     """
 
-    media_type = 'application/vnd.api+json'
-    format = 'vnd.api+json'
+    media_type = "application/vnd.api+json"
+    format = "vnd.api+json"
 
     @classmethod
     def extract_attributes(cls, fields, resource):
         """
-        Builds the `attributes` object of the JSON API resource object.
-        """
-        data = OrderedDict()
-        for field_name, field in iter(fields.items()):
-            # ID is always provided in the root of JSON API so remove it from attributes
-            if field_name == 'id':
-                continue
-            # don't output a key for write only fields
-            if fields[field_name].write_only:
-                continue
-            # Skip fields with relations
-            if isinstance(
-                    field, (relations.RelatedField, relations.ManyRelatedField)
-            ):
-                continue
-
-            # Skip read_only attribute fields when `resource` is an empty
-            # serializer. Prevents the "Raw Data" form of the browsable API
-            # from rendering `"foo": null` for read only fields
-            try:
-                resource[field_name]
-            except KeyError:
-                if fields[field_name].read_only:
-                    continue
+        Builds the `attributes` object of the JSON:API resource object.
 
-            data.update({
-                field_name: resource.get(field_name)
-            })
+        Ensures that ID which is always provided in a JSON:API resource object
+        and relationships are not returned.
+        """
 
-        return utils.format_field_names(data)
+        return {
+            format_field_name(field_name): value
+            for field_name, value in resource.items()
+            if field_name in fields
+            and field_name != "id"
+            and not is_relationship_field(fields[field_name])
+        }
 
     @classmethod
     def extract_relationships(cls, fields, resource, resource_instance):
@@ -88,7 +90,7 @@ def extract_relationships(cls, fields, resource, resource_instance):
         # Avoid circular deps
         from rest_framework_json_api.relations import ResourceRelatedField
 
-        data = OrderedDict()
+        data = {}
 
         # Don't try to extract relationships from a non-existent resource
         if resource_instance is None:
@@ -104,16 +106,14 @@ def extract_relationships(cls, fields, resource, resource_instance):
                 continue
 
             # Skip fields without relations
-            if not isinstance(
-                field, (relations.RelatedField, relations.ManyRelatedField)
-            ):
+            if not is_relationship_field(field):
                 continue
 
             source = field.source
-            relation_type = utils.get_related_resource_type(field)
+            relation_type = get_related_resource_type(field)
 
             if isinstance(field, relations.HyperlinkedIdentityField):
-                resolved, relation_instance = utils.get_relation_instance(
+                resolved, relation_instance = get_relation_instance(
                     resource_instance, source, field.parent
                 )
                 if not resolved:
@@ -122,73 +122,73 @@ def extract_relationships(cls, fields, resource, resource_instance):
                 relation_data = list()
 
                 # Don't try to query an empty relation
-                relation_queryset = relation_instance \
-                    if relation_instance is not None else list()
-
-                for related_object in relation_queryset:
-                    relation_data.append(
-                        OrderedDict([
-                            ('type', relation_type),
-                            ('id', encoding.force_str(related_object.pk))
-                        ])
-                    )
+                relation_queryset = (
+                    relation_instance if relation_instance is not None else list()
+                )
 
-                data.update({field_name: {
-                    'links': {
-                        "related": resource.get(field_name)},
-                    'data': relation_data,
-                    'meta': {
-                        'count': len(relation_data)
+                relation_data = [
+                    {"type": relation_type, "id": force_str(related_object.pk)}
+                    for related_object in relation_queryset
+                ]
+                data.update(
+                    {
+                        field_name: {
+                            "links": {"related": resource.get(field_name)},
+                            "data": relation_data,
+                            "meta": {"count": len(relation_data)},
+                        }
                     }
-                }})
+                )
                 continue
 
             relation_data = {}
             if isinstance(field, HyperlinkedMixin):
-                field_links = field.get_links(resource_instance, field.related_link_lookup_field)
-                relation_data.update({'links': field_links} if field_links else dict())
+                field_links = field.get_links(
+                    resource_instance, field.related_link_lookup_field
+                )
+                relation_data.update({"links": field_links} if field_links else dict())
                 data.update({field_name: relation_data})
 
-            if isinstance(field, (ResourceRelatedField, )):
+            if isinstance(field, (ResourceRelatedField,)):
                 if not isinstance(field, SkipDataMixin):
-                    relation_data.update({'data': resource.get(field_name)})
+                    relation_data.update({"data": resource.get(field_name)})
+
+                    if isinstance(field, ManySerializerMethodResourceRelatedField):
+                        relation_data.update(
+                            {"meta": {"count": len(resource.get(field_name))}}
+                        )
 
                 data.update({field_name: relation_data})
                 continue
 
             if isinstance(
-                    field, (relations.PrimaryKeyRelatedField, relations.HyperlinkedRelatedField)
+                field,
+                (relations.PrimaryKeyRelatedField, relations.HyperlinkedRelatedField),
             ):
-                resolved, relation = utils.get_relation_instance(
-                    resource_instance, '%s_id' % source, field.parent
+                resolved, relation = get_relation_instance(
+                    resource_instance, f"{source}_id", field.parent
                 )
                 if not resolved:
                     continue
                 relation_id = relation if resource.get(field_name) else None
-                relation_data = {
-                    'data': (
-                        OrderedDict([
-                            ('type', relation_type), ('id', encoding.force_str(relation_id))
-                        ])
-                        if relation_id is not None else None)
-                }
+                relation_data = {"data": None}
+                if relation_id is not None:
+                    relation_data["data"] = {
+                        "type": relation_type,
+                        "id": force_str(relation_id),
+                    }
 
-                if (
-                    isinstance(field, relations.HyperlinkedRelatedField) and
-                    resource.get(field_name)
-                ):
+                if isinstance(
+                    field, relations.HyperlinkedRelatedField
+                ) and resource.get(field_name):
                     relation_data.update(
-                        {
-                            'links': {
-                                'related': resource.get(field_name)
-                            }
-                        }
+                        {"links": {"related": resource.get(field_name)}}
                     )
                 data.update({field_name: relation_data})
                 continue
 
             if isinstance(field, relations.ManyRelatedField):
-                resolved, relation_instance = utils.get_relation_instance(
+                resolved, relation_instance = get_relation_instance(
                     resource_instance, source, field.parent
                 )
                 if not resolved:
@@ -198,25 +198,20 @@ def extract_relationships(cls, fields, resource, resource_instance):
 
                 if isinstance(resource.get(field_name), Iterable):
                     relation_data.update(
-                        {
-                            'meta': {'count': len(resource.get(field_name))}
-                        }
+                        {"meta": {"count": len(resource.get(field_name))}}
                     )
 
                 if isinstance(field.child_relation, ResourceRelatedField):
                     # special case for ResourceRelatedField
-                    relation_data.update(
-                        {'data': resource.get(field_name)}
-                    )
+                    relation_data.update({"data": resource.get(field_name)})
 
                 if isinstance(field.child_relation, HyperlinkedMixin):
                     field_links = field.child_relation.get_links(
                         resource_instance,
-                        field.child_relation.related_link_lookup_field
+                        field.child_relation.related_link_lookup_field,
                     )
                     relation_data.update(
-                        {'links': field_links}
-                        if field_links else dict()
+                        {"links": field_links} if field_links else dict()
                     )
 
                     data.update({field_name: relation_data})
@@ -225,25 +220,27 @@ def extract_relationships(cls, fields, resource, resource_instance):
                 relation_data = list()
                 for nested_resource_instance in relation_instance:
                     nested_resource_instance_type = (
-                        relation_type or
-                        utils.get_resource_type_from_instance(nested_resource_instance)
+                        relation_type
+                        or get_resource_type_from_instance(nested_resource_instance)
                     )
 
-                    relation_data.append(OrderedDict([
-                        ('type', nested_resource_instance_type),
-                        ('id', encoding.force_str(nested_resource_instance.pk))
-                    ]))
-                data.update({
-                    field_name: {
-                        'data': relation_data,
-                        'meta': {
-                            'count': len(relation_data)
+                    relation_data.append(
+                        {
+                            "type": nested_resource_instance_type,
+                            "id": force_str(nested_resource_instance.pk),
+                        }
+                    )
+                data.update(
+                    {
+                        field_name: {
+                            "data": relation_data,
+                            "meta": {"count": len(relation_data)},
                         }
                     }
-                })
+                )
                 continue
 
-        return utils.format_field_names(data)
+        return format_field_names(data)
 
     @classmethod
     def extract_relation_instance(cls, field, resource_instance):
@@ -262,8 +259,9 @@ def extract_relation_instance(cls, field, resource_instance):
             return None
 
     @classmethod
-    def extract_included(cls, fields, resource, resource_instance, included_resources,
-                         included_cache):
+    def extract_included(
+        cls, fields, resource, resource_instance, included_resources, included_cache
+    ):
         """
         Adds related data to the top level included key when the request includes
         ?include=example,example_field2
@@ -274,9 +272,10 @@ def extract_included(cls, fields, resource, resource_instance, included_resource
 
         current_serializer = fields.serializer
         context = current_serializer.context
-        included_serializers = utils.get_included_serializers(current_serializer)
+        included_serializers = getattr(
+            current_serializer, "included_serializers", dict()
+        )
         included_resources = copy.copy(included_resources)
-        included_resources = [inflection.underscore(value) for value in included_resources]
 
         for field_name, field in iter(fields.items()):
             # Skip URL field
@@ -284,9 +283,7 @@ def extract_included(cls, fields, resource, resource_instance, included_resource
                 continue
 
             # Skip fields without relations
-            if not isinstance(
-                field, (relations.RelatedField, relations.ManyRelatedField)
-            ):
+            if not is_relationship_field(field):
                 continue
 
             try:
@@ -294,12 +291,12 @@ def extract_included(cls, fields, resource, resource_instance, included_resource
             except ValueError:
                 # Skip fields not in requested included resources
                 # If no child field, directly continue with the next field
-                if field_name not in [node.split('.')[0] for node in included_resources]:
+                if field_name not in [
+                    node.split(".")[0] for node in included_resources
+                ]:
                     continue
 
-            relation_instance = cls.extract_relation_instance(
-                field, resource_instance
-            )
+            relation_instance = cls.extract_relation_instance(field, resource_instance)
             if isinstance(relation_instance, Manager):
                 relation_instance = relation_instance.all()
 
@@ -314,11 +311,14 @@ def extract_included(cls, fields, resource, resource_instance, included_resource
                 if relation_instance is None or not serializer_data:
                     continue
 
-                many = field._kwargs.get('child_relation', None) is not None
+                many = field._kwargs.get("child_relation", None) is not None
 
                 if isinstance(field, ResourceRelatedField) and not many:
-                    already_included = serializer_data['type'] in included_cache and \
-                        serializer_data['id'] in included_cache[serializer_data['type']]
+                    already_included = (
+                        serializer_data["type"] in included_cache
+                        and serializer_data["id"]
+                        in included_cache[serializer_data["type"]]
+                    )
 
                     if already_included:
                         continue
@@ -327,13 +327,15 @@ def extract_included(cls, fields, resource, resource_instance, included_resource
                 field = serializer_class(relation_instance, many=many, context=context)
                 serializer_data = field.data
 
-            new_included_resources = [key.replace('%s.' % field_name, '', 1)
-                                      for key in included_resources
-                                      if field_name == key.split('.')[0]]
+            new_included_resources = [
+                key.replace(f"{field_name}.", "", 1)
+                for key in included_resources
+                if field_name == key.split(".")[0]
+            ]
 
             if isinstance(field, ListSerializer):
                 serializer = field.child
-                relation_type = utils.get_resource_type_from_serializer(serializer)
+                relation_type = get_resource_type_from_serializer(serializer)
                 relation_queryset = list(relation_instance)
 
                 if serializer_data:
@@ -341,10 +343,10 @@ def extract_included(cls, fields, resource, resource_instance, included_resource
                         serializer_resource = serializer_data[position]
                         nested_resource_instance = relation_queryset[position]
                         resource_type = (
-                            relation_type or
-                            utils.get_resource_type_from_instance(nested_resource_instance)
+                            relation_type
+                            or get_resource_type_from_instance(nested_resource_instance)
                         )
-                        serializer_fields = utils.get_serializer_fields(
+                        serializer_fields = get_serializer_fields(
                             serializer.__class__(
                                 nested_resource_instance, context=serializer.context
                             )
@@ -354,10 +356,11 @@ def extract_included(cls, fields, resource, resource_instance, included_resource
                             serializer_resource,
                             nested_resource_instance,
                             resource_type,
-                            getattr(serializer, '_poly_force_type_resolution', False)
+                            serializer,
+                            getattr(serializer, "_poly_force_type_resolution", False),
                         )
-                        included_cache[new_item['type']][new_item['id']] = \
-                            utils.format_field_names(new_item)
+                        included_cache[new_item["type"]][new_item["id"]] = new_item
+
                         cls.extract_included(
                             serializer_fields,
                             serializer_resource,
@@ -367,21 +370,21 @@ def extract_included(cls, fields, resource, resource_instance, included_resource
                         )
 
             if isinstance(field, Serializer):
-                relation_type = utils.get_resource_type_from_serializer(field)
+                relation_type = get_resource_type_from_serializer(field)
 
                 # Get the serializer fields
-                serializer_fields = utils.get_serializer_fields(field)
+                serializer_fields = get_serializer_fields(field)
                 if serializer_data:
                     new_item = cls.build_json_resource_obj(
                         serializer_fields,
                         serializer_data,
                         relation_instance,
                         relation_type,
-                        getattr(field, '_poly_force_type_resolution', False)
-                    )
-                    included_cache[new_item['type']][new_item['id']] = utils.format_field_names(
-                        new_item
+                        field,
+                        getattr(field, "_poly_force_type_resolution", False),
                     )
+                    included_cache[new_item["type"]][new_item["id"]] = new_item
+
                     cls.extract_included(
                         serializer_fields,
                         serializer_data,
@@ -396,16 +399,15 @@ def extract_meta(cls, serializer, resource):
         Gathers the data from serializer fields specified in meta_fields and adds it to
         the meta object.
         """
-        if hasattr(serializer, 'child'):
-            meta = getattr(serializer.child, 'Meta', None)
+        if hasattr(serializer, "child"):
+            meta = getattr(serializer.child, "Meta", None)
         else:
-            meta = getattr(serializer, 'Meta', None)
-        meta_fields = getattr(meta, 'meta_fields', [])
-        data = OrderedDict()
+            meta = getattr(serializer, "Meta", None)
+        meta_fields = getattr(meta, "meta_fields", [])
+        data = {}
         for field_name in meta_fields:
-            data.update({
-                field_name: resource.get(field_name)
-            })
+            if field_name in resource:
+                data.update({field_name: resource[field_name]})
         return data
 
     @classmethod
@@ -414,170 +416,227 @@ def extract_root_meta(cls, serializer, resource):
         Calls a `get_root_meta` function on a serializer, if it exists.
         """
         many = False
-        if hasattr(serializer, 'child'):
+        if hasattr(serializer, "child"):
             many = True
             serializer = serializer.child
 
         data = {}
-        if getattr(serializer, 'get_root_meta', None):
+        if getattr(serializer, "get_root_meta", None):
             json_api_meta = serializer.get_root_meta(resource, many)
-            assert isinstance(json_api_meta, dict), 'get_root_meta must return a dict'
+            assert isinstance(json_api_meta, dict), "get_root_meta must return a dict"
             data.update(json_api_meta)
         return data
 
     @classmethod
-    def build_json_resource_obj(cls, fields, resource, resource_instance, resource_name,
-                                force_type_resolution=False):
+    def _filter_sparse_fields(cls, serializer, fields, resource_name):
+        request = serializer.context.get("request")
+        if request:
+            sparse_fieldset_query_param = f"fields[{resource_name}]"
+            sparse_fieldset_value = request.query_params.get(
+                sparse_fieldset_query_param
+            )
+            if sparse_fieldset_value is not None:
+                sparse_fields = sparse_fieldset_value.split(",")
+                return {
+                    field_name: field
+                    for field_name, field, in fields.items()
+                    if field.field_name in sparse_fields
+                    # URL field is not considered a field in JSON:API spec
+                    # but a link so need to keep it
+                    or (
+                        field.field_name == api_settings.URL_FIELD_NAME
+                        and isinstance(field, relations.HyperlinkedIdentityField)
+                    )
+                }
+
+        return fields
+
+    @classmethod
+    def build_json_resource_obj(
+        cls,
+        fields,
+        resource,
+        resource_instance,
+        resource_name,
+        serializer,
+        force_type_resolution=False,
+    ):
         """
         Builds the resource object (type, id, attributes) and extracts relationships.
         """
         # Determine type from the instance if the underlying model is polymorphic
         if force_type_resolution:
-            resource_name = utils.get_resource_type_from_instance(resource_instance)
-        resource_data = [
-            ('type', resource_name),
-            ('id', encoding.force_str(resource_instance.pk) if resource_instance else None),
-            ('attributes', cls.extract_attributes(fields, resource)),
-        ]
+            resource_name = get_resource_type_from_instance(resource_instance)
+        resource_data = {
+            "type": resource_name,
+            "id": get_resource_id(resource_instance, resource),
+        }
+
+        # TODO remove this filter by rewriting extract_relationships
+        # so it uses the serialized data as a basis
+        fields = cls._filter_sparse_fields(serializer, fields, resource_name)
+        attributes = cls.extract_attributes(fields, resource)
+        if attributes:
+            resource_data["attributes"] = attributes
         relationships = cls.extract_relationships(fields, resource, resource_instance)
         if relationships:
-            resource_data.append(('relationships', relationships))
+            resource_data["relationships"] = relationships
         # Add 'self' link if field is present and valid
-        if api_settings.URL_FIELD_NAME in resource and \
-                isinstance(fields[api_settings.URL_FIELD_NAME], relations.RelatedField):
-            resource_data.append(('links', {'self': resource[api_settings.URL_FIELD_NAME]}))
-        return OrderedDict(resource_data)
+        if api_settings.URL_FIELD_NAME in resource and isinstance(
+            fields[api_settings.URL_FIELD_NAME], relations.HyperlinkedIdentityField
+        ):
+            resource_data["links"] = {"self": resource[api_settings.URL_FIELD_NAME]}
+
+        meta = cls.extract_meta(serializer, resource)
+        if meta:
+            resource_data["meta"] = format_field_names(meta)
 
-    def render_relationship_view(self, data, accepted_media_type=None, renderer_context=None):
+        return resource_data
+
+    def render_relationship_view(
+        self, data, accepted_media_type=None, renderer_context=None
+    ):
         # Special case for RelationshipView
         view = renderer_context.get("view", None)
-        render_data = OrderedDict([
-            ('data', data)
-        ])
+        render_data = {"data": data}
         links = view.get_links()
         if links:
-            render_data.update({'links': links}),
-        return super(JSONRenderer, self).render(
-            render_data, accepted_media_type, renderer_context
-        )
+            render_data["links"] = links
+        return super().render(render_data, accepted_media_type, renderer_context)
 
     def render_errors(self, data, accepted_media_type=None, renderer_context=None):
-        return super(JSONRenderer, self).render(
-            utils.format_errors(data), accepted_media_type, renderer_context
+        return super().render(
+            format_errors(data), accepted_media_type, renderer_context
         )
 
     def render(self, data, accepted_media_type=None, renderer_context=None):
-
         renderer_context = renderer_context or {}
 
         view = renderer_context.get("view", None)
         request = renderer_context.get("request", None)
 
         # Get the resource name.
-        resource_name = utils.get_resource_name(renderer_context)
+        resource_name = get_resource_name(renderer_context)
 
         # If this is an error response, skip the rest.
-        if resource_name == 'errors':
+        if resource_name == "errors":
             return self.render_errors(data, accepted_media_type, renderer_context)
 
         # if response.status_code is 204 then the data to be rendered must
         # be None
-        response = renderer_context.get('response', None)
+        response = renderer_context.get("response", None)
         if response is not None and response.status_code == 204:
-            return super(JSONRenderer, self).render(
-                None, accepted_media_type, renderer_context
-            )
+            return super().render(None, accepted_media_type, renderer_context)
 
         from rest_framework_json_api.views import RelationshipView
+
         if isinstance(view, RelationshipView):
-            return self.render_relationship_view(data, accepted_media_type, renderer_context)
+            return self.render_relationship_view(
+                data, accepted_media_type, renderer_context
+            )
 
         # If `resource_name` is set to None then render default as the dev
         # wants to build the output format manually.
         if resource_name is None or resource_name is False:
-            return super(JSONRenderer, self).render(
-                data, accepted_media_type, renderer_context
-            )
+            return super().render(data, accepted_media_type, renderer_context)
 
         json_api_data = data
         # initialize json_api_meta with pagination meta or an empty dict
-        json_api_meta = data.get('meta', {}) if isinstance(data, dict) else {}
+        json_api_meta = data.get("meta", {}) if isinstance(data, dict) else {}
         included_cache = defaultdict(dict)
 
-        if data and 'results' in data:
+        if data and "results" in data:
             serializer_data = data["results"]
         else:
             serializer_data = data
 
-        serializer = getattr(serializer_data, 'serializer', None)
+        serializer = getattr(serializer_data, "serializer", None)
 
-        included_resources = utils.get_included_resources(request, serializer)
+        included_resources = get_included_resources(request, serializer)
 
         if serializer is not None:
-
             # Extract root meta for any type of serializer
             json_api_meta.update(self.extract_root_meta(serializer, serializer_data))
 
-            if getattr(serializer, 'many', False):
+            if getattr(serializer, "many", False):
                 json_api_data = list()
 
                 for position in range(len(serializer_data)):
                     resource = serializer_data[position]  # Get current resource
-                    resource_instance = serializer.instance[position]  # Get current instance
-
-                    if isinstance(serializer.child, rest_framework_json_api.
-                                  serializers.PolymorphicModelSerializer):
-                        resource_serializer_class = serializer.child.\
-                            get_polymorphic_serializer_for_instance(resource_instance)(
-                                context=serializer.child.context
-                            )
+                    resource_instance = serializer.instance[
+                        position
+                    ]  # Get current instance
+
+                    if isinstance(
+                        serializer.child,
+                        rest_framework_json_api.serializers.PolymorphicModelSerializer,
+                    ):
+                        resource_serializer_class = (
+                            serializer.child.get_polymorphic_serializer_for_instance(
+                                resource_instance
+                            )(context=serializer.child.context)
+                        )
                     else:
                         resource_serializer_class = serializer.child
 
-                    fields = utils.get_serializer_fields(resource_serializer_class)
+                    fields = get_serializer_fields(resource_serializer_class)
                     force_type_resolution = getattr(
-                        resource_serializer_class, '_poly_force_type_resolution', False)
+                        resource_serializer_class, "_poly_force_type_resolution", False
+                    )
 
                     json_resource_obj = self.build_json_resource_obj(
-                        fields, resource, resource_instance, resource_name, force_type_resolution
+                        fields,
+                        resource,
+                        resource_instance,
+                        resource_name,
+                        serializer,
+                        force_type_resolution,
                     )
-                    meta = self.extract_meta(serializer, resource)
-                    if meta:
-                        json_resource_obj.update({'meta': utils.format_field_names(meta)})
                     json_api_data.append(json_resource_obj)
 
                     self.extract_included(
-                        fields, resource, resource_instance, included_resources, included_cache
+                        fields,
+                        resource,
+                        resource_instance,
+                        included_resources,
+                        included_cache,
                     )
             else:
-                fields = utils.get_serializer_fields(serializer)
-                force_type_resolution = getattr(serializer, '_poly_force_type_resolution', False)
+                fields = get_serializer_fields(serializer)
+                force_type_resolution = getattr(
+                    serializer, "_poly_force_type_resolution", False
+                )
 
                 resource_instance = serializer.instance
                 json_api_data = self.build_json_resource_obj(
-                    fields, serializer_data, resource_instance, resource_name, force_type_resolution
+                    fields,
+                    serializer_data,
+                    resource_instance,
+                    resource_name,
+                    serializer,
+                    force_type_resolution,
                 )
 
-                meta = self.extract_meta(serializer, serializer_data)
-                if meta:
-                    json_api_data.update({'meta': utils.format_field_names(meta)})
-
                 self.extract_included(
-                    fields, serializer_data, resource_instance, included_resources, included_cache
+                    fields,
+                    serializer_data,
+                    resource_instance,
+                    included_resources,
+                    included_cache,
                 )
 
         # Make sure we render data in a specific order
-        render_data = OrderedDict()
+        render_data = {}
 
-        if isinstance(data, dict) and data.get('links'):
-            render_data['links'] = data.get('links')
+        if isinstance(data, dict) and data.get("links"):
+            render_data["links"] = data.get("links")
 
         # format the api root link list
-        if view.__class__ and view.__class__.__name__ == 'APIRoot':
-            render_data['data'] = None
-            render_data['links'] = json_api_data
+        if view.__class__ and view.__class__.__name__ == "APIRoot":
+            render_data["data"] = None
+            render_data["links"] = json_api_data
         else:
-            render_data['data'] = json_api_data
+            render_data["data"] = json_api_data
 
         if included_cache:
             if isinstance(json_api_data, list):
@@ -586,23 +645,75 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
                 objects = [json_api_data]
 
             for object in objects:
-                obj_type = object.get('type')
-                obj_id = object.get('id')
-                if obj_type in included_cache and \
-                   obj_id in included_cache[obj_type]:
+                obj_type = object.get("type")
+                obj_id = object.get("id")
+                if obj_type in included_cache and obj_id in included_cache[obj_type]:
                     del included_cache[obj_type][obj_id]
                 if not included_cache[obj_type]:
                     del included_cache[obj_type]
 
         if included_cache:
-            render_data['included'] = list()
+            render_data["included"] = list()
             for included_type in sorted(included_cache.keys()):
                 for included_id in sorted(included_cache[included_type].keys()):
-                    render_data['included'].append(included_cache[included_type][included_id])
+                    render_data["included"].append(
+                        included_cache[included_type][included_id]
+                    )
 
         if json_api_meta:
-            render_data['meta'] = utils.format_field_names(json_api_meta)
+            render_data["meta"] = format_field_names(json_api_meta)
 
-        return super(JSONRenderer, self).render(
-            render_data, accepted_media_type, renderer_context
-        )
+        return super().render(render_data, accepted_media_type, renderer_context)
+
+
+class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
+    template = "rest_framework_json_api/api.html"
+    includes_template = "rest_framework_json_api/includes.html"
+
+    def get_context(self, data, accepted_media_type, renderer_context):
+        context = super().get_context(data, accepted_media_type, renderer_context)
+        view = renderer_context["view"]
+
+        context["includes_form"] = self.get_includes_form(view)
+
+        return context
+
+    @classmethod
+    def _get_included_serializers(cls, serializer, prefix="", already_seen=None):
+        if not already_seen:
+            already_seen = set()
+
+        if serializer in already_seen:
+            return []
+
+        included_serializers = []
+        already_seen.add(serializer)
+
+        for include, included_serializer in getattr(
+            serializer, "included_serializers", dict()
+        ).items():
+            included_serializers.append(f"{prefix}{include}")
+            included_serializers.extend(
+                cls._get_included_serializers(
+                    included_serializer,
+                    f"{prefix}{include}.",
+                    already_seen=already_seen,
+                )
+            )
+
+        return included_serializers
+
+    def get_includes_form(self, view):
+        try:
+            if "related_field" in view.kwargs:
+                serializer_class = view.get_related_serializer_class()
+            else:
+                serializer_class = view.get_serializer_class()
+        except AttributeError:
+            return
+        if not hasattr(serializer_class, "included_serializers"):
+            return
+
+        template = loader.get_template(self.includes_template)
+        context = {"elements": self._get_included_serializers(serializer_class)}
+        return template.render(context)
diff --git a/rest_framework_json_api/schemas/openapi.py b/rest_framework_json_api/schemas/openapi.py
deleted file mode 100644
index fe6b095e..00000000
--- a/rest_framework_json_api/schemas/openapi.py
+++ /dev/null
@@ -1,818 +0,0 @@
-import warnings
-from urllib.parse import urljoin
-
-from django.utils.module_loading import import_string as import_class_from_dotted_path
-from rest_framework.fields import empty
-from rest_framework.relations import ManyRelatedField
-from rest_framework.schemas import openapi as drf_openapi
-from rest_framework.schemas.utils import is_list_view
-
-from rest_framework_json_api import serializers, views
-
-
-class SchemaGenerator(drf_openapi.SchemaGenerator):
-    """
-    Extend DRF's SchemaGenerator to implement jsonapi-flavored generateschema command.
-    """
-    #: These JSONAPI component definitions are referenced by the generated OAS schema.
-    #: If you need to add more or change these static component definitions, extend this dict.
-    jsonapi_components = {
-        'schemas': {
-            'jsonapi': {
-                'type': 'object',
-                'description': "The server's implementation",
-                'properties': {
-                    'version': {'type': 'string'},
-                    'meta': {'$ref': '#/components/schemas/meta'}
-                },
-                'additionalProperties': False
-            },
-            'resource': {
-                'type': 'object',
-                'required': ['type', 'id'],
-                'additionalProperties': False,
-                'properties': {
-                    'type': {
-                        '$ref': '#/components/schemas/type'
-                    },
-                    'id': {
-                        '$ref': '#/components/schemas/id'
-                    },
-                    'attributes': {
-                        'type': 'object',
-                        # ...
-                    },
-                    'relationships': {
-                        'type': 'object',
-                        # ...
-                    },
-                    'links': {
-                        '$ref': '#/components/schemas/links'
-                    },
-                    'meta': {'$ref': '#/components/schemas/meta'},
-                }
-            },
-            'link': {
-                'oneOf': [
-                    {
-                        'description': "a string containing the link's URL",
-                        'type': 'string',
-                        'format': 'uri-reference'
-                    },
-                    {
-                        'type': 'object',
-                        'required': ['href'],
-                        'properties': {
-                            'href': {
-                                'description': "a string containing the link's URL",
-                                'type': 'string',
-                                'format': 'uri-reference'
-                            },
-                            'meta': {'$ref': '#/components/schemas/meta'}
-                        }
-                    }
-                ]
-            },
-            'links': {
-                'type': 'object',
-                'additionalProperties': {'$ref': '#/components/schemas/link'}
-            },
-            'reltoone': {
-                'description': "a singular 'to-one' relationship",
-                'type': 'object',
-                'properties': {
-                    'links': {'$ref': '#/components/schemas/relationshipLinks'},
-                    'data': {'$ref': '#/components/schemas/relationshipToOne'},
-                    'meta': {'$ref': '#/components/schemas/meta'}
-                }
-            },
-            'relationshipToOne': {
-                'description': "reference to other resource in a to-one relationship",
-                'anyOf': [
-                    {'$ref': '#/components/schemas/nulltype'},
-                    {'$ref': '#/components/schemas/linkage'}
-                ],
-            },
-            'reltomany': {
-                'description': "a multiple 'to-many' relationship",
-                'type': 'object',
-                'properties': {
-                    'links': {'$ref': '#/components/schemas/relationshipLinks'},
-                    'data': {'$ref': '#/components/schemas/relationshipToMany'},
-                    'meta': {'$ref': '#/components/schemas/meta'}
-                }
-            },
-            'relationshipLinks': {
-                'description': 'optional references to other resource objects',
-                'type': 'object',
-                'additionalProperties': True,
-                'properties': {
-                    'self': {'$ref': '#/components/schemas/link'},
-                    'related': {'$ref': '#/components/schemas/link'}
-                }
-            },
-            'relationshipToMany': {
-                'description': "An array of objects each containing the "
-                               "'type' and 'id' for to-many relationships",
-                'type': 'array',
-                'items': {'$ref': '#/components/schemas/linkage'},
-                'uniqueItems': True
-            },
-            # A RelationshipView uses a ResourceIdentifierObjectSerializer (hence the name
-            # ResourceIdentifierObject returned by get_component_name()) which serializes type and
-            # id. These can be lists or individual items depending on whether the relationship is
-            # toMany or toOne so offer both options since we are not iterating over all the
-            # possible {related_field}'s but rather rendering one path schema which may represent
-            # toMany and toOne relationships.
-            'ResourceIdentifierObject': {
-                'oneOf': [
-                    {'$ref': '#/components/schemas/relationshipToOne'},
-                    {'$ref': '#/components/schemas/relationshipToMany'}
-                ]
-            },
-            'linkage': {
-                'type': 'object',
-                'description': "the 'type' and 'id'",
-                'required': ['type', 'id'],
-                'properties': {
-                    'type': {'$ref': '#/components/schemas/type'},
-                    'id': {'$ref': '#/components/schemas/id'},
-                    'meta': {'$ref': '#/components/schemas/meta'}
-                }
-            },
-            'pagination': {
-                'type': 'object',
-                'properties': {
-                    'first': {'$ref': '#/components/schemas/pageref'},
-                    'last': {'$ref': '#/components/schemas/pageref'},
-                    'prev': {'$ref': '#/components/schemas/pageref'},
-                    'next': {'$ref': '#/components/schemas/pageref'},
-                }
-            },
-            'pageref': {
-                'oneOf': [
-                    {'type': 'string', 'format': 'uri-reference'},
-                    {'$ref': '#/components/schemas/nulltype'}
-                ]
-            },
-            'failure': {
-                'type': 'object',
-                'required': ['errors'],
-                'properties': {
-                    'errors': {'$ref': '#/components/schemas/errors'},
-                    'meta': {'$ref': '#/components/schemas/meta'},
-                    'jsonapi': {'$ref': '#/components/schemas/jsonapi'},
-                    'links': {'$ref': '#/components/schemas/links'}
-                }
-            },
-            'errors': {
-                'type': 'array',
-                'items': {'$ref': '#/components/schemas/error'},
-                'uniqueItems': True
-            },
-            'error': {
-                'type': 'object',
-                'additionalProperties': False,
-                'properties': {
-                    'id': {'type': 'string'},
-                    'status': {'type': 'string'},
-                    'links': {'$ref': '#/components/schemas/links'},
-                    'code': {'type': 'string'},
-                    'title': {'type': 'string'},
-                    'detail': {'type': 'string'},
-                    'source': {
-                        'type': 'object',
-                        'properties': {
-                            'pointer': {
-                                'type': 'string',
-                                'description':
-                                    "A [JSON Pointer](https://tools.ietf.org/html/rfc6901) "
-                                    "to the associated entity in the request document "
-                                    "[e.g. `/data` for a primary data object, or "
-                                    "`/data/attributes/title` for a specific attribute."
-                            },
-                            'parameter': {
-                                'type': 'string',
-                                'description':
-                                    "A string indicating which query parameter "
-                                    "caused the error."
-                            },
-                            'meta': {'$ref': '#/components/schemas/meta'}
-                        }
-                    }
-                }
-            },
-            'onlymeta': {
-                'additionalProperties': False,
-                'properties': {
-                    'meta': {'$ref': '#/components/schemas/meta'}
-                }
-            },
-            'meta': {
-                'type': 'object',
-                'additionalProperties': True
-            },
-            'datum': {
-                'description': 'singular item',
-                'properties': {
-                    'data': {'$ref': '#/components/schemas/resource'}
-                }
-            },
-            'nulltype': {
-                'type': 'object',
-                'nullable': True,
-                'default': None
-            },
-            'type': {
-                'type': 'string',
-                'description':
-                    'The [type]'
-                    '(https://jsonapi.org/format/#document-resource-object-identification) '
-                    'member is used to describe resource objects that share common attributes '
-                    'and relationships.'
-            },
-            'id': {
-                'type': 'string',
-                'description':
-                    "Each resource object’s type and id pair MUST "
-                    "[identify]"
-                    "(https://jsonapi.org/format/#document-resource-object-identification) "
-                    "a single, unique resource."
-            },
-        },
-        'parameters': {
-            'include': {
-                'name': 'include',
-                'in': 'query',
-                'description': '[list of included related resources]'
-                               '(https://jsonapi.org/format/#fetching-includes)',
-                'required': False,
-                'style': 'form',
-                'schema': {
-                    'type': 'string'
-                }
-            },
-            # TODO: deepObject not well defined/supported:
-            #       https://github.com/OAI/OpenAPI-Specification/issues/1706
-            'fields': {
-                'name': 'fields',
-                'in': 'query',
-                'description': '[sparse fieldsets]'
-                               '(https://jsonapi.org/format/#fetching-sparse-fieldsets).\n'
-                               'Use fields[\\<typename\\>]=field1,field2,...,fieldN',
-                'required': False,
-                'style': 'deepObject',
-                'schema': {
-                    'type': 'object',
-                },
-                'explode': True
-            },
-            'sort': {
-                'name': 'sort',
-                'in': 'query',
-                'description': '[list of fields to sort by]'
-                               '(https://jsonapi.org/format/#fetching-sorting)',
-                'required': False,
-                'style': 'form',
-                'schema': {
-                    'type': 'string'
-                }
-            },
-        },
-    }
-
-    def get_schema(self, request=None, public=False):
-        """
-        Generate a JSONAPI OpenAPI schema.
-        Overrides upstream DRF's get_schema.
-        """
-        # TODO: avoid copying so much of upstream get_schema()
-        schema = super().get_schema(request, public)
-
-        components_schemas = {}
-
-        # Iterate endpoints generating per method path operations.
-        paths = {}
-        _, view_endpoints = self._get_paths_and_endpoints(None if public else request)
-
-        #: `expanded_endpoints` is like view_endpoints with one extra field tacked on:
-        #: - 'action' copy of current view.action (list/fetch) as this gets reset for each request.
-        expanded_endpoints = []
-        for path, method, view in view_endpoints:
-            if hasattr(view, 'action') and view.action == 'retrieve_related':
-                expanded_endpoints += self._expand_related(path, method, view, view_endpoints)
-            else:
-                expanded_endpoints.append((path, method, view, getattr(view, 'action', None)))
-
-        for path, method, view, action in expanded_endpoints:
-            if not self.has_view_permissions(path, method, view):
-                continue
-            # kludge to preserve view.action as it is 'list' for the parent ViewSet
-            # but the related viewset that was expanded may be either 'fetch' (to_one) or 'list'
-            # (to_many). This patches the view.action appropriately so that
-            # view.schema.get_operation() "does the right thing" for fetch vs. list.
-            current_action = None
-            if hasattr(view, 'action'):
-                current_action = view.action
-                view.action = action
-            operation = view.schema.get_operation(path, method)
-            components = view.schema.get_components(path, method)
-            for k in components.keys():
-                if k not in components_schemas:
-                    continue
-                if components_schemas[k] == components[k]:
-                    continue
-                warnings.warn(
-                    'Schema component "{}" has been overriden with a different value.'.format(k))
-
-            components_schemas.update(components)
-
-            if hasattr(view, 'action'):
-                view.action = current_action
-            # Normalise path for any provided mount url.
-            if path.startswith('/'):
-                path = path[1:]
-            path = urljoin(self.url or '/', path)
-
-            paths.setdefault(path, {})
-            paths[path][method.lower()] = operation
-
-        self.check_duplicate_operation_id(paths)
-
-        # Compile final schema, overriding stuff from super class.
-        schema['paths'] = paths
-        schema['components'] = self.jsonapi_components
-        schema['components']['schemas'].update(components_schemas)
-
-        return schema
-
-    def _expand_related(self, path, method, view, view_endpoints):
-        """
-        Expand path containing .../{id}/{related_field} into list of related fields
-        and **their** views, making sure toOne relationship's views are a 'fetch' and toMany
-        relationship's are a 'list'.
-        :param path
-        :param method
-        :param view
-        :param view_endpoints
-        :return:list[tuple(path, method, view, action)]
-        """
-        result = []
-        serializer = view.get_serializer()
-        # It's not obvious if it's allowed to have both included_ and related_ serializers,
-        # so just merge both dicts.
-        serializers = {}
-        if hasattr(serializer, 'included_serializers'):
-            serializers = {**serializers, **serializer.included_serializers}
-        if hasattr(serializer, 'related_serializers'):
-            serializers = {**serializers, **serializer.related_serializers}
-        related_fields = [fs for fs in serializers.items()]
-
-        for field, related_serializer in related_fields:
-            related_view = self._find_related_view(view_endpoints, related_serializer, view)
-            if related_view:
-                action = self._field_is_one_or_many(field, view)
-                result.append(
-                    (path.replace('{related_field}', field), method, related_view, action)
-                )
-
-        return result
-
-    def _find_related_view(self, view_endpoints, related_serializer, parent_view):
-        """
-        For a given related_serializer, try to find it's "parent" view instance in view_endpoints.
-        :param view_endpoints: list of all view endpoints
-        :param related_serializer: the related serializer for a given related field
-        :param parent_view: the parent view (used to find toMany vs. toOne).
-               TODO: not actually used.
-        :return:view
-        """
-        for path, method, view in view_endpoints:
-            view_serializer = view.get_serializer()
-            if not isinstance(related_serializer, type):
-                related_serializer_class = import_class_from_dotted_path(related_serializer)
-            else:
-                related_serializer_class = related_serializer
-            if isinstance(view_serializer, related_serializer_class):
-                return view
-
-        return None
-
-    def _field_is_one_or_many(self, field, view):
-        serializer = view.get_serializer()
-        if isinstance(serializer.fields[field], ManyRelatedField):
-            return 'list'
-        else:
-            return 'fetch'
-
-
-class AutoSchema(drf_openapi.AutoSchema):
-    """
-    Extend DRF's openapi.AutoSchema for JSONAPI serialization.
-    """
-    #: ignore all the media types and only generate a JSONAPI schema.
-    content_types = ['application/vnd.api+json']
-
-    def get_operation(self, path, method):
-        """
-        JSONAPI adds some standard fields to the API response that are not in upstream DRF:
-        - some that only apply to GET/HEAD methods.
-        - collections
-        - special handling for POST, PATCH, DELETE
-        """
-        operation = {}
-        operation['operationId'] = self.get_operation_id(path, method)
-        operation['description'] = self.get_description(path, method)
-
-        parameters = []
-        parameters += self.get_path_parameters(path, method)
-        # pagination, filters only apply to GET/HEAD of collections and items
-        if method in ['GET', 'HEAD']:
-            parameters += self._get_include_parameters(path, method)
-            parameters += self._get_fields_parameters(path, method)
-            parameters += self._get_sort_parameters(path, method)
-            parameters += self.get_pagination_parameters(path, method)
-            parameters += self.get_filter_parameters(path, method)
-        operation['parameters'] = parameters
-
-        # get request and response code schemas
-        if method == 'GET':
-            if is_list_view(path, method, self.view):
-                self._add_get_collection_response(operation)
-            else:
-                self._add_get_item_response(operation)
-        elif method == 'POST':
-            self._add_post_item_response(operation, path)
-        elif method == 'PATCH':
-            self._add_patch_item_response(operation, path)
-        elif method == 'DELETE':
-            # should only allow deleting a resource, not a collection
-            # TODO: implement delete of a relationship in future release.
-            self._add_delete_item_response(operation, path)
-        return operation
-
-    def get_operation_id(self, path, method):
-        """
-        The upstream DRF version creates non-unique operationIDs, because the same view is
-        used for the main path as well as such as related and relationships.
-        This concatenates the (mapped) method name and path as the spec allows most any
-        """
-        method_name = getattr(self.view, 'action', method.lower())
-        if is_list_view(path, method, self.view):
-            action = 'List'
-        elif method_name not in self.method_mapping:
-            action = method_name
-        else:
-            action = self.method_mapping[method.lower()]
-        return action + path
-
-    def _get_include_parameters(self, path, method):
-        """
-        includes parameter: https://jsonapi.org/format/#fetching-includes
-        """
-        return [{'$ref': '#/components/parameters/include'}]
-
-    def _get_fields_parameters(self, path, method):
-        """
-        sparse fieldsets https://jsonapi.org/format/#fetching-sparse-fieldsets
-        """
-        # TODO: See if able to identify the specific types for fields[type]=... and return this:
-        # name: fields
-        # in: query
-        # description: '[sparse fieldsets](https://jsonapi.org/format/#fetching-sparse-fieldsets)'
-        # required: true
-        # style: deepObject
-        # schema:
-        #   type: object
-        #   properties:
-        #     hello:
-        #       type: string  # noqa F821
-        #     world:
-        #       type: string  # noqa F821
-        # explode: true
-        return [{'$ref': '#/components/parameters/fields'}]
-
-    def _get_sort_parameters(self, path, method):
-        """
-        sort parameter: https://jsonapi.org/format/#fetching-sorting
-        """
-        return [{'$ref': '#/components/parameters/sort'}]
-
-    def _add_get_collection_response(self, operation):
-        """
-        Add GET 200 response for a collection to operation
-        """
-        operation['responses'] = {
-            '200': self._get_toplevel_200_response(operation, collection=True)
-        }
-        self._add_get_4xx_responses(operation)
-
-    def _add_get_item_response(self, operation):
-        """
-        add GET 200 response for an item to operation
-        """
-        operation['responses'] = {
-            '200': self._get_toplevel_200_response(operation, collection=False)
-        }
-        self._add_get_4xx_responses(operation)
-
-    def _get_toplevel_200_response(self, operation, collection=True):
-        """
-        return top-level JSONAPI GET 200 response
-
-        :param collection: True for collections; False for individual items.
-
-        Uses a $ref to the components.schemas.<Name> component definition.
-        """
-        if collection:
-            data = {'type': 'array', 'items': self._get_reference(self.view.get_serializer())}
-        else:
-            data = self._get_reference(self.view.get_serializer())
-
-        return {
-            'description': operation['operationId'],
-            'content': {
-                'application/vnd.api+json': {
-                    'schema': {
-                        'type': 'object',
-                        'required': ['data'],
-                        'properties': {
-                            'data': data,
-                            'included': {
-                                'type': 'array',
-                                'uniqueItems': True,
-                                'items': {
-                                    '$ref': '#/components/schemas/resource'
-                                }
-                            },
-                            'links': {
-                                'description': 'Link members related to primary data',
-                                'allOf': [
-                                    {'$ref': '#/components/schemas/links'},
-                                    {'$ref': '#/components/schemas/pagination'}
-                                ]
-                            },
-                            'jsonapi': {
-                                '$ref': '#/components/schemas/jsonapi'
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-    def _add_post_item_response(self, operation, path):
-        """
-        add response for POST of an item to operation
-        """
-        operation['requestBody'] = self.get_request_body(path, 'POST')
-        operation['responses'] = {
-            '201': self._get_toplevel_200_response(operation, collection=False)
-        }
-        operation['responses']['201']['description'] = (
-            '[Created](https://jsonapi.org/format/#crud-creating-responses-201). '
-            'Assigned `id` and/or any other changes are in this response.'
-        )
-        self._add_async_response(operation)
-        operation['responses']['204'] = {
-            'description': '[Created](https://jsonapi.org/format/#crud-creating-responses-204) '
-            'with the supplied `id`. No other changes from what was POSTed.'
-        }
-        self._add_post_4xx_responses(operation)
-
-    def _add_patch_item_response(self, operation, path):
-        """
-        Add PATCH response for an item to operation
-        """
-        operation['requestBody'] = self.get_request_body(path, 'PATCH')
-        operation['responses'] = {
-            '200': self._get_toplevel_200_response(operation, collection=False)
-        }
-        self._add_patch_4xx_responses(operation)
-
-    def _add_delete_item_response(self, operation, path):
-        """
-        add DELETE response for item or relationship(s) to operation
-        """
-        # Only DELETE of relationships has a requestBody
-        if isinstance(self.view, views.RelationshipView):
-            operation['requestBody'] = self.get_request_body(path, 'DELETE')
-        self._add_delete_responses(operation)
-
-    def get_request_body(self, path, method):
-        """
-        A request body is required by jsonapi for POST, PATCH, and DELETE methods.
-        """
-        serializer = self.get_serializer(path, method)
-        if not isinstance(serializer, (serializers.BaseSerializer, )):
-            return {}
-        is_relationship = isinstance(self.view, views.RelationshipView)
-
-        # DRF uses a $ref to the component schema definition, but this
-        # doesn't work for jsonapi due to the different required fields based on
-        # the method, so make those changes and inline another copy of the schema.
-        # TODO: A future improvement could make this DRYer with multiple component schemas:
-        #   A base schema for each viewset that has no required fields
-        #   One subclassed from the base that requires some fields (`type` but not `id` for POST)
-        #   Another subclassed from base with required type/id but no required attributes (PATCH)
-
-        if is_relationship:
-            item_schema = {'$ref': '#/components/schemas/ResourceIdentifierObject'}
-        else:
-            item_schema = self.map_serializer(serializer)
-            if method == 'POST':
-                # 'type' and 'id' are both required for:
-                # - all relationship operations
-                # - regular PATCH or DELETE
-                # Only 'type' is required for POST: system may assign the 'id'.
-                item_schema['required'] = ['type']
-
-        if 'properties' in item_schema and 'attributes' in item_schema['properties']:
-            # No required attributes for PATCH
-            if method in ['PATCH', 'PUT'] and 'required' in item_schema['properties']['attributes']:
-                del item_schema['properties']['attributes']['required']
-            # No read_only fields for request.
-            for name, schema in item_schema['properties']['attributes']['properties'].copy().items():  # noqa E501
-                if 'readOnly' in schema:
-                    del item_schema['properties']['attributes']['properties'][name]
-        return {
-            'content': {
-                ct: {
-                    'schema': {
-                        'required': ['data'],
-                        'properties': {
-                            'data': item_schema
-                        }
-                    }
-                }
-                for ct in self.content_types
-            }
-        }
-
-    def map_serializer(self, serializer):
-        """
-        Custom map_serializer that serializes the schema using the jsonapi spec.
-        Non-attributes like related and identity fields, are move to 'relationships' and 'links'.
-        """
-        # TODO: remove attributes, etc. for relationshipView??
-        required = []
-        attributes = {}
-        relationships = {}
-
-        for field in serializer.fields.values():
-            if isinstance(field, serializers.HyperlinkedIdentityField):
-                # the 'url' is not an attribute but rather a self.link, so don't map it here.
-                continue
-            if isinstance(field, serializers.HiddenField):
-                continue
-            if isinstance(field, serializers.RelatedField):
-                relationships[field.field_name] = {'$ref': '#/components/schemas/reltoone'}
-                continue
-            if isinstance(field, serializers.ManyRelatedField):
-                relationships[field.field_name] = {'$ref': '#/components/schemas/reltomany'}
-                continue
-
-            if field.required:
-                required.append(field.field_name)
-
-            schema = self.map_field(field)
-            if field.read_only:
-                schema['readOnly'] = True
-            if field.write_only:
-                schema['writeOnly'] = True
-            if field.allow_null:
-                schema['nullable'] = True
-            if field.default and field.default != empty:
-                schema['default'] = field.default
-            if field.help_text:
-                # Ensure django gettext_lazy is rendered correctly
-                schema['description'] = str(field.help_text)
-            self.map_field_validators(field, schema)
-
-            attributes[field.field_name] = schema
-
-        result = {
-            'type': 'object',
-            'required': ['type', 'id'],
-            'additionalProperties': False,
-            'properties': {
-                'type': {'$ref': '#/components/schemas/type'},
-                'id': {'$ref': '#/components/schemas/id'},
-                'links': {
-                    'type': 'object',
-                    'properties': {
-                        'self': {'$ref': '#/components/schemas/link'}
-                    }
-                }
-            }
-        }
-        if attributes:
-            result['properties']['attributes'] = {
-                'type': 'object',
-                'properties': attributes
-            }
-            if required:
-                result['properties']['attributes']['required'] = required
-
-        if relationships:
-            result['properties']['relationships'] = {
-                'type': 'object',
-                'properties': relationships
-            }
-        return result
-
-    def _add_async_response(self, operation):
-        """
-        Add async response to operation
-        """
-        operation['responses']['202'] = {
-            'description': 'Accepted for [asynchronous processing]'
-                           '(https://jsonapi.org/recommendations/#asynchronous-processing)',
-            'content': {
-                'application/vnd.api+json': {
-                    'schema': {'$ref': '#/components/schemas/datum'}
-                }
-            }
-        }
-
-    def _failure_response(self, reason):
-        """
-        Return failure response reason as the description
-        """
-        return {
-            'description': reason,
-            'content': {
-                'application/vnd.api+json': {
-                    'schema': {'$ref': '#/components/schemas/failure'}
-                }
-            }
-        }
-
-    def _add_generic_failure_responses(self, operation):
-        """
-        Add generic failure response(s) to operation
-        """
-        for code, reason in [('401', 'not authorized'), ]:
-            operation['responses'][code] = self._failure_response(reason)
-
-    def _add_get_4xx_responses(self, operation):
-        """
-        Add generic 4xx GET responses to operation
-        """
-        self._add_generic_failure_responses(operation)
-        for code, reason in [('404', 'not found')]:
-            operation['responses'][code] = self._failure_response(reason)
-
-    def _add_post_4xx_responses(self, operation):
-        """
-        Add POST 4xx error responses to operation
-        """
-        self._add_generic_failure_responses(operation)
-        for code, reason in [
-            ('403', '[Forbidden](https://jsonapi.org/format/#crud-creating-responses-403)'),
-            ('404', '[Related resource does not exist]'
-                    '(https://jsonapi.org/format/#crud-creating-responses-404)'),
-            ('409', '[Conflict](https://jsonapi.org/format/#crud-creating-responses-409)'),
-        ]:
-            operation['responses'][code] = self._failure_response(reason)
-
-    def _add_patch_4xx_responses(self, operation):
-        """
-        Add PATCH 4xx error responses to operation
-        """
-        self._add_generic_failure_responses(operation)
-        for code, reason in [
-            ('403', '[Forbidden](https://jsonapi.org/format/#crud-updating-responses-403)'),
-            ('404', '[Related resource does not exist]'
-                    '(https://jsonapi.org/format/#crud-updating-responses-404)'),
-            ('409', '[Conflict]([Conflict]'
-                    '(https://jsonapi.org/format/#crud-updating-responses-409)'),
-        ]:
-            operation['responses'][code] = self._failure_response(reason)
-
-    def _add_delete_responses(self, operation):
-        """
-        Add generic DELETE responses to operation
-        """
-        # the 2xx statuses:
-        operation['responses'] = {
-            '200': {
-                'description': '[OK](https://jsonapi.org/format/#crud-deleting-responses-200)',
-                'content': {
-                    'application/vnd.api+json': {
-                        'schema': {'$ref': '#/components/schemas/onlymeta'}
-                    }
-                }
-            }
-        }
-        self._add_async_response(operation)
-        operation['responses']['204'] = {
-            'description': '[no content](https://jsonapi.org/format/#crud-deleting-responses-204)',
-        }
-        # the 4xx errors:
-        self._add_generic_failure_responses(operation)
-        for code, reason in [
-            ('404', '[Resource does not exist]'
-                    '(https://jsonapi.org/format/#crud-deleting-responses-404)'),
-        ]:
-            operation['responses'][code] = self._failure_response(reason)
diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py
index 31f2a86b..26f6b02e 100644
--- a/rest_framework_json_api/serializers.py
+++ b/rest_framework_json_api/serializers.py
@@ -1,93 +1,121 @@
-import inflection
+from collections.abc import Mapping
+
 from django.core.exceptions import ObjectDoesNotExist
 from django.db.models.query import QuerySet
+from django.utils.module_loading import import_string as import_class_from_dotted_path
 from django.utils.translation import gettext_lazy as _
 from rest_framework.exceptions import ParseError
-from rest_framework.serializers import *  # noqa: F403
+from rest_framework.relations import HyperlinkedIdentityField
+
+# star import defined so `rest_framework_json_api.serializers` can be
+# a simple drop in for `rest_framework.serializers`
+from rest_framework.serializers import *  # noqa: F401, F403
+from rest_framework.serializers import (
+    BaseSerializer,
+    HyperlinkedModelSerializer,
+    ModelSerializer,
+    Serializer,
+    SerializerMetaclass,
+)
+from rest_framework.settings import api_settings
 
 from rest_framework_json_api.exceptions import Conflict
 from rest_framework_json_api.relations import ResourceRelatedField
 from rest_framework_json_api.utils import (
     get_included_resources,
-    get_included_serializers,
     get_resource_type_from_instance,
     get_resource_type_from_model,
-    get_resource_type_from_serializer
+    get_resource_type_from_serializer,
+    undo_format_field_name,
 )
 
 
 class ResourceIdentifierObjectSerializer(BaseSerializer):
     default_error_messages = {
-        'incorrect_model_type': _(
-            'Incorrect model type. Expected {model_type}, received {received_type}.'
+        "incorrect_model_type": _(
+            "Incorrect model type. Expected {model_type}, received {received_type}."
         ),
-        'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
-        'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),
+        "does_not_exist": _('Invalid pk "{pk_value}" - object does not exist.'),
+        "incorrect_type": _("Incorrect type. Expected pk value, received {data_type}."),
     }
 
     model_class = None
 
     def __init__(self, *args, **kwargs):
-        self.model_class = kwargs.pop('model_class', self.model_class)
+        self.model_class = kwargs.pop("model_class", self.model_class)
         # this has no fields but assumptions are made elsewhere that self.fields exists.
         self.fields = {}
-        super(ResourceIdentifierObjectSerializer, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
 
     def to_representation(self, instance):
         return {
-            'type': get_resource_type_from_instance(instance),
-            'id': str(instance.pk)
+            "type": get_resource_type_from_instance(instance),
+            "id": str(instance.pk),
         }
 
     def to_internal_value(self, data):
-        if data['type'] != get_resource_type_from_model(self.model_class):
+        if data["type"] != get_resource_type_from_model(self.model_class):
             self.fail(
-                'incorrect_model_type', model_type=self.model_class, received_type=data['type']
+                "incorrect_model_type",
+                model_type=self.model_class,
+                received_type=data["type"],
             )
-        pk = data['id']
+        pk = data["id"]
         try:
             return self.model_class.objects.get(pk=pk)
         except ObjectDoesNotExist:
-            self.fail('does_not_exist', pk_value=pk)
+            self.fail("does_not_exist", pk_value=pk)
         except (TypeError, ValueError):
-            self.fail('incorrect_type', data_type=type(data['pk']).__name__)
+            self.fail("incorrect_type", data_type=type(data["pk"]).__name__)
 
 
-class SparseFieldsetsMixin(object):
+class SparseFieldsetsMixin:
     """
     A serializer mixin that adds support for sparse fieldsets through `fields` query parameter.
 
     Specification: https://jsonapi.org/format/#fetching-sparse-fieldsets
     """
 
-    def __init__(self, *args, **kwargs):
-        super(SparseFieldsetsMixin, self).__init__(*args, **kwargs)
-        context = kwargs.get('context')
-        request = context.get('request') if context else None
+    @property
+    def _readable_fields(self):
+        request = self.context.get("request") if self.context else None
+        readable_fields = super()._readable_fields
 
         if request:
-            sparse_fieldset_query_param = 'fields[{}]'.format(
-                get_resource_type_from_serializer(self)
-            )
             try:
-                param_name = next(
-                    key for key in request.query_params if sparse_fieldset_query_param == key
+                resource_type = get_resource_type_from_serializer(self)
+                sparse_fieldset_query_param = f"fields[{resource_type}]"
+
+                sparse_fieldset_value = request.query_params.get(
+                    sparse_fieldset_query_param
                 )
-            except StopIteration:
+                if sparse_fieldset_value is not None:
+                    sparse_fields = [
+                        undo_format_field_name(sparse_field)
+                        for sparse_field in sparse_fieldset_value.split(",")
+                    ]
+                    return (
+                        field
+                        for field in readable_fields
+                        if field.field_name in sparse_fields
+                        # URL_FIELD_NAME is the field used as self-link to resource
+                        # however only when it is a HyperlinkedIdentityField
+                        or (
+                            field.field_name == api_settings.URL_FIELD_NAME
+                            and isinstance(field, HyperlinkedIdentityField)
+                        )
+                        # ID is a required field which might have been overwritten
+                        # so need to keep it
+                        or field.field_name == "id"
+                    )
+            except AttributeError:
+                # no type on serializer, may only be used nested
                 pass
-            else:
-                fieldset = request.query_params.get(param_name).split(',')
-                # iterate over a *copy* of self.fields' underlying OrderedDict, because we may
-                # modify the original during the iteration.
-                # self.fields is a `rest_framework.utils.serializer_helpers.BindingDict`
-                for field_name, field in self.fields.fields.copy().items():
-                    if field_name == api_settings.URL_FIELD_NAME:  # leave self link there
-                        continue
-                    if field_name not in fieldset:
-                        self.fields.pop(field_name)
-
-
-class IncludedResourcesValidationMixin(object):
+
+        return readable_fields
+
+
+class IncludedResourcesValidationMixin:
     """
     A serializer mixin that adds validation of `include` query parameter to
     support compound documents.
@@ -96,19 +124,19 @@ class IncludedResourcesValidationMixin(object):
     """
 
     def __init__(self, *args, **kwargs):
-        context = kwargs.get('context')
-        request = context.get('request') if context else None
-        view = context.get('view') if context else None
+        context = kwargs.get("context")
+        request = context.get("request") if context else None
+        view = context.get("view") if context else None
 
         def validate_path(serializer_class, field_path, path):
-            serializers = get_included_serializers(serializer_class)
+            serializers = getattr(serializer_class, "included_serializers", None)
             if serializers is None:
-                raise ParseError('This endpoint does not support the include parameter')
-            this_field_name = inflection.underscore(field_path[0])
+                raise ParseError("This endpoint does not support the include parameter")
+            this_field_name = field_path[0]
             this_included_serializer = serializers.get(this_field_name)
             if this_included_serializer is None:
                 raise ParseError(
-                    'This endpoint does not support the include parameter for path {}'.format(
+                    "This endpoint does not support the include parameter for path {}".format(
                         path
                     )
                 )
@@ -120,29 +148,105 @@ def validate_path(serializer_class, field_path, path):
         if request and view:
             included_resources = get_included_resources(request)
             for included_field_name in included_resources:
-                included_field_path = included_field_name.split('.')
-                this_serializer_class = view.get_serializer_class()
+                included_field_path = included_field_name.split(".")
+                if "related_field" in view.kwargs:
+                    this_serializer_class = view.get_related_serializer_class()
+                else:
+                    this_serializer_class = view.get_serializer_class()
                 # lets validate the current path
-                validate_path(this_serializer_class, included_field_path, included_field_name)
+                validate_path(
+                    this_serializer_class, included_field_path, included_field_name
+                )
+
+        super().__init__(*args, **kwargs)
+
+
+class ReservedFieldNamesMixin:
+    """Ensures that reserved field names are not used and an error raised instead."""
+
+    _reserved_field_names = {
+        "meta",
+        "results",
+        "type",
+        api_settings.NON_FIELD_ERRORS_KEY,
+    }
+
+    def get_fields(self):
+        fields = super().get_fields()
+
+        found_reserved_field_names = self._reserved_field_names.intersection(
+            fields.keys()
+        )
+        assert not found_reserved_field_names, (
+            f"Serializer class {self.__class__.__module__}.{self.__class__.__qualname__} "
+            f"uses following reserved field name(s) which is not allowed: "
+            f"{', '.join(sorted(found_reserved_field_names))}"
+        )
+
+        return fields
+
+
+class LazySerializersDict(Mapping):
+    """
+    A dictionary of serializers which lazily import dotted class path and self.
+    """
+
+    def __init__(self, parent, serializers):
+        self.parent = parent
+        self.serializers = serializers
+
+    def __getitem__(self, key):
+        value = self.serializers[key]
+        if not isinstance(value, type):
+            if value == "self":
+                value = self.parent
+            else:
+                value = import_class_from_dotted_path(value)
+            self.serializers[key] = value
+
+        return value
 
-        super(IncludedResourcesValidationMixin, self).__init__(*args, **kwargs)
+    def __iter__(self):
+        return iter(self.serializers)
+
+    def __len__(self):
+        return len(self.serializers)
+
+    def __repr__(self):
+        return dict.__repr__(self.serializers)
 
 
 class SerializerMetaclass(SerializerMetaclass):
-    pass
+    def __new__(cls, name, bases, attrs):
+        serializer = super().__new__(cls, name, bases, attrs)
+
+        if attrs.get("included_serializers", None):
+            serializer.included_serializers = LazySerializersDict(
+                serializer, attrs["included_serializers"]
+            )
+
+        if attrs.get("related_serializers", None):
+            serializer.related_serializers = LazySerializersDict(
+                serializer, attrs["related_serializers"]
+            )
+
+        return serializer
 
 
 # If user imports serializer from here we can catch class definition and check
 # nested serializers for depricated use.
 class Serializer(
-    IncludedResourcesValidationMixin, SparseFieldsetsMixin, Serializer,
-    metaclass=SerializerMetaclass
+    IncludedResourcesValidationMixin,
+    SparseFieldsetsMixin,
+    ReservedFieldNamesMixin,
+    Serializer,
+    metaclass=SerializerMetaclass,
 ):
     """
     A `Serializer` is a model-less serializer class with additional
-    support for json:api spec features.
+    support for JSON:API spec features.
 
-    As in json:api specification a type is always required you need to
+    As in JSON:API specification a type is always required you need to
     make sure that you define `resource_name` in your `Meta` class
     when deriving from this class.
 
@@ -156,8 +260,11 @@ class Serializer(
 
 
 class HyperlinkedModelSerializer(
-        IncludedResourcesValidationMixin, SparseFieldsetsMixin, HyperlinkedModelSerializer,
-        metaclass=SerializerMetaclass
+    IncludedResourcesValidationMixin,
+    SparseFieldsetsMixin,
+    ReservedFieldNamesMixin,
+    HyperlinkedModelSerializer,
+    metaclass=SerializerMetaclass,
 ):
     """
     A type of `ModelSerializer` that uses hyperlinked relationships instead
@@ -173,8 +280,13 @@ class HyperlinkedModelSerializer(
     """
 
 
-class ModelSerializer(IncludedResourcesValidationMixin, SparseFieldsetsMixin, ModelSerializer,
-                      metaclass=SerializerMetaclass):
+class ModelSerializer(
+    IncludedResourcesValidationMixin,
+    SparseFieldsetsMixin,
+    ReservedFieldNamesMixin,
+    ModelSerializer,
+    metaclass=SerializerMetaclass,
+):
     """
     A `ModelSerializer` is just a regular `Serializer`, except that:
 
@@ -196,6 +308,7 @@ class ModelSerializer(IncludedResourcesValidationMixin, SparseFieldsetsMixin, Mo
     * A mixin class to enable sparse fieldsets is included
     * A mixin class to enable validation of included resources is included
     """
+
     serializer_related_field = ResourceRelatedField
 
     def get_field_names(self, declared_fields, info):
@@ -203,15 +316,15 @@ def get_field_names(self, declared_fields, info):
         We override the parent to omit explicity defined meta fields (such
         as SerializerMethodFields) from the list of declared fields
         """
-        meta_fields = getattr(self.Meta, 'meta_fields', [])
+        meta_fields = getattr(self.Meta, "meta_fields", [])
 
-        declared = OrderedDict()
-        for field_name in set(declared_fields.keys()):
-            field = declared_fields[field_name]
-            if field_name not in meta_fields:
-                declared[field_name] = field
-        fields = super(ModelSerializer, self).get_field_names(declared, info)
-        return list(fields) + list(getattr(self.Meta, 'meta_fields', list()))
+        declared = {
+            field_name: field
+            for field_name, field in declared_fields.items()
+            if field_name not in meta_fields
+        }
+        fields = super().get_field_names(declared, info)
+        return list(fields) + list(getattr(self.Meta, "meta_fields", list()))
 
 
 class PolymorphicSerializerMetaclass(SerializerMetaclass):
@@ -221,7 +334,7 @@ class PolymorphicSerializerMetaclass(SerializerMetaclass):
     """
 
     def __new__(cls, name, bases, attrs):
-        new_class = super(PolymorphicSerializerMetaclass, cls).__new__(cls, name, bases, attrs)
+        new_class = super().__new__(cls, name, bases, attrs)
 
         # Ensure initialization is only performed for subclasses of PolymorphicModelSerializer
         # (excluding PolymorphicModelSerializer class itself).
@@ -229,17 +342,20 @@ def __new__(cls, name, bases, attrs):
         if not parents:
             return new_class
 
-        polymorphic_serializers = getattr(new_class, 'polymorphic_serializers', None)
-        if not polymorphic_serializers:
-            raise NotImplementedError(
-                "A PolymorphicModelSerializer must define a `polymorphic_serializers` attribute.")
+        polymorphic_serializers = getattr(new_class, "polymorphic_serializers", None)
+        assert (
+            polymorphic_serializers is not None
+        ), "A PolymorphicModelSerializer must define a `polymorphic_serializers` attribute."
         serializer_to_model = {
-            serializer: serializer.Meta.model for serializer in polymorphic_serializers}
+            serializer: serializer.Meta.model for serializer in polymorphic_serializers
+        }
         model_to_serializer = {
-            serializer.Meta.model: serializer for serializer in polymorphic_serializers}
+            serializer.Meta.model: serializer for serializer in polymorphic_serializers
+        }
         type_to_serializer = {
-            get_resource_type_from_serializer(serializer): serializer for
-            serializer in polymorphic_serializers}
+            get_resource_type_from_serializer(serializer): serializer
+            for serializer in polymorphic_serializers
+        }
         new_class._poly_serializer_model_map = serializer_to_model
         new_class._poly_model_serializer_map = model_to_serializer
         new_class._poly_type_serializer_map = type_to_serializer
@@ -252,22 +368,31 @@ def __new__(cls, name, bases, attrs):
         return new_class
 
 
-class PolymorphicModelSerializer(ModelSerializer, metaclass=PolymorphicSerializerMetaclass):
+class PolymorphicModelSerializer(
+    ModelSerializer, metaclass=PolymorphicSerializerMetaclass
+):
     """
     A serializer for polymorphic models.
     Useful for "lazy" parent models. Leaves should be represented with a regular serializer.
     """
+
     def get_fields(self):
         """
         Return an exhaustive list of the polymorphic serializer fields.
         """
         if self.instance not in (None, []):
             if not isinstance(self.instance, QuerySet):
-                serializer_class = self.get_polymorphic_serializer_for_instance(self.instance)
-                return serializer_class(self.instance, context=self.context).get_fields()
+                serializer_class = self.get_polymorphic_serializer_for_instance(
+                    self.instance
+                )
+                return serializer_class(
+                    self.instance, context=self.context
+                ).get_fields()
             else:
-                raise Exception("Cannot get fields from a polymorphic serializer given a queryset")
-        return super(PolymorphicModelSerializer, self).get_fields()
+                raise Exception(
+                    "Cannot get fields from a polymorphic serializer given a queryset"
+                )
+        return super().get_fields()
 
     @classmethod
     def get_polymorphic_serializer_for_instance(cls, instance):
@@ -281,7 +406,9 @@ def get_polymorphic_serializer_for_instance(cls, instance):
         except KeyError:
             raise NotImplementedError(
                 "No polymorphic serializer has been found for model {}".format(
-                    instance._meta.model.__name__))
+                    instance._meta.model.__name__
+                )
+            )
 
     @classmethod
     def get_polymorphic_model_for_serializer(cls, serializer):
@@ -294,7 +421,10 @@ def get_polymorphic_model_for_serializer(cls, serializer):
             return cls._poly_serializer_model_map[serializer]
         except KeyError:
             raise NotImplementedError(
-                "No polymorphic model has been found for serializer {}".format(serializer.__name__))
+                "No polymorphic model has been found for serializer {}".format(
+                    serializer.__name__
+                )
+            )
 
     @classmethod
     def get_polymorphic_serializer_for_type(cls, obj_type):
@@ -307,7 +437,8 @@ def get_polymorphic_serializer_for_type(cls, obj_type):
             return cls._poly_type_serializer_map[obj_type]
         except KeyError:
             raise NotImplementedError(
-                "No polymorphic serializer has been found for type {}".format(obj_type))
+                f"No polymorphic serializer has been found for type {obj_type}"
+            )
 
     @classmethod
     def get_polymorphic_model_for_type(cls, obj_type):
@@ -317,7 +448,8 @@ def get_polymorphic_model_for_type(cls, obj_type):
         means that a serializer is missing in the class's `polymorphic_serializers` attribute.
         """
         return cls.get_polymorphic_model_for_serializer(
-            cls.get_polymorphic_serializer_for_type(obj_type))
+            cls.get_polymorphic_serializer_for_type(obj_type)
+        )
 
     @classmethod
     def get_polymorphic_types(cls):
@@ -331,21 +463,27 @@ def to_representation(self, instance):
         Retrieve the appropriate polymorphic serializer and use this to handle representation.
         """
         serializer_class = self.get_polymorphic_serializer_for_instance(instance)
-        return serializer_class(instance, context=self.context).to_representation(instance)
+        return serializer_class(instance, context=self.context).to_representation(
+            instance
+        )
 
     def to_internal_value(self, data):
         """
         Ensure that the given type is one of the expected polymorphic types, then retrieve the
         appropriate polymorphic serializer and use this to handle internal value.
         """
-        received_type = data.get('type')
+        received_type = data.get("type")
         expected_types = self.get_polymorphic_types()
         if received_type not in expected_types:
             raise Conflict(
-                'Incorrect relation type. Expected on of [{expected_types}], '
-                'received {received_type}.'.format(
-                    expected_types=', '.join(expected_types), received_type=received_type))
+                "Incorrect relation type. Expected on of [{expected_types}], "
+                "received {received_type}.".format(
+                    expected_types=", ".join(expected_types),
+                    received_type=received_type,
+                )
+            )
         serializer_class = self.get_polymorphic_serializer_for_type(received_type)
         self.__class__ = serializer_class
-        return serializer_class(self.instance, data, context=self.context,
-                                partial=self.partial).to_internal_value(data)
+        return serializer_class(
+            self.instance, data, context=self.context, partial=self.partial
+        ).to_internal_value(data)
diff --git a/rest_framework_json_api/settings.py b/rest_framework_json_api/settings.py
index 1385630c..83228359 100644
--- a/rest_framework_json_api/settings.py
+++ b/rest_framework_json_api/settings.py
@@ -1,25 +1,26 @@
 """
 This module provides the `json_api_settings` object that is used to access
-JSON API REST framework settings, checking for user settings first, then falling back to
+Django REST framework JSON:API settings, checking for user settings first, then falling back to
 the defaults.
 """
 
 from django.conf import settings
 from django.core.signals import setting_changed
 
-JSON_API_SETTINGS_PREFIX = 'JSON_API_'
+JSON_API_SETTINGS_PREFIX = "JSON_API_"
 
 DEFAULTS = {
-    'FORMAT_FIELD_NAMES': False,
-    'FORMAT_TYPES': False,
-    'PLURALIZE_TYPES': False,
-    'UNIFORM_EXCEPTIONS': False,
+    "FORMAT_FIELD_NAMES": False,
+    "FORMAT_TYPES": False,
+    "FORMAT_RELATED_LINKS": False,
+    "PLURALIZE_TYPES": False,
+    "UNIFORM_EXCEPTIONS": False,
 }
 
 
-class JSONAPISettings(object):
+class JSONAPISettings:
     """
-    A settings object that allows json api settings to be access as
+    A settings object that allows JSON:API settings to be access as
     properties.
     """
 
@@ -29,9 +30,11 @@ def __init__(self, user_settings=settings, defaults=DEFAULTS):
 
     def __getattr__(self, attr):
         if attr not in self.defaults:
-            raise AttributeError("Invalid JSON API setting: '%s'" % attr)
+            raise AttributeError(f"Invalid JSON:API setting: '{attr}'")
 
-        value = getattr(self.user_settings, JSON_API_SETTINGS_PREFIX + attr, self.defaults[attr])
+        value = getattr(
+            self.user_settings, JSON_API_SETTINGS_PREFIX + attr, self.defaults[attr]
+        )
 
         # Cache the result
         setattr(self, attr, value)
@@ -42,9 +45,9 @@ def __getattr__(self, attr):
 
 
 def reload_json_api_settings(*args, **kwargs):
-    django_setting = kwargs['setting']
-    setting = django_setting.replace(JSON_API_SETTINGS_PREFIX, '')
-    value = kwargs['value']
+    django_setting = kwargs["setting"]
+    setting = django_setting.replace(JSON_API_SETTINGS_PREFIX, "")
+    value = kwargs["value"]
     if setting in DEFAULTS.keys():
         if value is not None:
             setattr(json_api_settings, setting, value)
diff --git a/rest_framework_json_api/templates/rest_framework_json_api/api.html b/rest_framework_json_api/templates/rest_framework_json_api/api.html
new file mode 100644
index 00000000..a9f7fae1
--- /dev/null
+++ b/rest_framework_json_api/templates/rest_framework_json_api/api.html
@@ -0,0 +1,19 @@
+{% extends "rest_framework/base.html" %}
+{% load i18n %}
+
+{% block request_forms %}
+  {{ block.super }}
+  {% if includes_form %}
+    <button style="float: right; margin-right: 10px" data-toggle="modal" data-target="#includesModal" class="btn btn-default">
+      <span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
+      {% trans "JSON:API includes" %}
+    </button>
+  {% endif %}
+{% endblock request_forms %}
+
+{% block script %}
+  {{ block.super }}
+  {% if includes_form %}
+    {{ includes_form }}
+  {% endif %}
+{% endblock script %}
diff --git a/rest_framework_json_api/templates/rest_framework_json_api/includes.html b/rest_framework_json_api/templates/rest_framework_json_api/includes.html
new file mode 100644
index 00000000..a14a9285
--- /dev/null
+++ b/rest_framework_json_api/templates/rest_framework_json_api/includes.html
@@ -0,0 +1,40 @@
+{% load i18n %}
+
+<div class="modal fade" id="includesModal" tabindex="-1" role="dialog" aria-labelledby="includes" aria-hidden="true">
+  <div class="modal-dialog">
+    <div class="modal-content">
+      <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+        <h4 class="modal-title">{% trans "JSON:API includes" %}</h4>
+      </div>
+      <div class="modal-body">
+        {% for element in elements %}
+        <div>
+          <label for="includes-{{ element }}">{{ element }}</label>
+          <input type="checkbox" name="includes" id="includes-{{ element }}" value="{{ element }}">
+        </div>
+        {% endfor %}
+        <form method="get">
+          <input type="hidden" name="include">
+          <button type="submit">{% trans "Apply includes" %}</button>
+        </form>
+      </div>
+    </div>
+  </div>
+</div>
+<script>
+$(document).ready(function() {
+  let param_include = new URLSearchParams(window.location.search).get('include')
+  if (param_include) {
+    let applied_includes = param_include.split(',')
+    $('#includesModal input[name=includes]').each(function () {
+      this.checked = applied_includes.includes(this.value)
+    })
+  }
+  $('#includesModal form').submit(function () {
+    $('#includesModal input[name=include]').get(0).value = $('#includesModal input[name=includes]:checked').map(
+        function() {return this.value}
+    ).get().join(",")
+  })
+});
+</script>
diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py
index 2443575f..2dd79677 100644
--- a/rest_framework_json_api/utils.py
+++ b/rest_framework_json_api/utils.py
@@ -1,26 +1,24 @@
-import copy
 import inspect
 import operator
-from collections import OrderedDict
 
 import inflection
 from django.conf import settings
 from django.db.models import Manager
 from django.db.models.fields.related_descriptors import (
     ManyToManyDescriptor,
-    ReverseManyToOneDescriptor
+    ReverseManyToOneDescriptor,
 )
 from django.http import Http404
 from django.utils import encoding
-from django.utils.module_loading import import_string as import_class_from_dotted_path
 from django.utils.translation import gettext_lazy as _
-from rest_framework import exceptions
+from rest_framework import exceptions, relations
 from rest_framework.exceptions import APIException
+from rest_framework.settings import api_settings
 
 from .settings import json_api_settings
 
 # Generic relation descriptor from django.contrib.contenttypes.
-if 'django.contrib.contenttypes' not in settings.INSTALLED_APPS:  # pragma: no cover
+if "django.contrib.contenttypes" not in settings.INSTALLED_APPS:  # pragma: no cover
     # Target application does not use contenttypes. Importing would cause errors.
     ReverseGenericManyToOneDescriptor = object()
 else:
@@ -32,7 +30,8 @@ def get_resource_name(context, expand_polymorphic_types=False):
     Return the name of a resource.
     """
     from rest_framework_json_api.serializers import PolymorphicModelSerializer
-    view = context.get('view')
+
+    view = context.get("view")
 
     # Sanity check to make sure we have a view.
     if not view:
@@ -45,15 +44,20 @@ def get_resource_name(context, expand_polymorphic_types=False):
     except (AttributeError, ValueError):
         pass
     else:
-        if code.startswith('4') or code.startswith('5'):
-            return 'errors'
+        if code.startswith("4") or code.startswith("5"):
+            return "errors"
 
     try:
-        resource_name = getattr(view, 'resource_name')
+        resource_name = view.resource_name
     except AttributeError:
         try:
-            serializer = view.get_serializer_class()
-            if expand_polymorphic_types and issubclass(serializer, PolymorphicModelSerializer):
+            if "kwargs" in context and "related_field" in context["kwargs"]:
+                serializer = view.get_related_serializer_class()
+            else:
+                serializer = view.get_serializer_class()
+            if expand_polymorphic_types and issubclass(
+                serializer, PolymorphicModelSerializer
+            ):
                 return serializer.get_polymorphic_types()
             else:
                 return get_resource_type_from_serializer(serializer)
@@ -75,15 +79,15 @@ def get_resource_name(context, expand_polymorphic_types=False):
 
 def get_serializer_fields(serializer):
     fields = None
-    if hasattr(serializer, 'child'):
-        fields = getattr(serializer.child, 'fields')
-        meta = getattr(serializer.child, 'Meta', None)
-    if hasattr(serializer, 'fields'):
-        fields = getattr(serializer, 'fields')
-        meta = getattr(serializer, 'Meta', None)
+    if hasattr(serializer, "child"):
+        fields = serializer.child.fields
+        meta = getattr(serializer.child, "Meta", None)
+    if hasattr(serializer, "fields"):
+        fields = serializer.fields
+        meta = getattr(serializer, "Meta", None)
 
     if fields is not None:
-        meta_fields = getattr(meta, 'meta_fields', {})
+        meta_fields = getattr(meta, "meta_fields", {})
         for field in meta_fields:
             try:
                 fields.pop(field)
@@ -103,27 +107,74 @@ def format_field_names(obj, format_type=None):
         format_type = json_api_settings.FORMAT_FIELD_NAMES
 
     if isinstance(obj, dict):
-        formatted = OrderedDict()
-        for key, value in obj.items():
-            key = format_value(key, format_type)
-            formatted[key] = value
-        return formatted
+        return {format_value(key, format_type): value for key, value in obj.items()}
 
     return obj
 
 
-def format_value(value, format_type=None):
-    if format_type is None:
-        format_type = json_api_settings.FORMAT_FIELD_NAMES
-    if format_type == 'dasherize':
+def undo_format_field_names(obj):
+    """
+    Takes a dict and undo format field names to underscore which is the Python convention
+    but only in case `JSON_API_FORMAT_FIELD_NAMES` is actually configured.
+    """
+    if json_api_settings.FORMAT_FIELD_NAMES:
+        return format_field_names(obj, "underscore")
+
+    return obj
+
+
+def format_field_name(field_name):
+    """
+    Takes a field name and returns it with formatted keys as set in
+    `JSON_API_FORMAT_FIELD_NAMES`
+    """
+    return format_value(field_name, json_api_settings.FORMAT_FIELD_NAMES)
+
+
+def undo_format_field_name(field_name):
+    """
+    Takes a string and undos format field name to underscore which is the Python convention
+    but only in case `JSON_API_FORMAT_FIELD_NAMES` is actually configured.
+    """
+    if json_api_settings.FORMAT_FIELD_NAMES:
+        return format_value(field_name, "underscore")
+
+    return field_name
+
+
+def format_link_segment(value):
+    """
+    Takes a string value and returns it with formatted keys as set in `format_type`
+    or `JSON_API_FORMAT_RELATED_LINKS`.
+
+    :format_type: Either 'dasherize', 'camelize', 'capitalize' or 'underscore'
+    """
+    format_type = json_api_settings.FORMAT_RELATED_LINKS
+    return format_value(value, format_type)
+
+
+def undo_format_link_segment(value):
+    """
+    Takes a link segment and undos format link segment to underscore which is the Python
+    convention but only in case `JSON_API_FORMAT_RELATED_LINKS` is actually configured.
+    """
+
+    if json_api_settings.FORMAT_RELATED_LINKS:
+        return format_value(value, "underscore")
+
+    return value
+
+
+def format_value(value, format_type):
+    if format_type == "dasherize":
         # inflection can't dasherize camelCase
         value = inflection.underscore(value)
         value = inflection.dasherize(value)
-    elif format_type == 'camelize':
+    elif format_type == "camelize":
         value = inflection.camelize(value, False)
-    elif format_type == 'capitalize':
+    elif format_type == "capitalize":
         value = inflection.camelize(value)
-    elif format_type == 'underscore':
+    elif format_type == "underscore":
         value = inflection.underscore(value)
     return value
 
@@ -136,7 +187,6 @@ def format_resource_type(value, format_type=None, pluralize=None):
         pluralize = json_api_settings.PLURALIZE_TYPES
 
     if format_type:
-        # format_type will never be None here so we can use format_value
         value = format_value(value, format_type)
 
     return inflection.pluralize(value) if pluralize else value
@@ -144,44 +194,51 @@ def format_resource_type(value, format_type=None, pluralize=None):
 
 def get_related_resource_type(relation):
     from rest_framework_json_api.serializers import PolymorphicModelSerializer
+
     try:
         return get_resource_type_from_serializer(relation)
     except AttributeError:
         pass
     relation_model = None
-    if hasattr(relation, '_meta'):
+    if hasattr(relation, "_meta"):
         relation_model = relation._meta.model
-    elif hasattr(relation, 'model'):
+    elif hasattr(relation, "model"):
         # the model type was explicitly passed as a kwarg to ResourceRelatedField
         relation_model = relation.model
-    elif hasattr(relation, 'get_queryset') and relation.get_queryset() is not None:
+    elif hasattr(relation, "get_queryset") and relation.get_queryset() is not None:
         relation_model = relation.get_queryset().model
-    elif (
-            getattr(relation, 'many', False) and
-            hasattr(relation.child, 'Meta') and
-            hasattr(relation.child.Meta, 'model')):
-        # For ManyToMany relationships, get the model from the child
-        # serializer of the list serializer
-        relation_model = relation.child.Meta.model
-    else:
+    elif hasattr(relation, "child_relation"):
+        # For ManyRelatedField relationships, get the model from the child relationship
+        try:
+            return get_related_resource_type(relation.child_relation)
+        except AttributeError:
+            # Some read only relationships fail to get it directly, fall through to
+            # get via the parent
+            pass
+    if not relation_model:
         parent_serializer = relation.parent
         parent_model = None
         if isinstance(parent_serializer, PolymorphicModelSerializer):
             parent_model = parent_serializer.get_polymorphic_serializer_for_instance(
-                parent_serializer.instance).Meta.model
-        elif hasattr(parent_serializer, 'Meta'):
-            parent_model = getattr(parent_serializer.Meta, 'model', None)
-        elif hasattr(parent_serializer, 'parent') and hasattr(parent_serializer.parent, 'Meta'):
-            parent_model = getattr(parent_serializer.parent.Meta, 'model', None)
+                parent_serializer.instance
+            ).Meta.model
+        elif hasattr(parent_serializer, "Meta"):
+            parent_model = getattr(parent_serializer.Meta, "model", None)
+        elif hasattr(parent_serializer, "parent") and hasattr(
+            parent_serializer.parent, "Meta"
+        ):
+            parent_model = getattr(parent_serializer.parent.Meta, "model", None)
 
         if parent_model is not None:
             if relation.source:
-                if relation.source != '*':
+                if relation.source != "*":
                     parent_model_relation = getattr(parent_model, relation.source)
                 else:
                     parent_model_relation = getattr(parent_model, relation.field_name)
             else:
-                parent_model_relation = getattr(parent_model, parent_serializer.field_name)
+                parent_model_relation = getattr(
+                    parent_model, parent_serializer.field_name
+                )
 
             parent_model_relation_type = type(parent_model_relation)
             if parent_model_relation_type is ReverseManyToOneDescriptor:
@@ -193,7 +250,7 @@ def get_related_resource_type(relation):
                     relation_model = parent_model_relation.field.model
             elif parent_model_relation_type is ReverseGenericManyToOneDescriptor:
                 relation_model = parent_model_relation.rel.model
-            elif hasattr(parent_model_relation, 'field'):
+            elif hasattr(parent_model_relation, "field"):
                 try:
                     relation_model = parent_model_relation.field.remote_field.model
                 except AttributeError:
@@ -202,17 +259,21 @@ def get_related_resource_type(relation):
                 return get_related_resource_type(parent_model_relation)
 
     if relation_model is None:
-        raise APIException(_('Could not resolve resource type for relation %s' % relation))
+        # For ManyRelatedFields on plain Serializers the resource_type
+        # cannot be determined from a model, so we must get it from the
+        # child_relation
+        if hasattr(relation, "child_relation"):
+            return get_related_resource_type(relation.child_relation)
+        raise APIException(
+            _(f"Could not resolve resource type for relation {relation}")
+        )
 
     return get_resource_type_from_model(relation_model)
 
 
 def get_resource_type_from_model(model):
-    json_api_meta = getattr(model, 'JSONAPIMeta', None)
-    return getattr(
-        json_api_meta,
-        'resource_name',
-        format_resource_type(model.__name__))
+    json_api_meta = getattr(model, "JSONAPIMeta", None)
+    return getattr(json_api_meta, "resource_name", format_resource_type(model.__name__))
 
 
 def get_resource_type_from_queryset(qs):
@@ -220,7 +281,7 @@ def get_resource_type_from_queryset(qs):
 
 
 def get_resource_type_from_instance(instance):
-    if hasattr(instance, '_meta'):
+    if hasattr(instance, "_meta"):
         return get_resource_type_from_model(instance._meta.model)
 
 
@@ -229,46 +290,53 @@ def get_resource_type_from_manager(manager):
 
 
 def get_resource_type_from_serializer(serializer):
-    json_api_meta = getattr(serializer, 'JSONAPIMeta', None)
-    meta = getattr(serializer, 'Meta', None)
-    if hasattr(json_api_meta, 'resource_name'):
+    json_api_meta = getattr(serializer, "JSONAPIMeta", None)
+    meta = getattr(serializer, "Meta", None)
+    if hasattr(json_api_meta, "resource_name"):
         return json_api_meta.resource_name
-    elif hasattr(meta, 'resource_name'):
+    elif hasattr(meta, "resource_name"):
         return meta.resource_name
-    elif hasattr(meta, 'model'):
+    elif hasattr(meta, "model"):
         return get_resource_type_from_model(meta.model)
-    raise AttributeError()
+    raise AttributeError(
+        f"can not detect 'resource_name' on serializer {serializer.__class__.__name__!r}"
+        f" in module {serializer.__class__.__module__!r}"
+    )
+
+
+def get_resource_id(resource_instance, resource):
+    """Returns the resource identifier for a given instance (`id` takes priority over `pk`)."""
+    if resource and "id" in resource:
+        _id = resource["id"]
+        return encoding.force_str(_id) if _id is not None else None
+    if resource_instance:
+        pk = getattr(resource_instance, "pk", None)
+        return encoding.force_str(pk) if pk is not None else None
+    return None
 
 
 def get_included_resources(request, serializer=None):
-    """ Build a list of included resources. """
-    include_resources_param = request.query_params.get('include') if request else None
+    """
+    Build a list of included resources.
+
+    This method ensures that returned includes are in Python internally used
+    format.
+    """
+    include_resources_param = request.query_params.get("include") if request else None
     if include_resources_param:
-        return include_resources_param.split(',')
+        return [
+            undo_format_field_name(include)
+            for include in include_resources_param.split(",")
+        ]
     else:
         return get_default_included_resources_from_serializer(serializer)
 
 
 def get_default_included_resources_from_serializer(serializer):
-    meta = getattr(serializer, 'JSONAPIMeta', None)
-    if meta is None and getattr(serializer, 'many', False):
-        meta = getattr(serializer.child, 'JSONAPIMeta', None)
-    return list(getattr(meta, 'included_resources', []))
-
-
-def get_included_serializers(serializer):
-    included_serializers = copy.copy(getattr(serializer, 'included_serializers', dict()))
-
-    for name, value in iter(included_serializers.items()):
-        if not isinstance(value, type):
-            if value == 'self':
-                included_serializers[name] = (
-                    serializer if isinstance(serializer, type) else serializer.__class__
-                )
-            else:
-                included_serializers[name] = import_class_from_dotted_path(value)
-
-    return included_serializers
+    meta = getattr(serializer, "JSONAPIMeta", None)
+    if meta is None and getattr(serializer, "many", False):
+        meta = getattr(serializer.child, "JSONAPIMeta", None)
+    return list(getattr(meta, "included_resources", []))
 
 
 def get_relation_instance(resource_instance, source, serializer):
@@ -278,7 +346,7 @@ def get_relation_instance(resource_instance, source, serializer):
         # if the field is not defined on the model then we check the serializer
         # and if no value is there we skip over the field completely
         serializer_method = getattr(serializer, source, None)
-        if serializer_method and hasattr(serializer_method, '__call__'):
+        if serializer_method and callable(serializer_method):
             relation_instance = serializer_method(resource_instance)
         else:
             return False, None
@@ -289,6 +357,10 @@ def get_relation_instance(resource_instance, source, serializer):
     return True, relation_instance
 
 
+def is_relationship_field(field):
+    return isinstance(field, (relations.RelatedField, relations.ManyRelatedField))
+
+
 class Hyperlink(str):
     """
     A string like object that additionally has an associated name.
@@ -299,8 +371,8 @@ class Hyperlink(str):
     https://github.com/tomchristie/django-rest-framework
     """
 
-    def __new__(self, url, name):
-        ret = str.__new__(self, url)
+    def __new__(cls, url, name):
+        ret = str.__new__(cls, url)
         ret.name = name
         return ret
 
@@ -312,12 +384,34 @@ def format_drf_errors(response, context, exc):
     # handle generic errors. ValidationError('test') in a view for example
     if isinstance(response.data, list):
         for message in response.data:
-            errors.extend(format_error_object(message, '/data', response))
+            errors.extend(format_error_object(message, "/data", response))
     # handle all errors thrown from serializers
     else:
+        try:
+            serializer = context["view"].get_serializer()
+            fields = get_serializer_fields(serializer) or dict()
+            relationship_fields = [
+                format_field_name(name)
+                for name, field in fields.items()
+                if is_relationship_field(field)
+            ]
+        except Exception:
+            # ignore potential errors when retrieving serializer
+            # as it might shadow error which is currently being
+            # formatted
+            serializer = None
+
         for field, error in response.data.items():
-            field = format_value(field)
-            pointer = '/data/attributes/{}'.format(field)
+            non_field_error = field == api_settings.NON_FIELD_ERRORS_KEY
+            field = format_field_name(field)
+            pointer = None
+            if non_field_error:
+                # Serializer error does not refer to a specific field.
+                pointer = "/data"
+            elif serializer:
+                # pointer can be determined only if there's a serializer.
+                rel = "relationships" if field in relationship_fields else "attributes"
+                pointer = f"/data/{rel}/{field}"
             if isinstance(exc, Http404) and isinstance(error, str):
                 # 404 errors don't have a pointer
                 errors.extend(format_error_object(error, None, response))
@@ -325,14 +419,14 @@ def format_drf_errors(response, context, exc):
                 classes = inspect.getmembers(exceptions, inspect.isclass)
                 # DRF sets the `field` to 'detail' for its own exceptions
                 if isinstance(exc, tuple(x[1] for x in classes)):
-                    pointer = '/data'
+                    pointer = "/data"
                 errors.extend(format_error_object(error, pointer, response))
             elif isinstance(error, list):
                 errors.extend(format_error_object(error, pointer, response))
             else:
                 errors.extend(format_error_object(error, pointer, response))
 
-    context['view'].resource_name = 'errors'
+    context["view"].resource_name = "errors"
     response.data = errors
 
     return response
@@ -341,44 +435,45 @@ def format_drf_errors(response, context, exc):
 def format_error_object(message, pointer, response):
     errors = []
     if isinstance(message, dict):
-
         # as there is no required field in error object we check that all fields are string
-        # except links and source which might be a dict
-        is_custom_error = all([
-            isinstance(value, str)
-            for key, value in message.items() if key not in ['links', 'source']
-        ])
+        # except links, source or meta which might be a dict
+        is_custom_error = all(
+            [
+                isinstance(value, str)
+                for key, value in message.items()
+                if key not in ["links", "source", "meta"]
+            ]
+        )
 
         if is_custom_error:
-            if 'source' not in message:
-                message['source'] = {}
-            message['source'] = {
-                'pointer': pointer,
-            }
+            if "source" not in message:
+                message["source"] = {}
+            if "pointer" not in message["source"]:
+                message["source"]["pointer"] = pointer
             errors.append(message)
         else:
             for k, v in message.items():
-                errors.extend(format_error_object(v, pointer + '/{}'.format(k), response))
+                errors.extend(format_error_object(v, pointer + f"/{k}", response))
     elif isinstance(message, list):
         for num, error in enumerate(message):
             if isinstance(error, (list, dict)):
-                new_pointer = pointer + '/{}'.format(num)
+                new_pointer = pointer + f"/{num}"
             else:
                 new_pointer = pointer
             if error:
                 errors.extend(format_error_object(error, new_pointer, response))
     else:
         error_obj = {
-            'detail': message,
-            'status': encoding.force_str(response.status_code),
+            "detail": message,
+            "status": encoding.force_str(response.status_code),
         }
         if pointer is not None:
-            error_obj['source'] = {
-                'pointer': pointer,
+            error_obj["source"] = {
+                "pointer": pointer,
             }
         code = getattr(message, "code", None)
         if code is not None:
-            error_obj['code'] = code
+            error_obj["code"] = code
         errors.append(error_obj)
 
     return errors
@@ -386,5 +481,5 @@ def format_error_object(message, pointer, response):
 
 def format_errors(data):
     if len(data) > 1 and isinstance(data, list):
-        data.sort(key=lambda x: x.get('source', {}).get('pointer', ''))
-    return {'errors': data}
+        data.sort(key=lambda x: x.get("source", {}).get("pointer", ""))
+    return {"errors": data}
diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py
index 2e0b22ef..90bb5574 100644
--- a/rest_framework_json_api/views.py
+++ b/rest_framework_json_api/views.py
@@ -6,12 +6,11 @@
     ForwardManyToOneDescriptor,
     ManyToManyDescriptor,
     ReverseManyToOneDescriptor,
-    ReverseOneToOneDescriptor
+    ReverseOneToOneDescriptor,
 )
 from django.db.models.manager import Manager
 from django.db.models.query import QuerySet
 from django.urls import NoReverseMatch
-from django.utils.module_loading import import_string as import_class_from_dotted_path
 from rest_framework import generics, viewsets
 from rest_framework.exceptions import MethodNotAllowed, NotFound
 from rest_framework.fields import get_attribute
@@ -24,13 +23,13 @@
 from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer
 from rest_framework_json_api.utils import (
     Hyperlink,
-    OrderedDict,
     get_included_resources,
-    get_resource_type_from_instance
+    get_resource_type_from_instance,
+    undo_format_link_segment,
 )
 
 
-class PreloadIncludesMixin(object):
+class PreloadIncludesMixin:
     """
     This mixin provides a helper attributes to select or prefetch related models
     based on the include specified in the URL.
@@ -54,17 +53,18 @@ class MyViewSet(viewsets.ModelViewSet):
     """
 
     def get_select_related(self, include):
-        return getattr(self, 'select_for_includes', {}).get(include, None)
+        return getattr(self, "select_for_includes", {}).get(include, None)
 
     def get_prefetch_related(self, include):
-        return getattr(self, 'prefetch_for_includes', {}).get(include, None)
+        return getattr(self, "prefetch_for_includes", {}).get(include, None)
 
     def get_queryset(self, *args, **kwargs):
-        qs = super(PreloadIncludesMixin, self).get_queryset(*args, **kwargs)
-
-        included_resources = get_included_resources(self.request)
-        for included in included_resources + ['__all__']:
+        qs = super().get_queryset(*args, **kwargs)
 
+        included_resources = get_included_resources(
+            self.request, self.get_serializer_class()
+        )
+        for included in included_resources + ["__all__"]:
             select_related = self.get_select_related(included)
             if select_related is not None:
                 qs = qs.select_related(*select_related)
@@ -76,17 +76,19 @@ def get_queryset(self, *args, **kwargs):
         return qs
 
 
-class AutoPrefetchMixin(object):
+class AutoPrefetchMixin:
     def get_queryset(self, *args, **kwargs):
-        """ This mixin adds automatic prefetching for OneToOne and ManyToMany fields. """
-        qs = super(AutoPrefetchMixin, self).get_queryset(*args, **kwargs)
+        """This mixin adds automatic prefetching for OneToOne and ManyToMany fields."""
+        qs = super().get_queryset(*args, **kwargs)
 
-        included_resources = get_included_resources(self.request)
+        included_resources = get_included_resources(
+            self.request, self.get_serializer_class()
+        )
 
-        for included in included_resources + ['__all__']:
+        for included in included_resources + ["__all__"]:
             # If include was not defined, trying to resolve it automatically
             included_model = None
-            levels = included.split('.')
+            levels = included.split(".")
             level_model = qs.model
             for level in levels:
                 if not hasattr(level_model, level):
@@ -94,11 +96,11 @@ def get_queryset(self, *args, **kwargs):
                 field = getattr(level_model, level)
                 field_class = field.__class__
 
-                is_forward_relation = (
-                    issubclass(field_class, (ForwardManyToOneDescriptor, ManyToManyDescriptor))
+                is_forward_relation = issubclass(
+                    field_class, (ForwardManyToOneDescriptor, ManyToManyDescriptor)
                 )
-                is_reverse_relation = (
-                    issubclass(field_class, (ReverseManyToOneDescriptor, ReverseOneToOneDescriptor))
+                is_reverse_relation = issubclass(
+                    field_class, (ReverseManyToOneDescriptor, ReverseOneToOneDescriptor)
                 )
                 if not (is_forward_relation or is_reverse_relation):
                     break
@@ -106,7 +108,6 @@ def get_queryset(self, *args, **kwargs):
                 if level == levels[-1]:
                     included_model = field
                 else:
-
                     if issubclass(field_class, ReverseOneToOneDescriptor):
                         model_field = field.related.field
                     else:
@@ -118,21 +119,24 @@ def get_queryset(self, *args, **kwargs):
                         level_model = model_field.model
 
             if included_model is not None:
-                qs = qs.prefetch_related(included.replace('.', '__'))
+                qs = qs.prefetch_related(included.replace(".", "__"))
 
         return qs
 
 
-class RelatedMixin(object):
+class RelatedMixin:
     """
-    This mixin handles all related entities, whose Serializers are declared in "related_serializers"
+    Mixing handling related links.
+
+    This mixin handles all related entities, whose Serializers are declared
+    in "related_serializers".
     """
 
     def retrieve_related(self, request, *args, **kwargs):
         serializer_kwargs = {}
         instance = self.get_related_instance()
 
-        if hasattr(instance, 'all'):
+        if hasattr(instance, "all"):
             instance = instance.all()
 
         if callable(instance):
@@ -142,44 +146,53 @@ def retrieve_related(self, request, *args, **kwargs):
             return Response(data=None)
 
         if isinstance(instance, Iterable):
-            serializer_kwargs['many'] = True
+            serializer_kwargs["many"] = True
 
-        serializer = self.get_serializer(instance, **serializer_kwargs)
+        serializer = self.get_related_serializer(instance, **serializer_kwargs)
         return Response(serializer.data)
 
-    def get_serializer_class(self):
-        parent_serializer_class = super(RelatedMixin, self).get_serializer_class()
+    def get_related_serializer(self, instance, **kwargs):
+        serializer_class = self.get_related_serializer_class()
+        kwargs.setdefault("context", self.get_serializer_context())
+        return serializer_class(instance, **kwargs)
+
+    def get_related_serializer_class(self):
+        parent_serializer_class = self.get_serializer_class()
+
+        if "related_field" in self.kwargs:
+            field_name = self.get_related_field_name()
 
-        if 'related_field' in self.kwargs:
-            field_name = self.kwargs['related_field']
+            assert hasattr(parent_serializer_class, "included_serializers") or hasattr(
+                parent_serializer_class, "related_serializers"
+            ), 'Either "included_serializers" or "related_serializers" should be configured'
 
             # Try get the class from related_serializers
-            if hasattr(parent_serializer_class, 'related_serializers'):
-                _class = parent_serializer_class.related_serializers.get(field_name, None)
+            if hasattr(parent_serializer_class, "related_serializers"):
+                _class = parent_serializer_class.related_serializers.get(
+                    field_name, None
+                )
                 if _class is None:
                     raise NotFound
 
-            elif hasattr(parent_serializer_class, 'included_serializers'):
-                _class = parent_serializer_class.included_serializers.get(field_name, None)
+            elif hasattr(parent_serializer_class, "included_serializers"):
+                _class = parent_serializer_class.included_serializers.get(
+                    field_name, None
+                )
                 if _class is None:
                     raise NotFound
 
-            else:
-                assert False, \
-                    'Either "included_serializers" or "related_serializers" should be configured'
-
-            if not isinstance(_class, type):
-                return import_class_from_dotted_path(_class)
             return _class
 
         return parent_serializer_class
 
     def get_related_field_name(self):
-        return self.kwargs['related_field']
+        field_name = self.kwargs["related_field"]
+        return undo_format_link_segment(field_name)
 
     def get_related_instance(self):
         parent_obj = self.get_object()
-        parent_serializer = self.serializer_class(parent_obj)
+        parent_serializer_class = self.get_serializer_class()
+        parent_serializer = parent_serializer_class(parent_obj)
         field_name = self.get_related_field_name()
         field = parent_serializer.fields.get(field_name, None)
 
@@ -200,17 +213,16 @@ def get_related_instance(self):
                 raise NotFound
 
 
-class ModelViewSet(AutoPrefetchMixin,
-                   PreloadIncludesMixin,
-                   RelatedMixin,
-                   viewsets.ModelViewSet):
-    http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
+class ModelViewSet(
+    AutoPrefetchMixin, PreloadIncludesMixin, RelatedMixin, viewsets.ModelViewSet
+):
+    http_method_names = ["get", "post", "patch", "delete", "head", "options"]
 
 
-class ReadOnlyModelViewSet(AutoPrefetchMixin,
-                           RelatedMixin,
-                           viewsets.ReadOnlyModelViewSet):
-    http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
+class ReadOnlyModelViewSet(
+    AutoPrefetchMixin, PreloadIncludesMixin, RelatedMixin, viewsets.ReadOnlyModelViewSet
+):
+    http_method_names = ["get", "post", "patch", "delete", "head", "options"]
 
 
 class RelationshipView(generics.GenericAPIView):
@@ -218,15 +230,15 @@ class RelationshipView(generics.GenericAPIView):
     self_link_view_name = None
     related_link_view_name = None
     field_name_mapping = {}
-    http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
+    http_method_names = ["get", "post", "patch", "delete", "head", "options"]
 
     def get_serializer_class(self):
-        if getattr(self, 'action', False) is None:
+        if getattr(self, "action", False) is None:
             return Serializer
         return self.serializer_class
 
     def __init__(self, **kwargs):
-        super(RelationshipView, self).__init__(**kwargs)
+        super().__init__(**kwargs)
         # We include this simply for dependency injection in tests.
         # We can't add it as a class attributes or it would expect an
         # implicit `self` argument to be passed.
@@ -249,10 +261,10 @@ def get_url(self, name, view_name, kwargs, request):
             url = self.reverse(view_name, kwargs=kwargs, request=request)
         except NoReverseMatch:
             msg = (
-                'Could not resolve URL for hyperlinked relationship using '
+                "Could not resolve URL for hyperlinked relationship using "
                 'view name "%s". You may have failed to include the related '
-                'model in your API, or incorrectly configured the '
-                '`lookup_field` attribute on this field.'
+                "model in your API, or incorrectly configured the "
+                "`lookup_field` attribute on this field."
             )
             raise ImproperlyConfigured(msg % view_name)
 
@@ -262,16 +274,18 @@ def get_url(self, name, view_name, kwargs, request):
         return Hyperlink(url, name)
 
     def get_links(self):
-        return_data = OrderedDict()
-        self_link = self.get_url('self', self.self_link_view_name, self.kwargs, self.request)
+        return_data = {}
+        self_link = self.get_url(
+            "self", self.self_link_view_name, self.kwargs, self.request
+        )
         related_kwargs = {self.lookup_field: self.kwargs.get(self.lookup_field)}
         related_link = self.get_url(
-            'related', self.related_link_view_name, related_kwargs, self.request
+            "related", self.related_link_view_name, related_kwargs, self.request
         )
         if self_link:
-            return_data.update({'self': self_link})
+            return_data["self"] = self_link
         if related_link:
-            return_data.update({'related': related_link})
+            return_data["related"] = related_link
         return return_data
 
     def get(self, request, *args, **kwargs):
@@ -286,7 +300,7 @@ def remove_relationships(self, instance_manager, field):
             for obj in instance_manager.all():
                 setattr(obj, field_object.name, None)
                 obj.save()
-        elif hasattr(instance_manager, 'clear'):
+        elif hasattr(instance_manager, "clear"):
             instance_manager.clear()
         else:
             instance_manager.all().delete()
@@ -307,25 +321,33 @@ def patch(self, request, *args, **kwargs):
             # for to one
             if hasattr(related_instance_or_manager, "field"):
                 related_instance_or_manager = self.remove_relationships(
-                    instance_manager=related_instance_or_manager, field="field")
+                    instance_manager=related_instance_or_manager, field="field"
+                )
             # for to many
             else:
                 related_instance_or_manager = self.remove_relationships(
-                    instance_manager=related_instance_or_manager, field="target_field")
+                    instance_manager=related_instance_or_manager, field="target_field"
+                )
 
             # have to set bulk to False since data isn't saved yet
             class_name = related_instance_or_manager.__class__.__name__
-            if class_name != 'ManyRelatedManager':
+            if class_name != "ManyRelatedManager":
                 related_instance_or_manager.add(*serializer.validated_data, bulk=False)
             else:
                 related_instance_or_manager.add(*serializer.validated_data)
         else:
             related_model_class = related_instance_or_manager.__class__
-            serializer = self.get_serializer(data=request.data, model_class=related_model_class)
+            serializer = self.get_serializer(
+                data=request.data, model_class=related_model_class
+            )
             serializer.is_valid(raise_exception=True)
-            setattr(parent_obj, self.get_related_field_name(), serializer.validated_data)
+            setattr(
+                parent_obj, self.get_related_field_name(), serializer.validated_data
+            )
             parent_obj.save()
-            related_instance_or_manager = self.get_related_instance()  # Refresh instance
+            related_instance_or_manager = (
+                self.get_related_instance()
+            )  # Refresh instance
         result_serializer = self._instantiate_serializer(related_instance_or_manager)
         return Response(result_serializer.data)
 
@@ -338,11 +360,13 @@ def post(self, request, *args, **kwargs):
                 data=request.data, model_class=related_model_class, many=True
             )
             serializer.is_valid(raise_exception=True)
-            if frozenset(serializer.validated_data) <= frozenset(related_instance_or_manager.all()):
+            if frozenset(serializer.validated_data) <= frozenset(
+                related_instance_or_manager.all()
+            ):
                 return Response(status=204)
             related_instance_or_manager.add(*serializer.validated_data)
         else:
-            raise MethodNotAllowed('POST')
+            raise MethodNotAllowed("POST")
         result_serializer = self._instantiate_serializer(related_instance_or_manager)
         return Response(result_serializer.data)
 
@@ -362,11 +386,11 @@ def delete(self, request, *args, **kwargs):
                 related_instance_or_manager.remove(*serializer.validated_data)
             except AttributeError:
                 raise Conflict(
-                    'This object cannot be removed from this relationship without being '
-                    'added to another'
+                    "This object cannot be removed from this relationship without being "
+                    "added to another"
                 )
         else:
-            raise MethodNotAllowed('DELETE')
+            raise MethodNotAllowed("DELETE")
         result_serializer = self._instantiate_serializer(related_instance_or_manager)
         return Response(result_serializer.data)
 
@@ -377,7 +401,9 @@ def get_related_instance(self):
             raise NotFound
 
     def get_related_field_name(self):
-        field_name = self.kwargs['related_field']
+        field_name = self.kwargs["related_field"]
+        field_name = undo_format_link_segment(field_name)
+
         if field_name in self.field_name_mapping:
             return self.field_name_mapping[field_name]
         return field_name
@@ -392,7 +418,7 @@ def _instantiate_serializer(self, instance):
             return self.get_serializer(instance=instance, many=True)
 
     def get_resource_name(self):
-        if not hasattr(self, '_resource_name'):
+        if not hasattr(self, "_resource_name"):
             instance = getattr(self.get_object(), self.get_related_field_name())
             self._resource_name = get_resource_type_from_instance(instance)
         return self._resource_name
diff --git a/setup.cfg b/setup.cfg
index 4f483e02..92606700 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -5,29 +5,36 @@ test = pytest
 universal = 1
 
 [flake8]
-ignore = F405,W504
-max-line-length = 100
+max-line-length = 88
+extend-ignore =
+    # whitespace before ':' - disabled as not PEP8 compliant
+    E203,
+    # line too long, as using bugbear
+    E501
+extend-select =
+    # Line too long. This is a pragmatic equivalent of pycodestyle's E501
+    B950,
+    # Invalid first argument used for method.
+    B902
 exclude =
-    snapshots
     build/lib,
-    docs/conf.py,
-    migrations,
     .eggs
     .tox,
     env
     .venv
 
 [isort]
-indent = 4
+multi_line_output = 3
+include_trailing_comma = True
+force_grid_wrap = 0
+use_parentheses = True
+ensure_newline_before_comments = True
+line_length = 88
 known_first_party = rest_framework_json_api
 # This is to "trick" isort into putting example below DJA imports.
 known_localfolder = example
-line_length = 100
-multi_line_output = 3
 skip=
     build/lib,
-    docs/conf.py,
-    migrations,
     .eggs
     .tox,
     env
diff --git a/setup.py b/setup.py
index 3bf1c728..2a2a28dd 100755
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,4 @@
-#!/usr/bin/env python
-from __future__ import print_function
+#!/usr/bin/env python3
 
 import os
 import re
@@ -7,15 +6,15 @@
 
 from setuptools import setup
 
-needs_wheel = {'bdist_wheel'}.intersection(sys.argv)
-wheel = ['wheel'] if needs_wheel else []
+needs_wheel = {"bdist_wheel"}.intersection(sys.argv)
+wheel = ["wheel"] if needs_wheel else []
 
 
 def read(*paths):
     """
     Build a file path from paths and return the contents.
     """
-    with open(os.path.join(*paths), 'r') as f:
+    with open(os.path.join(*paths)) as f:
         return f.read()
 
 
@@ -23,7 +22,7 @@ def get_version(package):
     """
     Return package version as listed in `__version__` in `init.py`.
     """
-    init_py = open(os.path.join(package, '__init__.py')).read()
+    init_py = open(os.path.join(package, "__init__.py")).read()
     return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
 
 
@@ -31,9 +30,11 @@ def get_packages(package):
     """
     Return root package and all sub-packages.
     """
-    return [dirpath
-            for dirpath, dirnames, filenames in os.walk(package)
-            if os.path.exists(os.path.join(dirpath, '__init__.py'))]
+    return [
+        dirpath
+        for dirpath, dirnames, filenames in os.walk(package)
+        if os.path.exists(os.path.join(dirpath, "__init__.py"))
+    ]
 
 
 def get_package_data(package):
@@ -41,65 +42,78 @@ def get_package_data(package):
     Return all files under the root package, that are not in a
     package themselves.
     """
-    walk = [(dirpath.replace(package + os.sep, '', 1), filenames)
-            for dirpath, dirnames, filenames in os.walk(package)
-            if not os.path.exists(os.path.join(dirpath, '__init__.py'))]
+    walk = [
+        (dirpath.replace(package + os.sep, "", 1), filenames)
+        for dirpath, dirnames, filenames in os.walk(package)
+        if not os.path.exists(os.path.join(dirpath, "__init__.py"))
+    ]
 
     filepaths = []
     for base, filenames in walk:
-        filepaths.extend([os.path.join(base, filename)
-                          for filename in filenames])
+        filepaths.extend([os.path.join(base, filename) for filename in filenames])
     return {package: filepaths}
 
 
-if sys.argv[-1] == 'publish':
+if sys.argv[-1] == "publish":
     os.system("python setup.py sdist upload")
     os.system("python setup.py bdist_wheel upload")
     print("You probably want to also tag the version now:")
-    print("  git tag -a {0} -m 'version {0}'".format(
-        get_version('rest_framework_json_api')))
+    print(
+        "  git tag -a {0} -m 'version {0}'".format(
+            get_version("rest_framework_json_api")
+        )
+    )
     print("  git push --tags")
     sys.exit()
 
 setup(
-    name='djangorestframework-jsonapi',
-    version=get_version('rest_framework_json_api'),
-    url='https://github.com/django-json-api/django-rest-framework-json-api',
-    license='BSD',
-    description='A Django REST framework API adapter for the JSON API spec.',
-    long_description=read('README.rst'),
-    author='Jerel Unruh',
-    author_email='',
-    packages=get_packages('rest_framework_json_api'),
-    package_data=get_package_data('rest_framework_json_api'),
+    name="djangorestframework-jsonapi",
+    version=get_version("rest_framework_json_api"),
+    url="https://github.com/django-json-api/django-rest-framework-json-api",
+    license="BSD",
+    description="A Django REST framework API adapter for the JSON:API spec.",
+    long_description=read("README.rst"),
+    author="Jerel Unruh",
+    author_email="",
+    packages=get_packages("rest_framework_json_api"),
+    package_data=get_package_data("rest_framework_json_api"),
     classifiers=[
-        'Development Status :: 5 - Production/Stable',
-        'Environment :: Web Environment',
-        'Framework :: Django',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: BSD License',
-        'Operating System :: OS Independent',
-        'Programming Language :: Python',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.6',
-        'Programming Language :: Python :: 3.7',
-        'Programming Language :: Python :: 3.8',
-        'Programming Language :: Python :: 3.9',
-        'Topic :: Internet :: WWW/HTTP',
-        'Topic :: Software Development :: Libraries :: Application Frameworks',
-        'Topic :: Software Development :: Libraries :: Python Modules',
+        "Development Status :: 5 - Production/Stable",
+        "Environment :: Web Environment",
+        "Framework :: Django",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: BSD License",
+        "Operating System :: OS Independent",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3.12",
+        "Programming Language :: Python :: 3.13",
+        "Topic :: Internet :: WWW/HTTP",
+        "Topic :: Software Development :: Libraries :: Application Frameworks",
+        "Topic :: Software Development :: Libraries :: Python Modules",
     ],
+    project_urls={
+        "Documentation": "https://django-rest-framework-json-api.readthedocs.org/",
+        "Changelog": (
+            "https://github.com/django-json-api/django-rest-framework-json-api/"
+            "blob/main/CHANGELOG.md"
+        ),
+        "Source": "https://github.com/django-json-api/django-rest-framework-json-api",
+        "Tracker": "https://github.com/django-json-api/django-rest-framework-json-api/issues",
+    },
     install_requires=[
-        'inflection>=0.3.0',
-        'djangorestframework>=3.12,<3.13',
-        'django>=2.2,<3.2',
+        "inflection>=0.5.0",
+        "djangorestframework>=3.15",
+        "django>=4.2",
     ],
     extras_require={
-        'django-polymorphic': ['django-polymorphic>=2.0'],
-        'django-filter': ['django-filter>=2.0'],
-        'openapi': ['pyyaml>=5.3', 'uritemplate>=3.0.1']
+        "django-polymorphic": ["django-polymorphic>=4.0.0"],
+        "django-filter": ["django-filter>=2.4"],
     },
     setup_requires=wheel,
-    python_requires=">=3.6",
+    python_requires=">=3.9",
     zip_safe=False,
 )
diff --git a/tests/conftest.py b/tests/conftest.py
index 4942bf63..77b3676b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,15 @@
 import pytest
+from rest_framework.test import APIClient, APIRequestFactory
+
+from tests.models import (
+    BasicModel,
+    ForeignKeySource,
+    ForeignKeyTarget,
+    ManyToManySource,
+    ManyToManyTarget,
+    NestedRelatedSource,
+    URLModel,
+)
 
 
 @pytest.fixture(autouse=True)
@@ -19,3 +30,76 @@ def use_rest_framework_json_api_defaults(settings):
     settings.JSON_API_FORMAT_FIELD_NAMES = False
     settings.JSON_API_FORMAT_TYPES = False
     settings.JSON_API_PLURALIZE_TYPES = False
+
+
+@pytest.fixture
+def model(db):
+    return BasicModel.objects.create(text="Model")
+
+
+@pytest.fixture
+def url_instance(db):
+    return URLModel.objects.create(text="Url", url="https://example.com")
+
+
+@pytest.fixture
+def foreign_key_target(db):
+    return ForeignKeyTarget.objects.create(name="Target")
+
+
+@pytest.fixture
+def foreign_key_source(db, foreign_key_target):
+    return ForeignKeySource.objects.create(name="Source", target=foreign_key_target)
+
+
+@pytest.fixture
+def many_to_many_source(db, many_to_many_targets):
+    source = ManyToManySource.objects.create(name="Source")
+    source.targets.add(*many_to_many_targets)
+    return source
+
+
+@pytest.fixture
+def many_to_many_targets(db):
+    return [
+        ManyToManyTarget.objects.create(name="Target1"),
+        ManyToManyTarget.objects.create(name="Target2"),
+    ]
+
+
+@pytest.fixture
+def many_to_many_sources(db, many_to_many_targets):
+    source1 = ManyToManySource.objects.create(name="Source1")
+    source2 = ManyToManySource.objects.create(name="Source2")
+
+    source1.targets.add(*many_to_many_targets)
+    source2.targets.add(*many_to_many_targets)
+
+    return [source1, source2]
+
+
+@pytest.fixture
+def nested_related_source(
+    db,
+    foreign_key_source,
+    foreign_key_target,
+    many_to_many_targets,
+    many_to_many_sources,
+):
+    source = NestedRelatedSource.objects.create(
+        fk_source=foreign_key_source, fk_target=foreign_key_target
+    )
+    source.m2m_targets.add(*many_to_many_targets)
+    source.m2m_sources.add(*many_to_many_sources)
+
+    return source
+
+
+@pytest.fixture
+def client():
+    return APIClient()
+
+
+@pytest.fixture
+def rf():
+    return APIRequestFactory()
diff --git a/tests/models.py b/tests/models.py
new file mode 100644
index 00000000..63cb6434
--- /dev/null
+++ b/tests/models.py
@@ -0,0 +1,60 @@
+from django.db import models
+
+
+class DJAModel(models.Model):
+    """
+    Base for test models that sets app_label, so they play nicely.
+    """
+
+    class Meta:
+        app_label = "tests"
+        abstract = True
+
+
+class BasicModel(DJAModel):
+    text = models.CharField(max_length=100)
+
+    class Meta:
+        ordering = ("id",)
+
+
+class URLModel(DJAModel):
+    url = models.URLField()
+    text = models.CharField(max_length=100)
+
+    class Meta:
+        ordering = ("id",)
+
+
+# Models for relations tests
+# ManyToMany
+class ManyToManyTarget(DJAModel):
+    name = models.CharField(max_length=100)
+
+
+class ManyToManySource(DJAModel):
+    name = models.CharField(max_length=100)
+    targets = models.ManyToManyField(ManyToManyTarget, related_name="sources")
+
+
+# ForeignKey
+class ForeignKeyTarget(DJAModel):
+    name = models.CharField(max_length=100)
+
+
+class ForeignKeySource(DJAModel):
+    name = models.CharField(max_length=100)
+    target = models.ForeignKey(
+        ForeignKeyTarget, related_name="sources", on_delete=models.CASCADE
+    )
+
+
+class NestedRelatedSource(DJAModel):
+    m2m_sources = models.ManyToManyField(ManyToManySource, related_name="nested_source")
+    fk_source = models.ForeignKey(
+        ForeignKeySource, related_name="nested_source", on_delete=models.CASCADE
+    )
+    m2m_targets = models.ManyToManyField(ManyToManyTarget, related_name="nested_target")
+    fk_target = models.ForeignKey(
+        ForeignKeyTarget, related_name="nested_target", on_delete=models.CASCADE
+    )
diff --git a/tests/serializers.py b/tests/serializers.py
new file mode 100644
index 00000000..ef8a51cf
--- /dev/null
+++ b/tests/serializers.py
@@ -0,0 +1,94 @@
+from rest_framework.settings import api_settings
+
+from rest_framework_json_api import serializers
+from tests.models import (
+    BasicModel,
+    ForeignKeySource,
+    ForeignKeyTarget,
+    ManyToManySource,
+    ManyToManyTarget,
+    NestedRelatedSource,
+    URLModel,
+)
+
+
+class BasicModelSerializer(serializers.ModelSerializer):
+    class Meta:
+        fields = ("text",)
+        model = BasicModel
+
+
+class URLModelSerializer(serializers.ModelSerializer):
+    class Meta:
+        fields = (
+            "text",
+            "url",
+        )
+        model = URLModel
+
+
+class ForeignKeyTargetSerializer(serializers.ModelSerializer):
+    class Meta:
+        fields = ("name",)
+        model = ForeignKeyTarget
+
+
+class ForeignKeySourceSerializer(serializers.ModelSerializer):
+    included_serializers = {"target": ForeignKeyTargetSerializer}
+
+    class Meta:
+        model = ForeignKeySource
+        fields = (
+            "name",
+            "target",
+        )
+
+
+class ForeignKeySourcetHyperlinkedSerializer(serializers.HyperlinkedModelSerializer):
+    class Meta:
+        model = ForeignKeySource
+        fields = (
+            "name",
+            "target",
+            api_settings.URL_FIELD_NAME,
+        )
+
+
+class ManyToManyTargetSerializer(serializers.ModelSerializer):
+    class Meta:
+        fields = ("name",)
+        model = ManyToManyTarget
+
+
+class ManyToManySourceSerializer(serializers.ModelSerializer):
+    included_serializers = {"targets": "tests.serializers.ManyToManyTargetSerializer"}
+
+    class Meta:
+        model = ManyToManySource
+        fields = ("targets",)
+
+
+class ManyToManySourceReadOnlySerializer(serializers.ModelSerializer):
+    class Meta:
+        model = ManyToManySource
+        fields = ("targets",)
+
+
+class NestedRelatedSourceSerializer(serializers.ModelSerializer):
+    included_serializers = {
+        "m2m_sources": ManyToManySourceSerializer,
+        "fk_source": ForeignKeySourceSerializer,
+        "m2m_targets": ManyToManyTargetSerializer,
+        "fk_target": ForeignKeyTargetSerializer,
+    }
+
+    class Meta:
+        model = NestedRelatedSource
+        fields = ("m2m_sources", "fk_source", "m2m_targets", "fk_target")
+
+
+class CallableDefaultSerializer(serializers.Serializer):
+    field = serializers.CharField(default=serializers.CreateOnlyDefault("default"))
+
+    class Meta:
+        fields = ("field",)
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
new file mode 100644
index 00000000..10f0ebbf
--- /dev/null
+++ b/tests/test_pagination.py
@@ -0,0 +1,43 @@
+from rest_framework.request import Request
+
+from rest_framework_json_api.pagination import JsonApiLimitOffsetPagination
+
+
+class TestLimitOffsetPagination:
+    def test_get_paginated_response(self, rf):
+        pagination = JsonApiLimitOffsetPagination()
+        queryset = range(1, 101)
+        offset = 10
+        limit = 5
+        count = len(queryset)
+
+        request = Request(
+            rf.get(
+                "/",
+                {
+                    pagination.limit_query_param: limit,
+                    pagination.offset_query_param: offset,
+                },
+            )
+        )
+        queryset = list(pagination.paginate_queryset(queryset, request))
+        content = pagination.get_paginated_response(queryset).data
+
+        expected_content = {
+            "results": list(range(11, 16)),
+            "links": {
+                "first": "http://testserver/?page%5Blimit%5D=5",
+                "last": "http://testserver/?page%5Blimit%5D=5&page%5Boffset%5D=100",
+                "next": "http://testserver/?page%5Blimit%5D=5&page%5Boffset%5D=15",
+                "prev": "http://testserver/?page%5Blimit%5D=5&page%5Boffset%5D=5",
+            },
+            "meta": {
+                "pagination": {
+                    "count": count,
+                    "limit": limit,
+                    "offset": offset,
+                }
+            },
+        }
+
+        assert content == expected_content
diff --git a/tests/test_parsers.py b/tests/test_parsers.py
new file mode 100644
index 00000000..45ac5232
--- /dev/null
+++ b/tests/test_parsers.py
@@ -0,0 +1,144 @@
+import json
+from io import BytesIO
+
+import pytest
+from rest_framework.exceptions import ParseError
+
+from rest_framework_json_api.parsers import JSONParser
+from rest_framework_json_api.utils import format_value
+from tests.views import BasicModelViewSet
+
+
+class TestJSONParser:
+    @pytest.fixture
+    def parser(self):
+        return JSONParser()
+
+    @pytest.fixture
+    def parse(self, parser):
+        def parse_wrapper(data, parser_context):
+            stream = BytesIO(json.dumps(data).encode("utf-8"))
+            return parser.parse(stream, None, parser_context)
+
+        return parse_wrapper
+
+    @pytest.fixture
+    def parser_context(self, rf):
+        return {"request": rf.post("/"), "kwargs": {}, "view": BasicModelViewSet()}
+
+    @pytest.mark.parametrize(
+        "format_field_names",
+        [
+            False,
+            "dasherize",
+            "camelize",
+            "capitalize",
+            "underscore",
+        ],
+    )
+    def test_parse_formats_field_names(
+        self,
+        settings,
+        format_field_names,
+        parse,
+        parser_context,
+    ):
+        settings.JSON_API_FORMAT_FIELD_NAMES = format_field_names
+
+        data = {
+            "data": {
+                "id": "123",
+                "type": "BasicModel",
+                "attributes": {
+                    format_value("test_attribute", format_field_names): "test-value"
+                },
+                "relationships": {
+                    format_value("test_relationship", format_field_names): {
+                        "data": {"type": "TestRelationship", "id": "123"}
+                    }
+                },
+            }
+        }
+
+        result = parse(data, parser_context)
+        assert result == {
+            "id": "123",
+            "type": "BasicModel",
+            "test_attribute": "test-value",
+            "test_relationship": {"id": "123", "type": "TestRelationship"},
+        }
+
+    def test_parse_extracts_meta(self, parse, parser_context):
+        data = {
+            "data": {
+                "type": "BasicModel",
+            },
+            "meta": {"random_key": "random_value"},
+        }
+
+        result = parse(data, parser_context)
+        assert result["_meta"] == data["meta"]
+
+    def test_parse_with_default_arguments(self, parse):
+        data = {
+            "data": {
+                "type": "BasicModel",
+            },
+        }
+        result = parse(data, None)
+        assert result == {"type": "BasicModel"}
+
+    def test_parse_preserves_json_value_field_names(
+        self, settings, parse, parser_context
+    ):
+        settings.JSON_API_FORMAT_FIELD_NAMES = "dasherize"
+
+        data = {
+            "data": {
+                "type": "BasicModel",
+                "attributes": {"json-value": {"JsonKey": "JsonValue"}},
+            },
+        }
+
+        result = parse(data, parser_context)
+        assert result["json_value"] == {"JsonKey": "JsonValue"}
+
+    def test_parse_raises_error_on_empty_data(self, parse, parser_context):
+        data = []
+
+        with pytest.raises(ParseError) as excinfo:
+            parse(data, parser_context)
+        assert "Received document does not contain primary data" == str(excinfo.value)
+
+    def test_parse_fails_on_list_of_objects(self, parse, parser_context):
+        data = {
+            "data": [
+                {
+                    "type": "BasicModel",
+                    "attributes": {"json-value": {"JsonKey": "JsonValue"}},
+                }
+            ],
+        }
+
+        with pytest.raises(ParseError) as excinfo:
+            parse(data, parser_context)
+
+        assert (
+            "Received data is not a valid JSON:API Resource Identifier Object"
+            == str(excinfo.value)
+        )
+
+    def test_parse_fails_when_id_is_missing_on_patch(self, rf, parse, parser_context):
+        parser_context["request"] = rf.patch("/")
+        data = {
+            "data": {
+                "type": "BasicModel",
+            },
+        }
+
+        with pytest.raises(ParseError) as excinfo:
+            parse(data, parser_context)
+
+        assert "The resource identifier object must contain an 'id' member" == str(
+            excinfo.value
+        )
diff --git a/tests/test_relations.py b/tests/test_relations.py
new file mode 100644
index 00000000..5f8883be1
--- /dev/null
+++ b/tests/test_relations.py
@@ -0,0 +1,320 @@
+import pytest
+from django.urls import re_path
+from rest_framework import status
+from rest_framework.fields import SkipField
+from rest_framework.routers import SimpleRouter
+from rest_framework.serializers import Serializer
+
+from rest_framework_json_api.exceptions import Conflict
+from rest_framework_json_api.relations import (
+    HyperlinkedRelatedField,
+    SerializerMethodHyperlinkedRelatedField,
+)
+from rest_framework_json_api.serializers import ModelSerializer, ResourceRelatedField
+from rest_framework_json_api.utils import format_link_segment
+from rest_framework_json_api.views import RelationshipView
+from tests.models import BasicModel, ForeignKeySource, ForeignKeyTarget
+from tests.serializers import (
+    ForeignKeySourceSerializer,
+    ManyToManySourceReadOnlySerializer,
+    ManyToManySourceSerializer,
+)
+from tests.views import BasicModelViewSet
+
+
+@pytest.mark.django_db
+class TestResourceRelatedField:
+    @pytest.mark.parametrize(
+        "format_type,pluralize_type,resource_type",
+        [
+            (False, False, "ForeignKeyTarget"),
+            (False, True, "ForeignKeyTargets"),
+            ("dasherize", False, "foreign-key-target"),
+            ("dasherize", True, "foreign-key-targets"),
+        ],
+    )
+    def test_serialize(
+        self, format_type, pluralize_type, resource_type, foreign_key_target, settings
+    ):
+        settings.JSON_API_FORMAT_TYPES = format_type
+        settings.JSON_API_PLURALIZE_TYPES = pluralize_type
+
+        serializer = ForeignKeySourceSerializer(
+            instance={"target": foreign_key_target, "name": "Test"}
+        )
+        expected = {
+            "type": resource_type,
+            "id": str(foreign_key_target.pk),
+        }
+
+        assert serializer.data["target"] == expected
+
+    def test_get_resource_id(self, foreign_key_target):
+        class CustomResourceRelatedField(ResourceRelatedField):
+            def get_resource_id(self, value):
+                return value.name
+
+        class CustomPkFieldSerializer(ModelSerializer):
+            target = CustomResourceRelatedField(
+                queryset=ForeignKeyTarget.objects, pk_field="name"
+            )
+
+            class Meta:
+                model = ForeignKeySource
+                fields = ("target",)
+
+        serializer = CustomPkFieldSerializer(instance={"target": foreign_key_target})
+        expected = {
+            "type": "ForeignKeyTarget",
+            "id": "Target",
+        }
+
+        assert serializer.data["target"] == expected
+
+    @pytest.mark.parametrize(
+        "format_type,pluralize_type,resource_type",
+        [
+            (False, False, "ForeignKeyTarget"),
+            (False, True, "ForeignKeyTargets"),
+            ("dasherize", False, "foreign-key-target"),
+            ("dasherize", True, "foreign-key-targets"),
+        ],
+    )
+    def test_deserialize(
+        self, format_type, pluralize_type, resource_type, foreign_key_target, settings
+    ):
+        settings.JSON_API_FORMAT_TYPES = format_type
+        settings.JSON_API_PLURALIZE_TYPES = pluralize_type
+
+        serializer = ForeignKeySourceSerializer(
+            data={
+                "target": {"type": resource_type, "id": str(foreign_key_target.pk)},
+                "name": "Test",
+            }
+        )
+
+        assert serializer.is_valid()
+        assert serializer.validated_data["target"] == foreign_key_target
+
+    @pytest.mark.parametrize(
+        "format_type,pluralize_type,resource_type",
+        [
+            (False, False, "ForeignKeyTargets"),
+            (False, False, "Invalid"),
+            (False, False, "foreign-key-target"),
+            (False, True, "ForeignKeyTarget"),
+            ("dasherize", False, "ForeignKeyTarget"),
+            ("dasherize", True, "ForeignKeyTargets"),
+        ],
+    )
+    def test_validation_fails_on_invalid_type(
+        self, format_type, pluralize_type, resource_type, foreign_key_target, settings
+    ):
+        settings.JSON_API_FORMAT_TYPES = format_type
+        settings.JSON_API_PLURALIZE_TYPES = pluralize_type
+
+        with pytest.raises(Conflict) as e:
+            serializer = ForeignKeySourceSerializer(
+                data={
+                    "target": {"type": resource_type, "id": str(foreign_key_target.pk)}
+                }
+            )
+            serializer.is_valid()
+        assert e.value.status_code == status.HTTP_409_CONFLICT
+
+    @pytest.mark.parametrize(
+        "format_type,pluralize_type,resource_type",
+        [
+            (False, False, "ManyToManyTarget"),
+            (False, True, "ManyToManyTargets"),
+            ("dasherize", False, "many-to-many-target"),
+            ("dasherize", True, "many-to-many-targets"),
+        ],
+    )
+    def test_serialize_many_to_many_relation(
+        self,
+        format_type,
+        pluralize_type,
+        resource_type,
+        many_to_many_source,
+        many_to_many_targets,
+        settings,
+    ):
+        settings.JSON_API_FORMAT_TYPES = format_type
+        settings.JSON_API_PLURALIZE_TYPES = pluralize_type
+
+        serializer = ManyToManySourceSerializer(instance=many_to_many_source)
+        expected = [
+            {"type": resource_type, "id": str(target.pk)}
+            for target in many_to_many_targets
+        ]
+        assert serializer.data["targets"] == expected
+
+    @pytest.mark.parametrize(
+        "format_type,pluralize_type,resource_type",
+        [
+            (False, False, "ManyToManyTarget"),
+            (False, True, "ManyToManyTargets"),
+            ("dasherize", False, "many-to-many-target"),
+            ("dasherize", True, "many-to-many-targets"),
+        ],
+    )
+    @pytest.mark.parametrize(
+        "serializer_class",
+        [ManyToManySourceSerializer, ManyToManySourceReadOnlySerializer],
+    )
+    def test_deserialize_many_to_many_relation(
+        self,
+        format_type,
+        pluralize_type,
+        resource_type,
+        serializer_class,
+        many_to_many_targets,
+        settings,
+    ):
+        settings.JSON_API_FORMAT_TYPES = format_type
+        settings.JSON_API_PLURALIZE_TYPES = pluralize_type
+
+        targets = [
+            {"type": resource_type, "id": target.pk} for target in many_to_many_targets
+        ]
+        serializer = ManyToManySourceSerializer(data={"targets": targets})
+        assert serializer.is_valid()
+        assert serializer.validated_data["targets"] == many_to_many_targets
+
+    @pytest.mark.parametrize(
+        "resource_identifier,error",
+        [
+            (
+                {"type": "ForeignKeyTarget"},
+                "Invalid resource identifier object: missing 'id' attribute",
+            ),
+            (
+                {"id": "1234"},
+                "Invalid resource identifier object: missing 'type' attribute",
+            ),
+        ],
+    )
+    def test_invalid_resource_id_object(self, resource_identifier, error):
+        serializer = ForeignKeySourceSerializer(
+            data={"target": resource_identifier, "name": "Test"}
+        )
+        assert not serializer.is_valid()
+        assert serializer.errors == {"target": [error]}
+
+
+class TestHyperlinkedRelatedField:
+    @pytest.fixture
+    def instance(self):
+        # dummy instance
+        return object()
+
+    @pytest.fixture
+    def serializer(self):
+        class HyperlinkedRelatedFieldSerializer(Serializer):
+            single = HyperlinkedRelatedField(
+                self_link_view_name="basic-model-relationships",
+                related_link_view_name="basic-model-related",
+                read_only=True,
+            )
+            many = HyperlinkedRelatedField(
+                self_link_view_name="basic-model-relationships",
+                related_link_view_name="basic-model-related",
+                read_only=True,
+                many=True,
+            )
+            single_serializer_method = SerializerMethodHyperlinkedRelatedField(
+                self_link_view_name="basic-model-relationships",
+                related_link_view_name="basic-model-related",
+                read_only=True,
+            )
+            many_serializer_method = SerializerMethodHyperlinkedRelatedField(
+                self_link_view_name="basic-model-relationships",
+                related_link_view_name="basic-model-related",
+                read_only=True,
+                many=True,
+            )
+
+            def get_single_serializer_method(self, obj):  # pragma: no cover
+                raise NotImplementedError
+
+            def get_many_serializer_method(self, obj):  # pragma: no cover
+                raise NotImplementedError
+
+        return HyperlinkedRelatedFieldSerializer()
+
+    @pytest.fixture(
+        params=["single", "many", "single_serializer_method", "many_serializer_method"]
+    )
+    def field(self, serializer, request):
+        field = serializer.fields[request.param]
+        field.field_name = request.param
+        return field
+
+    def test_get_attribute(self, model, field):
+        with pytest.raises(SkipField):
+            field.get_attribute(model)
+
+    def test_to_representation(self, model, field):
+        with pytest.raises(NotImplementedError):
+            field.to_representation(model)
+
+    @pytest.mark.urls(__name__)
+    @pytest.mark.parametrize(
+        "format_related_links",
+        [
+            False,
+            "dasherize",
+            "camelize",
+            "capitalize",
+            "underscore",
+        ],
+    )
+    def test_get_links(
+        self,
+        format_related_links,
+        field,
+        settings,
+        model,
+    ):
+        settings.JSON_API_FORMAT_RELATED_LINKS = format_related_links
+
+        link_segment = format_link_segment(field.field_name)
+
+        expected = {
+            "self": f"/basic_models/{model.pk}/relationships/{link_segment}/",
+            "related": f"/basic_models/{model.pk}/{link_segment}/",
+        }
+
+        if hasattr(field, "child_relation"):
+            # many case
+            field = field.child_relation
+
+        actual = field.get_links(model)
+        assert expected == actual
+
+
+# Routing setup
+
+
+class BasicModelRelationshipView(RelationshipView):
+    queryset = BasicModel.objects
+
+
+router = SimpleRouter()
+router.register(r"basic_models", BasicModelViewSet, basename="basic-model")
+
+urlpatterns = [
+    re_path(
+        r"^basic_models/(?P<pk>[^/.]+)/(?P<related_field>[^/.]+)/$",
+        BasicModelViewSet.as_view({"get": "retrieve_related"}),
+        name="basic-model-related",
+    ),
+    re_path(
+        r"^basic_models/(?P<pk>[^/.]+)/relationships/(?P<related_field>[^/.]+)/$",
+        BasicModelRelationshipView.as_view(),
+        name="basic-model-relationships",
+    ),
+]
+
+urlpatterns += router.urls
diff --git a/tests/test_serializers.py b/tests/test_serializers.py
new file mode 100644
index 00000000..98cb2850
--- /dev/null
+++ b/tests/test_serializers.py
@@ -0,0 +1,106 @@
+import pytest
+from django.db import models
+from rest_framework.request import Request
+from rest_framework.utils import model_meta
+
+from rest_framework_json_api import serializers
+from tests.models import DJAModel, ManyToManyTarget
+from tests.serializers import ManyToManyTargetSerializer
+
+
+def test_get_included_serializers():
+    class IncludedSerializersModel(DJAModel):
+        self = models.ForeignKey("self", on_delete=models.CASCADE)
+        target = models.ForeignKey(ManyToManyTarget, on_delete=models.CASCADE)
+        other_target = models.ForeignKey(ManyToManyTarget, on_delete=models.CASCADE)
+
+        class Meta:
+            app_label = "tests"
+
+    class IncludedSerializersSerializer(serializers.ModelSerializer):
+        included_serializers = {
+            "self": "self",
+            "target": ManyToManyTargetSerializer,
+            "other_target": "tests.serializers.ManyToManyTargetSerializer",
+        }
+
+        class Meta:
+            model = IncludedSerializersModel
+            fields = ("self", "other_target", "target")
+
+    included_serializers = IncludedSerializersSerializer.included_serializers
+    expected_included_serializers = {
+        "self": IncludedSerializersSerializer,
+        "target": ManyToManyTargetSerializer,
+        "other_target": ManyToManyTargetSerializer,
+    }
+
+    assert included_serializers == expected_included_serializers
+
+
+def test_reserved_field_names():
+    with pytest.raises(AssertionError) as e:
+
+        class ReservedFieldNamesSerializer(serializers.Serializer):
+            meta = serializers.CharField()
+            results = serializers.CharField()
+
+        ReservedFieldNamesSerializer().fields
+
+    assert str(e.value) == (
+        "Serializer class tests.test_serializers.test_reserved_field_names.<locals>."
+        "ReservedFieldNamesSerializer uses following reserved field name(s) which is "
+        "not allowed: meta, results"
+    )
+
+
+def test_get_field_names():
+    class MyTestModel(DJAModel):
+        verified = models.BooleanField(default=False)
+        uuid = models.UUIDField()
+
+    class AnotherSerializer(serializers.Serializer):
+        ref_id = serializers.CharField()
+        reference_string = serializers.CharField()
+
+    class MyTestModelSerializer(AnotherSerializer, serializers.ModelSerializer):
+        an_extra_field = serializers.CharField()
+
+        class Meta:
+            model = MyTestModel
+            fields = "__all__"
+            extra_kwargs = {
+                "verified": {"read_only": True},
+            }
+
+    # Same logic than in DRF get_fields() method
+    declared_fields = MyTestModelSerializer._declared_fields
+    info = model_meta.get_field_info(MyTestModel)
+
+    assert MyTestModelSerializer().get_field_names(declared_fields, info) == [
+        "id",
+        "ref_id",
+        "reference_string",
+        "an_extra_field",
+        "verified",
+        "uuid",
+    ]
+
+
+def test_readable_fields_with_sparse_fields(client, rf, settings):
+    class TestSerializer(serializers.Serializer):
+        name = serializers.CharField()
+        value = serializers.CharField()
+        multi_part_name = serializers.CharField()
+
+        class Meta:
+            resource_name = "test"
+
+    settings.JSON_API_FORMAT_FIELD_NAMES = "camelize"
+    request = Request(rf.get("/test/", {"fields[test]": "value,multiPartName"}))
+    context = {"request": request}
+    serializer = TestSerializer(context=context)
+    assert [field.field_name for field in serializer._readable_fields] == [
+        "value",
+        "multi_part_name",
+    ]
diff --git a/example/tests/unit/test_settings.py b/tests/test_settings.py
similarity index 72%
rename from example/tests/unit/test_settings.py
rename to tests/test_settings.py
index e6b82a24..666eae4a 100644
--- a/example/tests/unit/test_settings.py
+++ b/tests/test_settings.py
@@ -13,5 +13,5 @@ def test_settings_default():
 
 
 def test_settings_override(settings):
-    settings.JSON_API_FORMAT_FIELD_NAMES = 'dasherize'
-    assert json_api_settings.FORMAT_FIELD_NAMES == 'dasherize'
+    settings.JSON_API_FORMAT_FIELD_NAMES = "dasherize"
+    assert json_api_settings.FORMAT_FIELD_NAMES == "dasherize"
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 00000000..08e36b6a
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,479 @@
+import pytest
+from rest_framework import status
+from rest_framework.fields import Field
+from rest_framework.generics import GenericAPIView
+from rest_framework.request import Request
+from rest_framework.response import Response
+from rest_framework.views import APIView
+
+from rest_framework_json_api import serializers
+from rest_framework_json_api.utils import (
+    format_error_object,
+    format_field_name,
+    format_field_names,
+    format_link_segment,
+    format_resource_type,
+    format_value,
+    get_included_resources,
+    get_related_resource_type,
+    get_resource_id,
+    get_resource_name,
+    get_resource_type_from_serializer,
+    undo_format_field_name,
+    undo_format_field_names,
+    undo_format_link_segment,
+)
+from tests.models import (
+    BasicModel,
+    ForeignKeySource,
+    ForeignKeyTarget,
+    ManyToManySource,
+    ManyToManyTarget,
+    NestedRelatedSource,
+)
+from tests.serializers import BasicModelSerializer
+
+
+def test_get_resource_name_no_view():
+    assert get_resource_name({}) is None
+
+
+@pytest.mark.parametrize(
+    "format_type,pluralize_type,output",
+    [
+        (False, False, "APIView"),
+        (False, True, "APIViews"),
+        ("dasherize", False, "api-view"),
+        ("dasherize", True, "api-views"),
+    ],
+)
+def test_get_resource_name_from_view(settings, format_type, pluralize_type, output):
+    settings.JSON_API_FORMAT_TYPES = format_type
+    settings.JSON_API_PLURALIZE_TYPES = pluralize_type
+
+    view = APIView()
+    context = {"view": view}
+    assert output == get_resource_name(context)
+
+
+@pytest.mark.parametrize(
+    "format_type,pluralize_type",
+    [
+        (False, False),
+        (False, True),
+        ("dasherize", False),
+        ("dasherize", True),
+    ],
+)
+def test_get_resource_name_from_view_custom_resource_name(
+    settings, format_type, pluralize_type
+):
+    settings.JSON_API_FORMAT_TYPES = format_type
+    settings.JSON_API_PLURALIZE_TYPES = pluralize_type
+
+    view = APIView()
+    view.resource_name = "custom"
+    context = {"view": view}
+    assert "custom" == get_resource_name(context)
+
+
+@pytest.mark.parametrize(
+    "format_type,pluralize_type,output",
+    [
+        (False, False, "BasicModel"),
+        (False, True, "BasicModels"),
+        ("dasherize", False, "basic-model"),
+        ("dasherize", True, "basic-models"),
+    ],
+)
+def test_get_resource_name_from_model(settings, format_type, pluralize_type, output):
+    settings.JSON_API_FORMAT_TYPES = format_type
+    settings.JSON_API_PLURALIZE_TYPES = pluralize_type
+
+    view = APIView()
+    view.model = BasicModel
+    context = {"view": view}
+    assert output == get_resource_name(context)
+
+
+@pytest.mark.parametrize(
+    "format_type,pluralize_type,output",
+    [
+        (False, False, "BasicModel"),
+        (False, True, "BasicModels"),
+        ("dasherize", False, "basic-model"),
+        ("dasherize", True, "basic-models"),
+    ],
+)
+def test_get_resource_name_from_model_serializer_class(
+    settings, format_type, pluralize_type, output
+):
+    settings.JSON_API_FORMAT_TYPES = format_type
+    settings.JSON_API_PLURALIZE_TYPES = pluralize_type
+
+    view = GenericAPIView()
+    view.serializer_class = BasicModelSerializer
+    context = {"view": view}
+    assert output == get_resource_name(context)
+
+
+@pytest.mark.parametrize(
+    "format_type,pluralize_type",
+    [
+        (False, False),
+        (False, True),
+        ("dasherize", False),
+        ("dasherize", True),
+    ],
+)
+def test_get_resource_name_from_model_serializer_class_custom_resource_name(
+    settings, format_type, pluralize_type
+):
+    settings.JSON_API_FORMAT_TYPES = format_type
+    settings.JSON_API_PLURALIZE_TYPES = pluralize_type
+
+    view = GenericAPIView()
+    view.serializer_class = BasicModelSerializer
+    view.serializer_class.Meta.resource_name = "custom"
+
+    context = {"view": view}
+    assert "custom" == get_resource_name(context)
+
+
+@pytest.mark.parametrize(
+    "format_type,pluralize_type",
+    [
+        (False, False),
+        (False, True),
+        ("dasherize", False),
+        ("dasherize", True),
+    ],
+)
+def test_get_resource_name_from_plain_serializer_class(
+    settings, format_type, pluralize_type
+):
+    class PlainSerializer(serializers.Serializer):
+        class Meta:
+            resource_name = "custom"
+
+    settings.JSON_API_FORMAT_TYPES = format_type
+    settings.JSON_API_PLURALIZE_TYPES = pluralize_type
+
+    view = GenericAPIView()
+    view.serializer_class = PlainSerializer
+
+    context = {"view": view}
+    assert "custom" == get_resource_name(context)
+
+
+@pytest.mark.parametrize(
+    "status_code",
+    [
+        status.HTTP_400_BAD_REQUEST,
+        status.HTTP_403_FORBIDDEN,
+        status.HTTP_500_INTERNAL_SERVER_ERROR,
+    ],
+)
+def test_get_resource_name_with_errors(status_code):
+    view = APIView()
+    context = {"view": view}
+    view.response = Response(status=status_code)
+    assert "errors" == get_resource_name(context)
+
+
+@pytest.mark.parametrize(
+    "format_type,output",
+    [
+        (False, {"full_name": {"last-name": "a", "first-name": "b"}}),
+        ("camelize", {"fullName": {"last-name": "a", "first-name": "b"}}),
+        ("capitalize", {"FullName": {"last-name": "a", "first-name": "b"}}),
+        ("dasherize", {"full-name": {"last-name": "a", "first-name": "b"}}),
+        ("underscore", {"full_name": {"last-name": "a", "first-name": "b"}}),
+    ],
+)
+def test_format_field_names(settings, format_type, output):
+    settings.JSON_API_FORMAT_FIELD_NAMES = format_type
+
+    value = {"full_name": {"last-name": "a", "first-name": "b"}}
+    assert format_field_names(value, format_type) == output
+
+
+@pytest.mark.parametrize(
+    "format_type,output",
+    [
+        (False, {"fullName": "Test Name"}),
+        ("camelize", {"full_name": "Test Name"}),
+    ],
+)
+def test_undo_format_field_names(settings, format_type, output):
+    settings.JSON_API_FORMAT_FIELD_NAMES = format_type
+
+    value = {"fullName": "Test Name"}
+    assert undo_format_field_names(value) == output
+
+
+@pytest.mark.parametrize(
+    "format_type,output",
+    [
+        (False, "full_name"),
+        ("camelize", "fullName"),
+        ("capitalize", "FullName"),
+        ("dasherize", "full-name"),
+        ("underscore", "full_name"),
+    ],
+)
+def test_format_field_name(settings, format_type, output):
+    settings.JSON_API_FORMAT_FIELD_NAMES = format_type
+
+    field_name = "full_name"
+    assert format_field_name(field_name) == output
+
+
+@pytest.mark.parametrize(
+    "format_type,output",
+    [
+        (False, "fullName"),
+        ("camelize", "full_name"),
+    ],
+)
+def test_undo_format_field_name(settings, format_type, output):
+    settings.JSON_API_FORMAT_FIELD_NAMES = format_type
+
+    field_name = "fullName"
+    assert undo_format_field_name(field_name) == output
+
+
+@pytest.mark.parametrize(
+    "format_type,output",
+    [
+        (False, "first_Name"),
+        ("camelize", "firstName"),
+        ("capitalize", "FirstName"),
+        ("dasherize", "first-name"),
+        ("underscore", "first_name"),
+    ],
+)
+def test_format_link_segment(settings, format_type, output):
+    settings.JSON_API_FORMAT_RELATED_LINKS = format_type
+    assert format_link_segment("first_Name") == output
+
+
+@pytest.mark.parametrize(
+    "format_links,output",
+    [
+        (False, "fullName"),
+        ("camelize", "full_name"),
+    ],
+)
+def test_undo_format_link_segment(settings, format_links, output):
+    settings.JSON_API_FORMAT_RELATED_LINKS = format_links
+
+    link_segment = "fullName"
+    assert undo_format_link_segment(link_segment) == output
+
+
+@pytest.mark.parametrize(
+    "format_type,output",
+    [
+        (False, "first_name"),
+        ("camelize", "firstName"),
+        ("capitalize", "FirstName"),
+        ("dasherize", "first-name"),
+        ("underscore", "first_name"),
+    ],
+)
+def test_format_value(settings, format_type, output):
+    assert format_value("first_name", format_type) == output
+
+
+@pytest.mark.parametrize(
+    "resource_type,pluralize,output",
+    [
+        (None, None, "ResourceType"),
+        ("camelize", False, "resourceType"),
+        ("camelize", True, "resourceTypes"),
+    ],
+)
+def test_format_resource_type(settings, resource_type, pluralize, output):
+    assert format_resource_type("ResourceType", resource_type, pluralize) == output
+
+
+@pytest.mark.parametrize(
+    "model_class,field,output",
+    [
+        (ManyToManySource, "targets", "ManyToManyTarget"),
+        (ManyToManyTarget, "sources", "ManyToManySource"),
+        (ForeignKeySource, "target", "ForeignKeyTarget"),
+        (ForeignKeyTarget, "sources", "ForeignKeySource"),
+    ],
+)
+def test_get_related_resource_type(model_class, field, output):
+    class RelatedResourceTypeSerializer(serializers.ModelSerializer):
+        class Meta:
+            model = model_class
+            fields = (field,)
+
+    serializer = RelatedResourceTypeSerializer()
+    field = serializer.fields[field]
+    assert get_related_resource_type(field) == output
+
+
+@pytest.mark.parametrize(
+    "field,output,related_field_kwargs",
+    [
+        (
+            "m2m_source.targets",
+            "ManyToManyTarget",
+            {"many": True, "queryset": ManyToManyTarget.objects.all()},
+        ),
+        (
+            "m2m_target.sources",
+            "ManyToManySource",
+            {"many": True, "queryset": ManyToManySource.objects.all()},
+        ),
+        (
+            "fk_source.target",
+            "ForeignKeyTarget",
+            {"many": True, "queryset": ForeignKeyTarget.objects.all()},
+        ),
+        (
+            "fk_target.source",
+            "ForeignKeySource",
+            {"many": True, "queryset": ForeignKeySource.objects.all()},
+        ),
+    ],
+)
+def test_get_related_resource_type_from_nested_source(
+    db, field, output, related_field_kwargs
+):
+    class RelatedResourceTypeSerializer(serializers.ModelSerializer):
+        relation = serializers.ResourceRelatedField(
+            source=field, **related_field_kwargs
+        )
+
+        class Meta:
+            model = NestedRelatedSource
+            fields = ("relation",)
+
+    serializer = RelatedResourceTypeSerializer()
+    field = serializer.fields["relation"]
+    assert get_related_resource_type(field) == output
+
+
+@pytest.mark.parametrize(
+    "related_field_kwargs,output",
+    [
+        ({"queryset": BasicModel.objects}, "BasicModel"),
+        ({"queryset": BasicModel.objects, "model": BasicModel}, "BasicModel"),
+        ({"model": BasicModel, "read_only": True}, "BasicModel"),
+    ],
+)
+def test_get_related_resource_type_from_plain_serializer_class(
+    related_field_kwargs, output
+):
+    class PlainRelatedResourceTypeSerializer(serializers.Serializer):
+        basic_models = serializers.ResourceRelatedField(
+            many=True, **related_field_kwargs
+        )
+
+    serializer = PlainRelatedResourceTypeSerializer()
+    field = serializer.fields["basic_models"]
+    assert get_related_resource_type(field) == output
+
+
+def test_get_resource_type_from_serializer_without_resource_name_raises_error():
+    class SerializerWithoutResourceName(serializers.Serializer):
+        something = Field()
+
+    serializer = SerializerWithoutResourceName()
+
+    with pytest.raises(AttributeError) as excinfo:
+        get_resource_type_from_serializer(serializer=serializer)
+    assert str(excinfo.value) == (
+        "can not detect 'resource_name' on serializer "
+        "'SerializerWithoutResourceName' in module 'tests.test_utils'"
+    )
+
+
+@pytest.mark.parametrize(
+    "resource_instance, resource, expected",
+    [
+        (None, None, None),
+        (object(), {}, None),
+        (BasicModel(id=5), None, "5"),
+        (BasicModel(id=9), {}, "9"),
+        (None, {"id": 11}, "11"),
+        (object(), {"pk": 11}, None),
+        (BasicModel(id=6), {"id": 11}, "11"),
+        (BasicModel(id=0), None, "0"),
+        (None, {"id": 0}, "0"),
+        (
+            BasicModel(id=0),
+            {"id": 0},
+            "0",
+        ),
+    ],
+)
+def test_get_resource_id(resource_instance, resource, expected):
+    assert get_resource_id(resource_instance, resource) == expected
+
+
+@pytest.mark.parametrize(
+    "message,pointer,response,result",
+    [
+        # Test that pointer does not get overridden in custom error
+        (
+            {
+                "status": "400",
+                "source": {
+                    "pointer": "/data/custom-pointer",
+                },
+                "meta": {"key": "value"},
+            },
+            "/data/default-pointer",
+            Response(status.HTTP_400_BAD_REQUEST),
+            [
+                {
+                    "status": "400",
+                    "source": {"pointer": "/data/custom-pointer"},
+                    "meta": {"key": "value"},
+                }
+            ],
+        ),
+        # Test that pointer gets added to custom error
+        (
+            {
+                "detail": "custom message",
+            },
+            "/data/default-pointer",
+            Response(status.HTTP_400_BAD_REQUEST),
+            [
+                {
+                    "detail": "custom message",
+                    "source": {"pointer": "/data/default-pointer"},
+                }
+            ],
+        ),
+    ],
+)
+def test_format_error_object(message, pointer, response, result):
+    assert result == format_error_object(message, pointer, response)
+
+
+@pytest.mark.parametrize(
+    "format_type,include_param,expected_includes",
+    [
+        ("dasherize", "author-bio", ["author_bio"]),
+        ("dasherize", "author-bio,author-type", ["author_bio", "author_type"]),
+        ("dasherize", "author-bio.author-type", ["author_bio.author_type"]),
+        ("camelize", "authorBio", ["author_bio"]),
+    ],
+)
+def test_get_included_resources(
+    rf, include_param, expected_includes, format_type, settings
+):
+    settings.JSON_API_FORMAT_FIELD_NAMES = format_type
+
+    request = Request(rf.get("/test/", {"include": include_param}))
+    includes = get_included_resources(request)
+    assert includes == expected_includes
diff --git a/tests/test_views.py b/tests/test_views.py
new file mode 100644
index 00000000..349d37da
--- /dev/null
+++ b/tests/test_views.py
@@ -0,0 +1,574 @@
+import pytest
+from django.urls import path, reverse
+from rest_framework import status
+from rest_framework.decorators import action
+from rest_framework.response import Response
+from rest_framework.routers import SimpleRouter
+from rest_framework.views import APIView
+
+from rest_framework_json_api import serializers
+from rest_framework_json_api.parsers import JSONParser
+from rest_framework_json_api.relations import ResourceRelatedField
+from rest_framework_json_api.renderers import JSONRenderer
+from rest_framework_json_api.utils import format_link_segment
+from rest_framework_json_api.views import ModelViewSet, ReadOnlyModelViewSet
+from tests.models import BasicModel, ForeignKeySource
+from tests.serializers import BasicModelSerializer, ForeignKeyTargetSerializer
+from tests.views import (
+    BasicModelViewSet,
+    ForeignKeySourcetHyperlinkedViewSet,
+    ForeignKeySourceViewSet,
+    ForeignKeyTargetViewSet,
+    ManyToManySourceViewSet,
+    NestedRelatedSourceViewSet,
+    URLModelViewSet,
+)
+
+
+class TestModelViewSet:
+    @pytest.mark.parametrize(
+        "format_links",
+        [
+            False,
+            "dasherize",
+            "camelize",
+            "capitalize",
+            "underscore",
+        ],
+    )
+    def test_get_related_field_name_handles_formatted_link_segments(
+        self, settings, format_links, rf
+    ):
+        settings.JSON_API_FORMAT_RELATED_LINKS = format_links
+
+        # use field name which actually gets formatted
+        related_model_field_name = "related_field_model"
+
+        class RelatedFieldNameSerializer(serializers.ModelSerializer):
+            related_model_field = ResourceRelatedField(queryset=BasicModel.objects)
+
+            def __init__(self, *args, **kwargs):
+                self.related_model_field.field_name = related_model_field_name
+                super().__init(*args, **kwargs)
+
+            class Meta:
+                model = BasicModel
+
+        class RelatedFieldNameView(ModelViewSet):
+            serializer_class = RelatedFieldNameSerializer
+
+        url_segment = format_link_segment(related_model_field_name)
+
+        request = rf.get(f"/basic_models/1/{url_segment}")
+
+        view = RelatedFieldNameView()
+        view.setup(request, related_field=url_segment)
+
+        assert view.get_related_field_name() == related_model_field_name
+
+    @pytest.mark.urls(__name__)
+    def test_list(self, client, model):
+        url = reverse("basic-model-list")
+        response = client.get(url)
+        assert response.status_code == status.HTTP_200_OK
+        assert response.json() == {
+            "data": [
+                {
+                    "type": "BasicModel",
+                    "id": str(model.pk),
+                    "attributes": {"text": "Model"},
+                }
+            ],
+            "links": {
+                "first": "http://testserver/basic_models/?page%5Bnumber%5D=1",
+                "last": "http://testserver/basic_models/?page%5Bnumber%5D=1",
+                "next": None,
+                "prev": None,
+            },
+            "meta": {"pagination": {"count": 1, "page": 1, "pages": 1}},
+        }
+
+    @pytest.mark.urls(__name__)
+    def test_list_with_include_foreign_key(self, client, foreign_key_source):
+        url = reverse("foreignkeysource-list")
+        response = client.get(url, data={"include": "target"})
+        assert response.status_code == status.HTTP_200_OK
+        result = response.json()
+        assert "included" in result
+        assert [
+            {
+                "type": "ForeignKeyTarget",
+                "id": str(foreign_key_source.target.pk),
+                "attributes": {"name": foreign_key_source.target.name},
+            }
+        ] == result["included"]
+
+    @pytest.mark.urls(__name__)
+    def test_list_with_include_many_to_many_field(
+        self, client, many_to_many_source, many_to_many_targets
+    ):
+        url = reverse("many-to-many-source-list")
+        response = client.get(url, data={"include": "targets"})
+        assert response.status_code == status.HTTP_200_OK
+        result = response.json()
+        assert "included" in result
+        assert [
+            {
+                "type": "ManyToManyTarget",
+                "id": str(target.pk),
+                "attributes": {"name": target.name},
+            }
+            for target in many_to_many_targets
+        ] == result["included"]
+
+    @pytest.mark.urls(__name__)
+    def test_list_with_include_nested_related_field(
+        self, client, nested_related_source, many_to_many_sources, many_to_many_targets
+    ):
+        url = reverse("nested-related-source-list")
+        response = client.get(url, data={"include": "m2m_sources,m2m_sources.targets"})
+        assert response.status_code == status.HTTP_200_OK
+        result = response.json()
+        assert "included" in result
+
+        assert [
+            {
+                "type": "ManyToManySource",
+                "id": str(source.pk),
+                "relationships": {
+                    "targets": {
+                        "data": [
+                            {"id": str(target.pk), "type": "ManyToManyTarget"}
+                            for target in source.targets.all()
+                        ],
+                        "meta": {"count": source.targets.count()},
+                    }
+                },
+            }
+            for source in many_to_many_sources
+        ] + [
+            {
+                "type": "ManyToManyTarget",
+                "id": str(target.pk),
+                "attributes": {"name": target.name},
+            }
+            for target in many_to_many_targets
+        ] == result[
+            "included"
+        ]
+
+    @pytest.mark.urls(__name__)
+    def test_list_with_invalid_include(self, client, foreign_key_source):
+        url = reverse("foreignkeysource-list")
+        response = client.get(url, data={"include": "invalid"})
+        assert response.status_code == status.HTTP_400_BAD_REQUEST
+        result = response.json()
+        assert (
+            result["errors"][0]["detail"]
+            == "This endpoint does not support the include parameter for path invalid"
+        )
+
+    @pytest.mark.urls(__name__)
+    def test_list_with_default_included_resources(self, client, foreign_key_source):
+        url = reverse("default-included-resources-list")
+        response = client.get(url)
+        assert response.status_code == status.HTTP_200_OK
+        result = response.json()
+        assert "included" in result
+        assert [
+            {
+                "type": "ForeignKeyTarget",
+                "id": str(foreign_key_source.target.pk),
+                "attributes": {"name": foreign_key_source.target.name},
+            }
+        ] == result["included"]
+
+    @pytest.mark.urls(__name__)
+    def test_list_allow_overwriting_url_field(self, client, url_instance):
+        """
+        Test overwriting of url is possible.
+
+        URL_FIELD_NAME which is set to 'url' per default is used as self in links.
+        However if field is overwritten and not a HyperlinkedIdentityField it should be allowed
+        to use as a attribute as well.
+        """
+
+        url = reverse("urlmodel-list")
+        response = client.get(url)
+        assert response.status_code == status.HTTP_200_OK
+        data = response.json()["data"]
+        assert data == [
+            {
+                "type": "URLModel",
+                "id": str(url_instance.pk),
+                "attributes": {"text": "Url", "url": "https://example.com"},
+            }
+        ]
+
+    @pytest.mark.urls(__name__)
+    def test_list_allow_overwiritng_url_with_sparse_fields(self, client, url_instance):
+        url = reverse("urlmodel-list")
+        response = client.get(url, data={"fields[URLModel]": "text"})
+        assert response.status_code == status.HTTP_200_OK
+        data = response.json()["data"]
+        assert data == [
+            {
+                "type": "URLModel",
+                "id": str(url_instance.pk),
+                "attributes": {"text": "Url"},
+            }
+        ]
+
+    @pytest.mark.urls(__name__)
+    def test_list_with_sparse_fields_empty_value(self, client, model):
+        url = reverse("basic-model-list")
+        response = client.get(url, data={"fields[BasicModel]": ""})
+        assert response.status_code == status.HTTP_200_OK
+        data = response.json()["data"]
+        assert data == [
+            {
+                "type": "BasicModel",
+                "id": str(model.pk),
+            }
+        ]
+
+    @pytest.mark.urls(__name__)
+    def test_retrieve(self, client, model):
+        url = reverse("basic-model-detail", kwargs={"pk": model.pk})
+        response = client.get(url)
+        assert response.status_code == status.HTTP_200_OK
+        assert response.json() == {
+            "data": {
+                "type": "BasicModel",
+                "id": str(model.pk),
+                "attributes": {"text": "Model"},
+            }
+        }
+
+    @pytest.mark.urls(__name__)
+    def test_retrieve_with_include_foreign_key(self, client, foreign_key_source):
+        url = reverse("foreignkeysource-detail", kwargs={"pk": foreign_key_source.pk})
+        response = client.get(url, data={"include": "target"})
+        assert response.status_code == status.HTTP_200_OK
+        result = response.json()
+        assert "included" in result
+        assert [
+            {
+                "type": "ForeignKeyTarget",
+                "id": str(foreign_key_source.target.pk),
+                "attributes": {"name": foreign_key_source.target.name},
+            }
+        ] == result["included"]
+
+    @pytest.mark.urls(__name__)
+    def test_retrieve_hyperlinked_with_sparse_fields(self, client, foreign_key_source):
+        url = reverse(
+            "foreignkeysourcehyperlinked-detail", kwargs={"pk": foreign_key_source.pk}
+        )
+        response = client.get(url, data={"fields[ForeignKeySource]": "name"})
+        assert response.status_code == status.HTTP_200_OK
+        data = response.json()["data"]
+        assert data["attributes"] == {"name": foreign_key_source.name}
+        assert "relationships" not in data
+        assert data["links"] == {
+            "self": f"http://testserver/foreign_key_sources/{foreign_key_source.pk}/"
+        }
+
+    @pytest.mark.urls(__name__)
+    def test_patch(self, client, model):
+        data = {
+            "data": {
+                "id": str(model.pk),
+                "type": "BasicModel",
+                "attributes": {"text": "changed"},
+            }
+        }
+
+        url = reverse("basic-model-detail", kwargs={"pk": model.pk})
+        response = client.patch(url, data=data)
+        assert response.status_code == status.HTTP_200_OK
+        assert response.json() == {
+            "data": {
+                "type": "BasicModel",
+                "id": str(model.pk),
+                "attributes": {"text": "changed"},
+            }
+        }
+
+    @pytest.mark.urls(__name__)
+    def test_delete(self, client, model):
+        url = reverse("basic-model-detail", kwargs={"pk": model.pk})
+        response = client.delete(url)
+        assert response.status_code == status.HTTP_204_NO_CONTENT
+        assert BasicModel.objects.count() == 0
+        assert len(response.rendered_content) == 0
+
+    @pytest.mark.urls(__name__)
+    def test_create_with_sparse_fields(self, client, foreign_key_target):
+        url = reverse("foreignkeysource-list")
+        data = {
+            "data": {
+                "id": None,
+                "type": "ForeignKeySource",
+                "attributes": {"name": "Test"},
+                "relationships": {
+                    "target": {
+                        "data": {
+                            "id": str(foreign_key_target.pk),
+                            "type": "ForeignKeyTarget",
+                        }
+                    }
+                },
+            }
+        }
+        response = client.post(f"{url}?fields[ForeignKeySource]=target", data=data)
+        assert response.status_code == status.HTTP_201_CREATED
+        foreign_key_source = ForeignKeySource.objects.first()
+        assert foreign_key_source.name == "Test"
+        assert response.json() == {
+            "data": {
+                "id": str(foreign_key_source.pk),
+                "type": "ForeignKeySource",
+                "relationships": {
+                    "target": {
+                        "data": {
+                            "id": str(foreign_key_target.pk),
+                            "type": "ForeignKeyTarget",
+                        }
+                    }
+                },
+            }
+        }
+
+
+class TestReadonlyModelViewSet:
+    @pytest.mark.parametrize(
+        "method",
+        ["get", "post", "patch", "delete"],
+    )
+    @pytest.mark.parametrize(
+        "custom_action,action_kwargs",
+        [("list_action", {}), ("detail_action", {"pk": 1})],
+    )
+    def test_custom_action_allows_all_methods(
+        self, rf, method, custom_action, action_kwargs
+    ):
+        """
+        Test that write methods are allowed on custom list actions.
+
+        Even though a read only view only allows reading, custom actions
+        should be allowed to define other methods which are allowed.
+        """
+
+        class ReadOnlyModelViewSetWithCustomActions(ReadOnlyModelViewSet):
+            serializer_class = BasicModelSerializer
+            queryset = BasicModel.objects.all()
+
+            @action(detail=False, methods=["get", "post", "patch", "delete"])
+            def list_action(self, request):
+                return Response(status=status.HTTP_204_NO_CONTENT)
+
+            @action(detail=True, methods=["get", "post", "patch", "delete"])
+            def detail_action(self, request, pk):
+                return Response(status=status.HTTP_204_NO_CONTENT)
+
+        view = ReadOnlyModelViewSetWithCustomActions.as_view({method: custom_action})
+        request = getattr(rf, method)("/", data={})
+        response = view(request, **action_kwargs)
+        assert response.status_code == status.HTTP_204_NO_CONTENT
+
+
+class TestAPIView:
+    @pytest.mark.urls(__name__)
+    def test_patch(self, client):
+        data = {
+            "data": {
+                "id": 123,
+                "type": "custom",
+                "attributes": {"body": "hello"},
+            }
+        }
+
+        url = reverse("custom")
+
+        response = client.patch(url, data=data)
+        assert response.status_code == status.HTTP_200_OK
+        assert response.json() == {
+            "data": {
+                "type": "custom",
+                "id": "123",
+                "attributes": {"body": "hello"},
+            }
+        }
+
+    @pytest.mark.urls(__name__)
+    def test_post_with_missing_id(self, client):
+        data = {
+            "data": {
+                "id": None,
+                "type": "custom",
+                "attributes": {"body": "hello"},
+            }
+        }
+
+        url = reverse("custom")
+
+        response = client.post(url, data=data)
+        assert response.status_code == status.HTTP_200_OK
+        assert response.json() == {
+            "data": {
+                "type": "custom",
+                "id": None,
+                "attributes": {"body": "hello"},
+            }
+        }
+
+    @pytest.mark.urls(__name__)
+    def test_patch_with_custom_id(self, client):
+        data = {
+            "data": {
+                "id": 2_193_102,
+                "type": "custom",
+                "attributes": {"body": "hello"},
+            }
+        }
+
+        url = reverse("custom-id")
+
+        response = client.patch(url, data=data)
+        assert response.status_code == status.HTTP_200_OK
+        assert response.json() == {
+            "data": {
+                "type": "custom",
+                "id": "2176ce",  # get_id() -> hex
+                "attributes": {"body": "hello"},
+            }
+        }
+
+    @pytest.mark.urls(__name__)
+    def test_patch_with_custom_id_with_sparse_fields(self, client):
+        data = {
+            "data": {
+                "id": 2_193_102,
+                "type": "custom",
+                "attributes": {"body": "hello"},
+            }
+        }
+
+        url = reverse("custom-id")
+
+        response = client.patch(f"{url}?fields[custom]=body", data=data)
+        assert response.status_code == status.HTTP_200_OK
+        assert response.json() == {
+            "data": {
+                "type": "custom",
+                "id": "2176ce",  # get_id() -> hex
+                "attributes": {"body": "hello"},
+            }
+        }
+
+
+# Routing setup
+
+
+class DefaultIncludedResourcesSerializer(serializers.ModelSerializer):
+    included_serializers = {"target": ForeignKeyTargetSerializer}
+
+    class Meta:
+        model = ForeignKeySource
+        fields = ("target",)
+
+    class JSONAPIMeta:
+        included_resources = ["target"]
+
+
+class DefaultIncludedResourcesViewSet(ModelViewSet):
+    serializer_class = DefaultIncludedResourcesSerializer
+    queryset = ForeignKeySource.objects.all()
+    ordering = ["id"]
+
+
+class CustomModel:
+    def __init__(self, response_dict):
+        for k, v in response_dict.items():
+            setattr(self, k, v)
+
+    @property
+    def pk(self):
+        return self.id if hasattr(self, "id") else None
+
+
+class CustomModelSerializer(serializers.Serializer):
+    body = serializers.CharField()
+    id = serializers.IntegerField()
+
+
+class CustomIdSerializer(serializers.Serializer):
+    id = serializers.SerializerMethodField()
+    body = serializers.CharField()
+
+    def get_id(self, obj):
+        return hex(obj.id)[2:]
+
+    class Meta:
+        resource_name = "custom"
+
+
+class CustomAPIView(APIView):
+    parser_classes = [JSONParser]
+    renderer_classes = [JSONRenderer]
+    resource_name = "custom"
+
+    def patch(self, request, *args, **kwargs):
+        serializer = CustomModelSerializer(CustomModel(request.data))
+        return Response(status=status.HTTP_200_OK, data=serializer.data)
+
+    def post(self, request, *args, **kwargs):
+        serializer = CustomModelSerializer(request.data)
+        return Response(status=status.HTTP_200_OK, data=serializer.data)
+
+
+class CustomIdAPIView(APIView):
+    parser_classes = [JSONParser]
+    renderer_classes = [JSONRenderer]
+    resource_name = "custom"
+
+    def patch(self, request, *args, **kwargs):
+        serializer = CustomIdSerializer(
+            CustomModel(request.data), context={"request": self.request}
+        )
+        return Response(status=status.HTTP_200_OK, data=serializer.data)
+
+
+# TODO remove basename and use default (lowercase of model)
+# this makes using HyperlinkedIdentityField easier and reduces
+# configuration in general
+router = SimpleRouter()
+router.register(r"basic_models", BasicModelViewSet, basename="basic-model")
+router.register(r"url_models", URLModelViewSet)
+router.register(r"foreign_key_sources", ForeignKeySourceViewSet)
+router.register(r"foreign_key_targets", ForeignKeyTargetViewSet)
+router.register(
+    r"foreign_key_sources_hyperlinked",
+    ForeignKeySourcetHyperlinkedViewSet,
+    "foreignkeysourcehyperlinked",
+)
+router.register(
+    r"many_to_many_sources", ManyToManySourceViewSet, basename="many-to-many-source"
+)
+router.register(
+    r"nested_related_sources",
+    NestedRelatedSourceViewSet,
+    basename="nested-related-source",
+)
+router.register(
+    r"default_included_resources",
+    DefaultIncludedResourcesViewSet,
+    basename="default-included-resources",
+)
+
+urlpatterns = [
+    path("custom", CustomAPIView.as_view(), name="custom"),
+    path("custom-id", CustomIdAPIView.as_view(), name="custom-id"),
+]
+urlpatterns += router.urls
diff --git a/tests/views.py b/tests/views.py
new file mode 100644
index 00000000..7958c6b9
--- /dev/null
+++ b/tests/views.py
@@ -0,0 +1,60 @@
+from rest_framework_json_api.views import ModelViewSet
+from tests.models import (
+    BasicModel,
+    ForeignKeySource,
+    ForeignKeyTarget,
+    ManyToManySource,
+    NestedRelatedSource,
+    URLModel,
+)
+from tests.serializers import (
+    BasicModelSerializer,
+    ForeignKeySourceSerializer,
+    ForeignKeySourcetHyperlinkedSerializer,
+    ForeignKeyTargetSerializer,
+    ManyToManySourceSerializer,
+    NestedRelatedSourceSerializer,
+    URLModelSerializer,
+)
+
+
+class BasicModelViewSet(ModelViewSet):
+    serializer_class = BasicModelSerializer
+    queryset = BasicModel.objects.all()
+    ordering = ["text"]
+
+
+class URLModelViewSet(ModelViewSet):
+    serializer_class = URLModelSerializer
+    queryset = URLModel.objects.all()
+    ordering = ["url"]
+
+
+class ForeignKeySourceViewSet(ModelViewSet):
+    serializer_class = ForeignKeySourceSerializer
+    queryset = ForeignKeySource.objects.all()
+    ordering = ["name"]
+
+
+class ForeignKeySourcetHyperlinkedViewSet(ModelViewSet):
+    serializer_class = ForeignKeySourcetHyperlinkedSerializer
+    queryset = ForeignKeySource.objects.all()
+    ordering = ["name"]
+
+
+class ForeignKeyTargetViewSet(ModelViewSet):
+    serializer_class = ForeignKeyTargetSerializer
+    queryset = ForeignKeyTarget.objects.all()
+    ordering = ["name"]
+
+
+class ManyToManySourceViewSet(ModelViewSet):
+    serializer_class = ManyToManySourceSerializer
+    queryset = ManyToManySource.objects.all()
+    ordering = ["name"]
+
+
+class NestedRelatedSourceViewSet(ModelViewSet):
+    serializer_class = NestedRelatedSourceSerializer
+    queryset = NestedRelatedSource.objects.all()
+    ordering = ["id"]
diff --git a/tox.ini b/tox.ini
index c0ebf760..8dd0e759 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,14 +1,19 @@
 [tox]
 envlist =
-    py{36,37,38,39}-django{22,30,31}-drf{312,master},
-    lint,docs
+    py{39,310,311,312}-django42-drf{315,316,master},
+    py{310,311,312}-django{51,52}-drf{315,316,master},
+    py{313}-django{51,52}-drf{316,master},
+    black,
+    docs,
+    lint
 
 [testenv]
 deps =
-    django22: Django>=2.2,<2.3
-    django30: Django>=3.0,<3.1
-    django31: Django>=3.1,<3.2
-    drf312: djangorestframework>=3.12,<3.13
+    django42: Django>=4.2,<4.3
+    django51: Django>=5.1,<5.2
+    django52: Django>=5.2,<5.3
+    drf315: djangorestframework>=3.15,<3.16
+    drf316: djangorestframework>=3.16,<3.17
     drfmaster: https://github.com/encode/django-rest-framework/archive/master.zip
     -rrequirements/requirements-testing.txt
     -rrequirements/requirements-optionals.txt
@@ -20,8 +25,14 @@ setenv =
 commands =
     pytest --cov --no-cov-on-fail --cov-report xml {posargs}
 
+[testenv:black]
+basepython = python3.10
+deps =
+    -rrequirements/requirements-codestyle.txt
+commands = black --check .
+
 [testenv:lint]
-basepython = python3.6
+basepython = python3.10
 deps =
     -rrequirements/requirements-codestyle.txt
     -rrequirements/requirements-testing.txt
@@ -29,9 +40,9 @@ deps =
 commands = flake8
 
 [testenv:docs]
-basepython = python3.6
+# keep in sync with .readthedocs.yml
+basepython = python3.10
 deps =
-    -rrequirements/requirements-testing.txt
     -rrequirements/requirements-optionals.txt
     -rrequirements/requirements-documentation.txt
 commands =
