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..2ec34d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/example/tests/integration/test_includes.py b/example/tests/integration/test_includes.py index d32ff7e3..238c3076 100644 --- a/example/tests/integration/test_includes.py +++ b/example/tests/integration/test_includes.py @@ -36,7 +36,9 @@ 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 +206,47 @@ 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_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_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 b670338f..f418b080 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -652,7 +652,7 @@ 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()):