From 90590d8365d5b4688e58c397b7beea6aff2f44f7 Mon Sep 17 00:00:00 2001 From: Harshal Date: Wed, 20 Aug 2025 19:04:21 +0530 Subject: [PATCH 1/5] JSON:API 1.1: return empty 'included' array when ?include= is present Fixes #1109. Ensures top-level 'included' is returned as [] when the query param ?include= is provided but no related resources exist. Updates integration tests to reflect JSON:API v1.1 spec compliance. --- .gitignore | 4 ++++ example/tests/integration/test_includes.py | 28 +++++++++++++++++++++- rest_framework_json_api/renderers.py | 4 ++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a49ab451..3482680b 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,8 @@ coverage.xml # VirtualEnv .venv/ +venv/ + # Developers *.sw* @@ -51,3 +53,5 @@ manage.py # example database drf_example + +pytest.ini diff --git a/example/tests/integration/test_includes.py b/example/tests/integration/test_includes.py index d32ff7e3..f7b8e2da 100644 --- a/example/tests/integration/test_includes.py +++ b/example/tests/integration/test_includes.py @@ -1,6 +1,8 @@ import pytest from django.urls import reverse + + pytestmark = pytest.mark.django_db @@ -36,7 +38,10 @@ 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() + ### + content = response.json() + assert "included" in content + assert content["included"] == [] # Second author does author = author_factory() response = client.get(reverse("author-detail", args=[author.pk]) + "?include=bio") @@ -204,3 +209,24 @@ def test_meta_object_added_to_included_resources(single_entry, client): ) assert response.json()["included"][0].get("meta") assert response.json()["included"][1].get("meta") + + +def test_included_empty_array_when_requested(client, author_factory): + author = author_factory(bio=None) # explicitly ensure no related bio + url = reverse("author-detail", args=[author.pk]) + "?include=bio" + response = client.get(url) + assert response.status_code == 200 + + content = response.json() + assert "included" in content + assert content["included"] == [] + + +def test_included_absent_when_not_requested(client, author_factory): + # Create an author (bio can be None or default, doesn't matter here) + author = author_factory(bio=None) + url = reverse("author-detail", args=[author.pk]) + response = client.get(url) + assert response.status_code == 200 + content = response.json() + assert "included" not in content diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index b670338f..e0047cbb 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -659,6 +659,10 @@ def render(self, data, accepted_media_type=None, renderer_context=None): render_data["included"].append( included_cache[included_type][included_id] ) + else: + request = renderer_context.get("request") + if request and "include" in request.query_params: + render_data["included"] = [] if json_api_meta: render_data["meta"] = format_field_names(json_api_meta) From 433794b6422c510275442c0e75fbf55347889bc7 Mon Sep 17 00:00:00 2001 From: Harshal Date: Thu, 21 Aug 2025 16:23:33 +0530 Subject: [PATCH 2/5] Fix tox lint environment to use setup.cfg flake8 config --- .gitignore | 3 +++ example/tests/integration/test_includes.py | 3 --- setup.cfg | 5 +++-- tox.ini | 3 +++ 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 3482680b..2341741c 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ coverage.xml venv/ + # Developers *.sw* manage.py @@ -55,3 +56,5 @@ manage.py drf_example pytest.ini + + diff --git a/example/tests/integration/test_includes.py b/example/tests/integration/test_includes.py index f7b8e2da..e39d2fec 100644 --- a/example/tests/integration/test_includes.py +++ b/example/tests/integration/test_includes.py @@ -1,8 +1,6 @@ import pytest from django.urls import reverse - - pytestmark = pytest.mark.django_db @@ -38,7 +36,6 @@ 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") - ### content = response.json() assert "included" in content assert content["included"] == [] diff --git a/setup.cfg b/setup.cfg index 92606700..6b6385f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,8 +20,9 @@ exclude = build/lib, .eggs .tox, - env - .venv + env, + .venv, + venv [isort] multi_line_output = 3 diff --git a/tox.ini b/tox.ini index 8dd0e759..3d8b56a7 100644 --- a/tox.ini +++ b/tox.ini @@ -47,3 +47,6 @@ deps = -rrequirements/requirements-documentation.txt commands = sphinx-build -W -b html -d docs/_build/doctrees docs docs/_build/html + + + From 0380b4a934d14740bff7f86f5cb2b286cc170b0b Mon Sep 17 00:00:00 2001 From: Harshal Date: Tue, 26 Aug 2025 21:40:06 +0530 Subject: [PATCH 3/5] fix: ensuring empty array is added when requested and updated tests --- .gitignore | 6 --- AUTHORS | 1 + CHANGELOG.md | 10 ++++- example/tests/integration/test_includes.py | 51 ++++++++++++++++------ example/tests/test_filters.py | 3 +- rest_framework_json_api/renderers.py | 6 +-- setup.cfg | 3 +- tox.ini | 2 - 8 files changed, 50 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 2341741c..95c2324e 100644 --- a/.gitignore +++ b/.gitignore @@ -43,9 +43,6 @@ coverage.xml # VirtualEnv .venv/ -venv/ - - # Developers *.sw* @@ -55,6 +52,3 @@ manage.py # example database drf_example -pytest.ini - - diff --git a/AUTHORS b/AUTHORS index d421d5e6..1cfc9206 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,6 +15,7 @@ David Guillot, for Contexte David Vogt Felix Viernickel Greg Aker +Harshal Kalewar Humayun Ahmad Jamie Bliss Jason Housley diff --git a/CHANGELOG.md b/CHANGELOG.md index fe7c9e14..444f07a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changelog +# _Changelog All notable changes to this project will be documented in this file. @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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. +## [Unreleased] + +### Fixed + +* Ensured that an empty `included` array is returned in responses when the `include` query parameter is present but no related resources exist. + ## [8.0.0] - 2025-07-24 ### Added @@ -31,7 +37,7 @@ any parts of the framework not mentioned in the documentation should generally b * 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 +## [7.1.0] - 2024-10-25_ This is the last release supporting Python 3.8, Django 5.0 and Django REST framework 3.14. diff --git a/example/tests/integration/test_includes.py b/example/tests/integration/test_includes.py index e39d2fec..238c3076 100644 --- a/example/tests/integration/test_includes.py +++ b/example/tests/integration/test_includes.py @@ -208,22 +208,45 @@ def test_meta_object_added_to_included_resources(single_entry, client): assert response.json()["included"][1].get("meta") -def test_included_empty_array_when_requested(client, author_factory): - author = author_factory(bio=None) # explicitly ensure no related bio - url = reverse("author-detail", args=[author.pk]) + "?include=bio" - response = client.get(url) - assert response.status_code == 200 - +def test_included_array_empty_when_requested_but_no_data(blog_factory, client): + blog = blog_factory() + response = client.get( + reverse("blog-detail", kwargs={"pk": blog.pk}) + "?include=tags" + ) content = response.json() + assert "included" in content assert content["included"] == [] -def test_included_absent_when_not_requested(client, author_factory): - # Create an author (bio can be None or default, doesn't matter here) - author = author_factory(bio=None) - url = reverse("author-detail", args=[author.pk]) - response = client.get(url) - assert response.status_code == 200 - content = response.json() - assert "included" not in content +def test_included_array_populated_when_related_data_exists( + blog_factory, tagged_item_factory, client +): + blog = blog_factory() + tag = tagged_item_factory(tag="django") + blog.tags.add(tag) + + response = client.get( + reverse("blog-detail", kwargs={"pk": blog.pk}) + "?include=tags" + ) + included = response.json()["included"] + + assert included, "Expected included array to be populated" + assert [x.get("type") for x in included] == [ + "taggedItems" + ], "Included types incorrect" + assert included[0]["attributes"]["tag"] == "django" + + +def test_included_array_present_via_jsonapimeta_defaults( + single_entry, comment_factory, author_factory, client +): + author = author_factory() + comment_factory(entry=single_entry, author=author) + + response = client.get(reverse("entry-detail", kwargs={"pk": single_entry.pk})) + + included = response.json()["included"] + + assert included, "Expected included array due to JSONAPIMeta defaults" + assert any(resource["type"] == "comments" for resource in included) diff --git a/example/tests/test_filters.py b/example/tests/test_filters.py index 8e45ded1..ff7bf263 100644 --- a/example/tests/test_filters.py +++ b/example/tests/test_filters.py @@ -530,7 +530,8 @@ def test_search_keywords(self): }, "meta": {"bodyFormat": "text"}, } - ] + ], + "included": [], } assert response.json() == expected_result diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index e0047cbb..f418b080 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -652,17 +652,13 @@ def render(self, data, accepted_media_type=None, renderer_context=None): if not included_cache[obj_type]: del included_cache[obj_type] - if included_cache: + if included_resources: 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] ) - else: - request = renderer_context.get("request") - if request and "include" in request.query_params: - render_data["included"] = [] if json_api_meta: render_data["meta"] = format_field_names(json_api_meta) diff --git a/setup.cfg b/setup.cfg index 6b6385f9..d5e03569 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,8 +21,7 @@ exclude = .eggs .tox, env, - .venv, - venv + .venv [isort] multi_line_output = 3 diff --git a/tox.ini b/tox.ini index 3d8b56a7..502cc46b 100644 --- a/tox.ini +++ b/tox.ini @@ -48,5 +48,3 @@ deps = commands = sphinx-build -W -b html -d docs/_build/doctrees docs docs/_build/html - - From dbe3644f86815fab30d1aa3c6e1e30b4c3561433 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Tue, 26 Aug 2025 23:10:17 +0400 Subject: [PATCH 4/5] Reverted unrelated changes --- .gitignore | 1 - CHANGELOG.md | 4 ++-- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 95c2324e..a49ab451 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,3 @@ manage.py # example database drf_example - diff --git a/CHANGELOG.md b/CHANGELOG.md index 444f07a5..2ec34d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# _Changelog +# Changelog All notable changes to this project will be documented in this file. @@ -37,7 +37,7 @@ any parts of the framework not mentioned in the documentation should generally b * 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_ +## [7.1.0] - 2024-10-25 This is the last release supporting Python 3.8, Django 5.0 and Django REST framework 3.14. diff --git a/setup.cfg b/setup.cfg index d5e03569..92606700 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ exclude = build/lib, .eggs .tox, - env, + env .venv [isort] From 720ccc8135a9ee3eb1981e49057fd831b839be18 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Tue, 26 Aug 2025 23:12:52 +0400 Subject: [PATCH 5/5] Reverted unrelated tox changes --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 502cc46b..8dd0e759 100644 --- a/tox.ini +++ b/tox.ini @@ -47,4 +47,3 @@ deps = -rrequirements/requirements-documentation.txt commands = sphinx-build -W -b html -d docs/_build/doctrees docs docs/_build/html -