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 6, 2024
1 parent 738216c commit 780b041
Show file tree
Hide file tree
Showing 5 changed files with 109 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
52 changes: 47 additions & 5 deletions pulpcore/app/serializers/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,55 @@ 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.
# * Because of that, we want to have more specific types for the schema.
# * 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)
# 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(
f"Invalid type. Expected a JSON object (dict), got {value!r}."
)
elif not value.strip().startswith("{"):
raise serializers.ValidationError(
f"Invalid type. Expected a JSON object (dict), got {value!r}."
)
return value


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

def to_internal_value(self, data):
value = super().to_internal_value(data)
# 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(
"Invalid type. Expected a JSON object (a dict-like structure)."
)
elif not value.strip().startswith("["):
raise serializers.ValidationError(
"Invalid type. Expected a JSON object (a dict-like structure)."
)
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 780b041

Please sign in to comment.