Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit ed5a999

Browse files
Fix schema generation for nested serializers (#1177)
* Fix Serializer schema generation when used as a ListField child * Fix Serializer schema generation when used in another serializer
1 parent cd5f179 commit ed5a999

File tree

13 files changed

+194
-0
lines changed

13 files changed

+194
-0
lines changed

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
Note that in line with [Django REST framework policy](https://www.django-rest-framework.org/topics/release-notes/),
99
any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change.
1010

11+
## [Unreleased]
12+
13+
### Fixed
14+
15+
* Fixed OpenAPI schema generation for `Serializer` when used inside another `Serializer` or as a child of `ListField`.
16+
1117
## [6.1.0] - 2023年08月25日
1218

1319
### Added

‎example/factories.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
Company,
1313
Entry,
1414
ProjectType,
15+
Questionnaire,
1516
ResearchProject,
1617
TaggedItem,
1718
)
@@ -140,3 +141,24 @@ def future_projects(self, create, extracted, **kwargs):
140141
if extracted:
141142
for project in extracted:
142143
self.future_projects.add(project)
144+
145+
146+
class QuestionnaireFactory(factory.django.DjangoModelFactory):
147+
class Meta:
148+
model = Questionnaire
149+
150+
name = factory.LazyAttribute(lambda x: faker.text())
151+
questions = [
152+
{
153+
"text": "What is your name?",
154+
"required": True,
155+
},
156+
{
157+
"text": "What is your quest?",
158+
"required": False,
159+
},
160+
{
161+
"text": "What is the air-speed velocity of an unladen swallow?",
162+
},
163+
]
164+
metadata = {"author": "Bridgekeeper"}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 4.2.5 on 2023年09月07日 02:35
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("example", "0012_author_full_name"),
9+
]
10+
11+
operations = [
12+
migrations.CreateModel(
13+
name="Questionnaire",
14+
fields=[
15+
(
16+
"id",
17+
models.AutoField(
18+
auto_created=True,
19+
primary_key=True,
20+
serialize=False,
21+
verbose_name="ID",
22+
),
23+
),
24+
("name", models.CharField(max_length=100)),
25+
("questions", models.JSONField()),
26+
],
27+
),
28+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.5 on 2023年09月12日 07:12
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("example", "0013_questionnaire"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="questionnaire",
14+
name="metadata",
15+
field=models.JSONField(default={}),
16+
preserve_default=False,
17+
),
18+
]

‎example/models.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,9 @@ class Company(models.Model):
180180

181181
def __str__(self):
182182
return self.name
183+
184+
185+
class Questionnaire(models.Model):
186+
name = models.CharField(max_length=100)
187+
questions = models.JSONField()
188+
metadata = models.JSONField()

‎example/serializers.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
LabResults,
1919
Project,
2020
ProjectType,
21+
Questionnaire,
2122
ResearchProject,
2223
TaggedItem,
2324
)
@@ -421,3 +422,22 @@ class CompanySerializer(serializers.ModelSerializer):
421422
class Meta:
422423
model = Company
423424
fields = "__all__"
425+
426+
427+
class QuestionSerializer(serializers.Serializer):
428+
text = serializers.CharField()
429+
required = serializers.BooleanField(default=False)
430+
431+
432+
class QuestionnaireMetadataSerializer(serializers.Serializer):
433+
author = serializers.CharField()
434+
producer = serializers.CharField(default=None)
435+
436+
437+
class QuestionnaireSerializer(serializers.ModelSerializer):
438+
questions = serializers.ListField(child=QuestionSerializer())
439+
metadata = QuestionnaireMetadataSerializer()
440+
441+
class Meta:
442+
model = Questionnaire
443+
fields = ("name", "questions", "metadata")

‎example/tests/conftest.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
CommentFactory,
1313
CompanyFactory,
1414
EntryFactory,
15+
QuestionnaireFactory,
1516
ResearchProjectFactory,
1617
TaggedItemFactory,
1718
)
@@ -27,6 +28,7 @@
2728
register(ArtProjectFactory)
2829
register(ResearchProjectFactory)
2930
register(CompanyFactory)
31+
register(QuestionnaireFactory)
3032

3133

3234
@pytest.fixture

‎example/tests/test_openapi.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,46 @@ def test_schema_id_field():
125125
assert "id" not in company_properties["attributes"]["properties"]
126126

127127

128+
def test_schema_subserializers():
129+
"""Schema for child Serializers reflects the actual response structure."""
130+
patterns = [
131+
re_path(
132+
"^questionnaires/?$", views.QuestionnaireViewset.as_view({"get": "list"})
133+
),
134+
]
135+
generator = SchemaGenerator(patterns=patterns)
136+
137+
request = create_request("/")
138+
schema = generator.get_schema(request=request)
139+
140+
assert {
141+
"type": "object",
142+
"properties": {
143+
"metadata": {
144+
"type": "object",
145+
"properties": {
146+
"author": {"type": "string"},
147+
"producer": {"type": "string"},
148+
},
149+
"required": ["author"],
150+
},
151+
"questions": {
152+
"type": "array",
153+
"items": {
154+
"type": "object",
155+
"properties": {
156+
"text": {"type": "string"},
157+
"required": {"type": "boolean", "default": False},
158+
},
159+
"required": ["text"],
160+
},
161+
},
162+
"name": {"type": "string", "maxLength": 100},
163+
},
164+
"required": ["name", "questions", "metadata"],
165+
} == schema["components"]["schemas"]["Questionnaire"]["properties"]["attributes"]
166+
167+
128168
def test_schema_parameters_include():
129169
"""Include paramater is only used when serializer defines included_serializers."""
130170
patterns = [

‎example/tests/test_serializers.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,39 @@ def test_model_serializer_with_implicit_fields(self, comment, client):
224224
assert response.status_code == 200
225225
assert expected == response.json()
226226

227+
def test_model_serializer_with_subserializers(self, questionnaire, client):
228+
expected = {
229+
"data": {
230+
"type": "questionnaires",
231+
"id": str(questionnaire.pk),
232+
"attributes": {
233+
"name": questionnaire.name,
234+
"questions": [
235+
{
236+
"text": "What is your name?",
237+
"required": True,
238+
},
239+
{
240+
"text": "What is your quest?",
241+
"required": False,
242+
},
243+
{
244+
"text": "What is the air-speed velocity of an unladen swallow?",
245+
"required": False,
246+
},
247+
],
248+
"metadata": {"author": "Bridgekeeper", "producer": None},
249+
},
250+
},
251+
}
252+
253+
response = client.get(
254+
reverse("questionnaire-detail", kwargs={"pk": questionnaire.pk})
255+
)
256+
257+
assert response.status_code == 200
258+
assert expected == response.json()
259+
227260

228261
class TestPolymorphicModelSerializer(TestCase):
229262
def setUp(self):

‎example/urls.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
NonPaginatedEntryViewSet,
2020
ProjectTypeViewset,
2121
ProjectViewset,
22+
QuestionnaireViewset,
2223
)
2324

2425
router = routers.DefaultRouter(trailing_slash=False)
@@ -32,6 +33,7 @@
3233
router.register(r"projects", ProjectViewset)
3334
router.register(r"project-types", ProjectTypeViewset)
3435
router.register(r"lab-results", LabResultViewSet)
36+
router.register(r"questionnaires", QuestionnaireViewset)
3537

3638
urlpatterns = [
3739
path("", include(router.urls)),

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /