Skip to content

Commit 842df00

Browse files
committed
Cover with tests
1 parent 7dadd81 commit 842df00

9 files changed

+244
-26
lines changed

django_custom_jsonfield/fields.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
from django.core import checks, exceptions
33
from django.db import models
44
from django.utils.translation import gettext_lazy as _
5-
from jsonschema.validators import validator_for
5+
from jsonschema import validators
66

77

88
class CustomJSONField(models.JSONField):
99
default_error_messages = {
10-
"invalid_data": _("Value does not match the schema."),
10+
"invalid_data": _("Value does not match the JSON schema."),
1111
}
1212

1313
def __init__(self, schema: dict, **kwargs):
@@ -16,6 +16,10 @@ def __init__(self, schema: dict, **kwargs):
1616
self.schema = schema
1717
super().__init__(**kwargs)
1818

19+
@property
20+
def non_db_attrs(self):
21+
return super().non_db_attrs + ("schema",)
22+
1923
def check(self, **kwargs):
2024
return [
2125
*super().check(**kwargs),
@@ -35,7 +39,7 @@ def validate(self, value, model_instance):
3539
) from e
3640

3741
def _check_jsonschema(self):
38-
validator = validator_for(self.schema)
42+
validator = validators.validator_for(self.schema)
3943
try:
4044
validator.check_schema(self.schema)
4145
except jsonschema.exceptions.SchemaError:

django_custom_jsonfield/forms.py

-7
This file was deleted.

django_custom_jsonfield/openapi.py

+35-8
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,47 @@
1+
from drf_spectacular.drainage import warn
12
from drf_spectacular.openapi import (
23
OpenApiSerializerFieldExtension,
4+
OpenApiTypes,
35
build_array_type,
6+
build_basic_type,
47
build_object_type,
5-
follow_field_source,
68
)
79

810

911
class CustomJSONFieldSerializerExtension(OpenApiSerializerFieldExtension):
1012
target_class = "django_custom_jsonfield.serializers.CustomJSONField"
1113

1214
def map_serializer_field(self, auto_schema, direction):
13-
model = self.target.parent.Meta.model
14-
model_field = follow_field_source(model, self.target.source.split("."))
15-
schema = model_field.schema
15+
schema = self.target.schema
1616

17-
if schema["type"] == "object":
18-
return build_object_type(schema["properties"])
19-
elif schema["type"] == "array":
20-
return build_array_type(schema["items"])
17+
try:
18+
if schema["type"] == "object":
19+
kwargs = {}
20+
if "additionalProperties" in schema:
21+
kwargs["additionalProperties"] = schema["additionalProperties"]
22+
23+
return build_object_type(
24+
schema["properties"],
25+
required=schema.get("required"),
26+
description=schema.get("description"),
27+
**kwargs,
28+
)
29+
elif schema["type"] == "array":
30+
return build_array_type(
31+
schema["items"],
32+
min_length=schema.get("minLength"),
33+
max_length=schema.get("maxLength"),
34+
)
35+
else:
36+
basic_type_mapping = {
37+
"string": OpenApiTypes.STR,
38+
"number": OpenApiTypes.NUMBER,
39+
"integer": OpenApiTypes.INT,
40+
"boolean": OpenApiTypes.BOOL,
41+
"null": OpenApiTypes.NONE,
42+
}
43+
44+
return build_basic_type(basic_type_mapping[schema["type"]])
45+
except: # noqa: E722
46+
warn(f"Encountered an issue resolving field {schema}, defaulting to string.")
47+
return build_basic_type(OpenApiTypes.STR)
+17-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1+
import jsonschema
2+
from django.utils.translation import gettext_lazy as _
13
from rest_framework import serializers
24

35

46
class CustomJSONField(serializers.JSONField):
5-
def to_internal_value(self, data):
6-
data = super().to_internal_value(data)
7+
default_error_messages = {
8+
"invalid_data": _("Value does not match the JSON schema."),
9+
}
710

8-
# if jsonschema.validate(data):
9-
# pass
11+
def __init__(self, schema: dict, **kwargs):
12+
self.schema = schema
13+
super().__init__(**kwargs)
14+
self.validators.append(self._validate_data)
1015

11-
return data
16+
def _validate_data(self, value):
17+
try:
18+
jsonschema.validate(value, self.schema)
19+
except jsonschema.exceptions.ValidationError:
20+
raise serializers.ValidationError(
21+
self.default_error_messages["invalid_data"],
22+
code="invalid_data",
23+
)

runtests.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ def exit_on_failure(ret, message=None):
99

1010

1111
if __name__ == "__main__":
12-
exit_on_failure(pytest.main())
12+
exit_on_failure(pytest.main(["--cov", "django_custom_jsonfield", "--cov-report", "xml"]))

tests/conftest.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ def pytest_configure(config):
66

77
settings.configure(
88
DATABASES={
9-
"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}
9+
"default": {
10+
"ENGINE": "django.db.backends.sqlite3",
11+
"NAME": ":memory:",
12+
},
1013
},
1114
SECRET_KEY="doesn't really matter",
1215
)

tests/test_model_field.py

+79-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
from typing import Any
2+
13
import pytest
4+
from django import VERSION as DJANGO_VERSION
25
from django.core import checks
6+
from django.core.exceptions import ValidationError
37
from django.db import models
48

59
from django_custom_jsonfield.fields import CustomJSONField
@@ -13,7 +17,7 @@
1317
{"pattern": "*invalid.regex"},
1418
],
1519
)
16-
def test_model_check_invalid_schema(schema: dict):
20+
def test_json_schema_invalid(schema: dict):
1721
class FakeModel(models.Model):
1822
json_field = CustomJSONField(schema=schema)
1923

