Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make JSONField support type annotation and OpanAPI document generation #1763

Merged
merged 16 commits into from
Nov 18, 2024
Merged
12 changes: 12 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ Changelog

0.21
====
0.21.8
------
Fixed
^^^^^
- TODO

Added
^^^^^
- JSONField adds optional generic support, and supports OpenAPI document generation by specifying `field_type` as a pydantic BaseModel (#1763)



0.21.7
------
Fixed
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Contributors
* Andrea Magistà ``@vlakius``
* Daniel Szucs ``@Quasar6X``
* Rui Catarino ``@ruitcatarino``
* Lance Moe ``@lancemoe``

Special Thanks
==============
Expand Down
4 changes: 2 additions & 2 deletions docs/query.rst
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ In PostgreSQL and MYSQL, you can use the ``contains``, ``contained_by`` and ``fi
.. code-block:: python3

class JSONModel:
data = fields.JSONField()
data = fields.JSONField[list]()

await JSONModel.create(data=["text", 3, {"msg": "msg2"}])
obj = await JSONModel.filter(data__contains=[{"msg": "msg2"}]).first()
Expand All @@ -257,7 +257,7 @@ In PostgreSQL and MYSQL, you can use the ``contains``, ``contained_by`` and ``fi
.. code-block:: python3

class JSONModel:
data = fields.JSONField()
data = fields.JSONField[dict]()

await JSONModel.create(data={"breed": "labrador",
"owner": {
Expand Down
2 changes: 1 addition & 1 deletion examples/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

class Report(Model):
id = fields.IntField(primary_key=True)
content = fields.JSONField()
content = fields.JSONField[dict]()

def __str__(self):
return str(self.id)
Expand Down
181 changes: 59 additions & 122 deletions tests/contrib/test_pydantic.py

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions tests/test_queryset_reuse.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
from tests.testmodels import (
Event,
Tournament,
)
from tests.testmodels import Event, Tournament
from tortoise.contrib import test
from tortoise.contrib.test.condition import NotEQ
from tortoise.expressions import F
Expand Down
38 changes: 28 additions & 10 deletions tests/testmodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from typing import List, Union

import pytz
from pydantic import ConfigDict
from pydantic import BaseModel, ConfigDict

from tortoise import fields
from tortoise.exceptions import ValidationError
Expand All @@ -34,6 +34,15 @@ def generate_token():
return binascii.hexlify(os.urandom(16)).decode("ascii")


class TestSchemaForJSONField(BaseModel):
foo: int
bar: str
__test__ = False


json_pydantic_default = TestSchemaForJSONField(foo=1, bar="baz")


class Author(Model):
name = fields.CharField(max_length=255)

Expand Down Expand Up @@ -286,21 +295,30 @@ class FloatFields(Model):
floatnum_null = fields.FloatField(null=True)


def raise_if_not_dict_or_list(value: Union[dict, list]):
if not isinstance(value, (dict, list)):
raise ValidationError("Value must be a dict or list.")


class JSONFields(Model):
"""
This model contains many JSON blobs
"""

@staticmethod
def dict_or_list(value: Union[dict, list]):
if not isinstance(value, (dict, list)):
raise ValidationError("Value must be a dict or list.")

id = fields.IntField(primary_key=True)
data = fields.JSONField()
data_null = fields.JSONField(null=True)
data_default = fields.JSONField(default={"a": 1})
data_validate = fields.JSONField(null=True, validators=[lambda v: JSONFields.dict_or_list(v)])
data = fields.JSONField() # type: ignore # Test cases where generics are not provided
data_null = fields.JSONField[Union[dict, list]](null=True)
data_default = fields.JSONField[dict](default={"a": 1})

# From Python 3.10 onwards, validator can be defined with staticmethod
data_validate = fields.JSONField[Union[dict, list]](
null=True, validators=[raise_if_not_dict_or_list]
)

# Test cases where generics are provided and the type is a pydantic base model
data_pydantic = fields.JSONField[TestSchemaForJSONField](
default=json_pydantic_default, field_type=TestSchemaForJSONField
)


class UUIDFields(Model):
Expand Down
42 changes: 42 additions & 0 deletions tests/utils/test_describe_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
SourceFields,
StraightFields,
Team,
TestSchemaForJSONField,
Tournament,
UUIDFkRelatedModel,
UUIDFkRelatedNullModel,
UUIDM2MRelatedModel,
UUIDPkModel,
json_pydantic_default,
)
from tortoise import Tortoise, fields
from tortoise.contrib import test
Expand Down Expand Up @@ -1392,6 +1394,26 @@ def test_describe_model_json(self):
"docstring": None,
"constraints": {},
},
{
"name": "data_pydantic",
"field_type": "JSONField",
"db_column": "data_pydantic",
"db_field_types": {
"": "JSON",
"mssql": "NVARCHAR(MAX)",
"oracle": "NCLOB",
"postgres": "JSONB",
},
"python_type": "tests.testmodels.TestSchemaForJSONField",
"generated": False,
"nullable": False,
"unique": False,
"indexed": False,
"default": "foo=1 bar='baz'",
"description": None,
"docstring": None,
"constraints": {},
},
],
"fk_fields": [],
"backward_fk_fields": [],
Expand Down Expand Up @@ -1511,6 +1533,26 @@ def test_describe_model_json_native(self):
"docstring": None,
"constraints": {},
},
{
"name": "data_pydantic",
"field_type": fields.JSONField,
"db_column": "data_pydantic",
"db_field_types": {
"": "JSON",
"mssql": "NVARCHAR(MAX)",
"oracle": "NCLOB",
"postgres": "JSONB",
},
"python_type": TestSchemaForJSONField,
"generated": False,
"nullable": False,
"unique": False,
"indexed": False,
"default": json_pydantic_default,
"description": None,
"docstring": None,
"constraints": {},
},
],
"fk_fields": [],
"backward_fk_fields": [],
Expand Down
Loading