Skip to content

Commit bb49a5f

Browse files
committed
Add and expose JSONDictField and JSONListField on the plugin API
DRF serializers.JSONField can be any json entity, but we want more precise types for better schema/bindings representation. New fields that are supposed to be dict or list structures should use the new JSONDictField or JSONListField field. Some context: <pulp/pulp_rpm#3639>
1 parent c6c87f9 commit bb49a5f

File tree

5 files changed

+102
-5
lines changed

5 files changed

+102
-5
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Exposed JSONDictField and JSONListField on the plugin API.
2+
3+
DRF serializers.JSONField can be any json entity, but we want more precise types
4+
for better schema/bindings representation. New fields that are supposed to be dict
5+
or list structures should use the new JSONDictField or JSONListField field.
6+
7+
Some context: <https://github.com/pulp/pulp_rpm/issues/3639>

pulpcore/app/serializers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
ImportsIdentityFromImporterField,
3131
ImportRelatedField,
3232
ImportIdentityField,
33+
JSONDictField,
34+
JSONListField,
3335
LatestVersionField,
3436
SingleContentArtifactField,
3537
RepositoryVersionsIdentityFromRepositoryField,

pulpcore/app/serializers/fields.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,48 @@ def relative_path_validator(relative_path):
2121
)
2222

2323

24+
# Prefer JSONDictField and JSONListField over JSONField:
25+
# * Drf serializers.JSONField provides a OpenApi schema type of Any.
26+
# * This can cause problems with bindings and is not helpful to the user.
27+
# * https://github.com/tfranzel/drf-spectacular/issues/1095
28+
29+
2430
@extend_schema_field(OpenApiTypes.OBJECT)
2531
class JSONDictField(serializers.JSONField):
26-
"""A drf JSONField override to force openapi schema to use 'object' type.
27-
28-
Not strictly correct, but we relied on that for a long time.
29-
See: https://github.com/tfranzel/drf-spectacular/issues/1095
30-
"""
32+
"""A JSONField accepting dicts, specifying as type 'object' in the openapi."""
33+
34+
def to_internal_value(self, data):
35+
value = super().to_internal_value(data)
36+
ERROR_MSG = f"Invalid type. Expected a JSON object (dict), got {value!r}."
37+
# This condition is from the JSONField source:
38+
# if it's True, it will return the python representation,
39+
# else the raw data string
40+
returns_python_repr = self.binary or getattr(data, "is_json_string", False)
41+
if returns_python_repr:
42+
if not isinstance(value, dict):
43+
raise serializers.ValidationError(ERROR_MSG)
44+
elif not value.strip().startswith("{"):
45+
raise serializers.ValidationError(ERROR_MSG)
46+
return value
47+
48+
49+
@extend_schema_field(serializers.ListField)
50+
class JSONListField(serializers.JSONField):
51+
"""A JSONField accepting lists, specifying as type 'array' in the openapi."""
52+
53+
def to_internal_value(self, data):
54+
value = super().to_internal_value(data)
55+
ERROR_MSG = f"Invalid type. Expected a JSON array (list), got {value!r}."
56+
# This condition is from the JSONField source:
57+
# if it's True, it will return the python representation,
58+
# else the raw data string
59+
returns_python_repr = self.binary or getattr(data, "is_json_string", False)
60+
if returns_python_repr:
61+
if not isinstance(value, list):
62+
raise serializers.ValidationError(ERROR_MSG)
63+
elif not value.strip().startswith("["):
64+
raise serializers.ValidationError(ERROR_MSG)
65+
return value
3166

3267

3368
class SingleContentArtifactField(RelatedField):

pulpcore/plugin/serializers/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
IdentityField,
1818
ImporterSerializer,
1919
ImportSerializer,
20+
JSONDictField,
21+
JSONListField,
2022
ModelSerializer,
2123
MultipleArtifactContentSerializer,
2224
NestedRelatedField,
@@ -62,6 +64,8 @@
6264
"IdentityField",
6365
"ImporterSerializer",
6466
"ImportSerializer",
67+
"JSONDictField",
68+
"JSONListField",
6569
"ModelSerializer",
6670
"MultipleArtifactContentSerializer",
6771
"NestedRelatedField",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import pytest
2+
from rest_framework import serializers
3+
4+
from pulpcore.app.serializers import fields
5+
6+
7+
@pytest.mark.parametrize(
8+
"field_and_data",
9+
[
10+
(fields.JSONDictField, '{"foo": 123, "bar": [1,2,3]}'),
11+
(fields.JSONListField, '[{"foo": 123}, {"bar": 456}]'),
12+
],
13+
)
14+
@pytest.mark.parametrize("binary_arg", [True, False])
15+
def test_custom_json_dict_field(field_and_data, binary_arg):
16+
"""
17+
On the happy overlap case,
18+
pulpcore JSONDictField and JSONListField should be compatible with drf JSONField.
19+
"""
20+
custom_field, data = field_and_data
21+
drf_json_field = serializers.JSONField(binary=binary_arg)
22+
custom_field = custom_field(binary=binary_arg)
23+
custom_field_result = custom_field.to_internal_value(data)
24+
drf_field_result = drf_json_field.to_internal_value(data)
25+
assert custom_field_result == drf_field_result
26+
27+
28+
@pytest.mark.parametrize(
29+
"field_and_data",
30+
[
31+
(fields.JSONDictField, '[{"foo": 123}, {"bar": 456}]'),
32+
(fields.JSONDictField, "123"),
33+
(fields.JSONDictField, "false"),
34+
(fields.JSONListField, '{"foo": 123, "bar": [1,2,3]}'),
35+
(fields.JSONListField, "123"),
36+
(fields.JSONListField, "false"),
37+
],
38+
)
39+
@pytest.mark.parametrize("binary_arg", [True, False])
40+
def test_custom_json_dict_field_raises(field_and_data, binary_arg):
41+
"""
42+
On the invalid data case,
43+
pulpcore JSONDictField and JSONListField should raise appropriately.
44+
"""
45+
custom_field, data = field_and_data
46+
custom_field = custom_field(binary=binary_arg)
47+
error_msg = "Invalid type"
48+
with pytest.raises(serializers.ValidationError, match=error_msg):
49+
custom_field.to_internal_value(data)

0 commit comments

Comments
 (0)