@@ -31,3 +35,77 @@ class Meta:
3135
]
3236

3337
assert errors == expected_errors
38+
39+
40+
def test_json_schema_ok():
41+
class FakeModel(models.Model):
42+
json_field = CustomJSONField(
43+
schema={
44+
"type": "array",
45+
"minLength": 1,
46+
"maxLength": 1,
47+
"items": {"type": "integer"},
48+
},
49+
)
50+
51+
class Meta:
52+
app_label = "test_app"
53+
54+
instance = FakeModel()
55+
56+
assert instance.check() == []
57+
58+
59+
@pytest.mark.parametrize(
60+
"schema",
61+
[10, 10.00, list(), tuple(), set(), "", b"", True, None],
62+
)
63+
def test_schema_type_invalid(schema: Any):
64+
with pytest.raises(ValueError) as e:
65+
CustomJSONField(schema=schema)
66+
67+
assert e.value.args[0] == "The schema parameter must be a dictionary."
68+
69+
70+
@pytest.mark.parametrize(
71+
"value,schema",
72+
[
73+
(
74+
{"name": "John"},
75+
{
76+
"type": "object",
77+
"properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
78+
"required": ["name", "age"],
79+
},
80+
),
81+
],
82+
)
83+
def test_validate_value_against_schema(value, schema):
84+
class FakeModel(models.Model):
85+
json_field = CustomJSONField(schema=schema)
86+
87+
class Meta:
88+
app_label = "test_app"
89+
90+
instance = FakeModel()
91+
instance.json_field = value
92+
93+
with pytest.raises(ValidationError) as e:
94+
instance.clean_fields()
95+
96+
assert isinstance(e.value.args[0]["json_field"][0], ValidationError)
97+
assert e.value.args[0]["json_field"][0].args[0] == "Value does not match the JSON schema."
98+
assert e.value.args[0]["json_field"][0].args[1] == "invalid_data"
99+
assert e.value.args[0]["json_field"][0].args[2] == {"value": value}
100+
101+
102+
def test_deconstruct():
103+
json_field = CustomJSONField(schema={})
104+
_, _, _, kwargs = json_field.deconstruct()
105+
assert "schema" in kwargs
106+
107+
108+
@pytest.mark.skipif(DJANGO_VERSION < (4, 1), reason="non_db_attrs is only available in Django 4.1+")
109+
def test_non_db_attrs():
110+
json_field = CustomJSONField(schema={})
111+
assert "schema" in json_field.non_db_attrs

tests/test_openapi_schema.py

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from unittest.mock import Mock
2+
3+
import pytest
4+
5+
from django_custom_jsonfield.openapi import CustomJSONFieldSerializerExtension
6+
from django_custom_jsonfield.serializers import CustomJSONField
7+
8+
9+
@pytest.mark.parametrize(
10+
"schema",
11+
[
12+
{
13+
"type": "object",
14+
"properties": {"name": {"type": "string"}},
15+
"required": ["name"],
16+
"additionalProperties": True,
17+
},
18+
{
19+
"type": "object",
20+
"properties": {"name": {"type": "string"}},
21+
},
22+
{
23+
"items": {"type": "integer"},
24+
"type": "array",
25+
"maxLength": 1,
26+
"minLength": 1,
27+
},
28+
{
29+
"items": {"type": "integer"},
30+
"type": "array",
31+
},
32+
{
33+
"type": "number",
34+
},
35+
{
36+
"type": "string",
37+
},
38+
{
39+
"type": "integer",
40+
},
41+
{
42+
"type": "boolean",
43+
},
44+
],
45+
)
46+
def test_map_serializer_field_ok(schema: dict):
47+
json_field = CustomJSONField(schema=schema)
48+
extension = CustomJSONFieldSerializerExtension(json_field)
49+
data = extension.map_serializer_field(Mock(), "response")
50+
assert data == schema
51+
52+
53+
@pytest.mark.parametrize(
54+
"schema",
55+
[
56+
{
57+
"type": "test",
58+
},
59+
],
60+
)
61+
def test_map_serializer_field_invalid_schema(schema: dict):
62+
json_field = CustomJSONField(schema=schema)
63+
extension = CustomJSONFieldSerializerExtension(json_field)
64+
data = extension.map_serializer_field(Mock(), "response")
65+
assert data == {"type": "string"}

tests/test_serializer_field.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
from rest_framework import serializers
3+
from rest_framework.exceptions import ErrorDetail
4+
5+
from django_custom_jsonfield.serializers import CustomJSONField
6+
7+
8+
@pytest.mark.parametrize(
9+
"value,schema",
10+
[
11+
(
12+
{"name": "John"},
13+
{
14+
"type": "object",
15+
"properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
16+
"required": ["name", "age"],
17+
},
18+
),
19+
],
20+
)
21+
def test_validate(value: dict, schema: dict):
22+
class FakeSerializer(serializers.Serializer):
23+
json_field = CustomJSONField(schema=schema)
24+
25+
serializer = FakeSerializer(data={"json_field": value})
26+
serializer.is_valid()
27+
28+
expected_errors = {
29+
"json_field": [
30+
ErrorDetail(
31+
string="Value does not match the JSON schema.",
32+
code="invalid_data",
33+
),
34+
],
35+
}
36+
assert serializer.errors == expected_errors

0 commit comments

Comments
 (0)