Skip to content

Commit

Permalink
Add and expose JSONDictField and JSONListField on the plugin API
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
pedro-psb committed Nov 22, 2024
1 parent c6c87f9 commit bb49a5f
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 5 deletions.
7 changes: 7 additions & 0 deletions CHANGES/plugin_api/+expose-json-dict.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Exposed 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: <https://github.com/pulp/pulp_rpm/issues/3639>
2 changes: 2 additions & 0 deletions pulpcore/app/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
ImportsIdentityFromImporterField,
ImportRelatedField,
ImportIdentityField,
JSONDictField,
JSONListField,
LatestVersionField,
SingleContentArtifactField,
RepositoryVersionsIdentityFromRepositoryField,
Expand Down
45 changes: 40 additions & 5 deletions pulpcore/app/serializers/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,48 @@ def relative_path_validator(relative_path):
)


# Prefer JSONDictField and JSONListField over JSONField:
# * Drf serializers.JSONField provides a OpenApi schema type of Any.
# * This can cause problems with bindings and is not helpful to the user.
# * https://github.com/tfranzel/drf-spectacular/issues/1095


@extend_schema_field(OpenApiTypes.OBJECT)
class JSONDictField(serializers.JSONField):
"""A drf JSONField override to force openapi schema to use 'object' type.
Not strictly correct, but we relied on that for a long time.
See: https://github.com/tfranzel/drf-spectacular/issues/1095
"""
"""A JSONField accepting dicts, specifying as type 'object' in the openapi."""

def to_internal_value(self, data):
value = super().to_internal_value(data)
ERROR_MSG = f"Invalid type. Expected a JSON object (dict), got {value!r}."
# This condition is from the JSONField source:
# if it's True, it will return the python representation,
# else the raw data string
returns_python_repr = self.binary or getattr(data, "is_json_string", False)
if returns_python_repr:
if not isinstance(value, dict):
raise serializers.ValidationError(ERROR_MSG)
elif not value.strip().startswith("{"):
raise serializers.ValidationError(ERROR_MSG)
return value


@extend_schema_field(serializers.ListField)
class JSONListField(serializers.JSONField):
"""A JSONField accepting lists, specifying as type 'array' in the openapi."""

def to_internal_value(self, data):
value = super().to_internal_value(data)
ERROR_MSG = f"Invalid type. Expected a JSON array (list), got {value!r}."
# This condition is from the JSONField source:
# if it's True, it will return the python representation,
# else the raw data string
returns_python_repr = self.binary or getattr(data, "is_json_string", False)
if returns_python_repr:
if not isinstance(value, list):
raise serializers.ValidationError(ERROR_MSG)
elif not value.strip().startswith("["):
raise serializers.ValidationError(ERROR_MSG)
return value


class SingleContentArtifactField(RelatedField):
Expand Down
4 changes: 4 additions & 0 deletions pulpcore/plugin/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
IdentityField,
ImporterSerializer,
ImportSerializer,
JSONDictField,
JSONListField,
ModelSerializer,
MultipleArtifactContentSerializer,
NestedRelatedField,
Expand Down Expand Up @@ -62,6 +64,8 @@
"IdentityField",
"ImporterSerializer",
"ImportSerializer",
"JSONDictField",
"JSONListField",
"ModelSerializer",
"MultipleArtifactContentSerializer",
"NestedRelatedField",
Expand Down
49 changes: 49 additions & 0 deletions pulpcore/tests/unit/serializers/test_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import pytest
from rest_framework import serializers

from pulpcore.app.serializers import fields


@pytest.mark.parametrize(
"field_and_data",
[
(fields.JSONDictField, '{"foo": 123, "bar": [1,2,3]}'),
(fields.JSONListField, '[{"foo": 123}, {"bar": 456}]'),
],
)
@pytest.mark.parametrize("binary_arg", [True, False])
def test_custom_json_dict_field(field_and_data, binary_arg):
"""
On the happy overlap case,
pulpcore JSONDictField and JSONListField should be compatible with drf JSONField.
"""
custom_field, data = field_and_data
drf_json_field = serializers.JSONField(binary=binary_arg)
custom_field = custom_field(binary=binary_arg)
custom_field_result = custom_field.to_internal_value(data)
drf_field_result = drf_json_field.to_internal_value(data)
assert custom_field_result == drf_field_result


@pytest.mark.parametrize(
"field_and_data",
[
(fields.JSONDictField, '[{"foo": 123}, {"bar": 456}]'),
(fields.JSONDictField, "123"),
(fields.JSONDictField, "false"),
(fields.JSONListField, '{"foo": 123, "bar": [1,2,3]}'),
(fields.JSONListField, "123"),
(fields.JSONListField, "false"),
],
)
@pytest.mark.parametrize("binary_arg", [True, False])
def test_custom_json_dict_field_raises(field_and_data, binary_arg):
"""
On the invalid data case,
pulpcore JSONDictField and JSONListField should raise appropriately.
"""
custom_field, data = field_and_data
custom_field = custom_field(binary=binary_arg)
error_msg = "Invalid type"
with pytest.raises(serializers.ValidationError, match=error_msg):
custom_field.to_internal_value(data)

0 comments on commit bb49a5f

Please sign in to comment.