Skip to content

RFC: Modelschema rework #1281

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

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions docs/docs/guides/response/config-pydantic.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Overriding Pydantic Config

There are many customizations available for a **Django Ninja `Schema`**, via the schema's
[Pydantic `Config` class](https://pydantic-docs.helpmanual.io/usage/model_config/).
[Pydantic `Config` class/`model_config` dict attr](https://pydantic-docs.helpmanual.io/usage/model_config/).

!!! Warning
Using a `Config` class is [deprecated in pydantic v2](https://docs.pydantic.dev/latest/concepts/config/).

!!! info
Under the hood **Django Ninja** uses [Pydantic Models](https://pydantic-docs.helpmanual.io/usage/models/)
Expand All @@ -22,27 +25,27 @@ def to_camel(string: str) -> str:
words = string.split('_')
return words[0].lower() + ''.join(word.capitalize() for word in words[1:])

class CamelModelSchema(Schema):
class CamelSchema(Schema):
str_field_name: str
float_field_name: float

class Config(Schema.Config):
class Config:
alias_generator = to_camel
```

!!! note
When overriding the schema's `Config`, it is necessary to inherit from the base `Config` class.

Keep in mind that when you want modify output for field names (like camel case) - you need to set as well `populate_by_name` and `by_alias`

```python hl_lines="6 9"
class UserSchema(ModelSchema):
class Config:
model = User
model_fields = ["id", "email", "is_staff"]
"""Pydantic config"""
alias_generator = to_camel
populate_by_name = True # !!!!!! <--------


class Meta:
"""ModelSchema config"""
model = User
model_fields = ["id", "email", "is_staff"]

@api.get("/users", response=list[UserSchema], by_alias=True) # !!!!!! <-------- by_alias
def get_users(request):
Expand Down Expand Up @@ -72,9 +75,11 @@ results:
## Custom Config from Django Model

When using [`create_schema`](django-pydantic-create-schema.md#create_schema), the resulting
schema can be used to build another class with a custom config like:
schema can be used to build another class with a custom config and/or more keys. However,
the model and fields being used with the model cannot be modified as this would require
an explicit `Meta` config class to be defined and inherited from.

```python hl_lines="10"
```python hl_lines="9"
from django.contrib.auth.models import User
from ninja.orm import create_schema

Expand All @@ -83,7 +88,7 @@ BaseUserSchema = create_schema(User)


class UserSchema(BaseUserSchema):

class Config(BaseUserSchema.Config):
...
model_config = ConfigDict() # any necessary pydantic config
related_item: RelatedItemModelSchema
stringkey: str
```
5 changes: 2 additions & 3 deletions docs/docs/guides/response/django-pydantic-create-schema.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Using create_schema

Under the hood, [`ModelSchema`](django-pydantic.md#modelschema) uses the `create_schema` function.
This is a more advanced (and less safe) method - please use it carefully.
This is a more advanced (and less safe) method - please use it carefully. It is usually better
to create `ModelSchema` classes with `Meta` configs explicitly.

## `create_schema`

Expand All @@ -19,7 +19,6 @@ def create_schema(
)
```


Take this example:

```python hl_lines="2 4"
Expand Down
164 changes: 156 additions & 8 deletions docs/docs/guides/response/django-pydantic.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,26 @@ Schemas are very useful to define your validation rules and responses, but somet

## ModelSchema

`ModelSchema` is a special base class that can automatically generate schemas from your models.
`ModelSchema` is a special base class that can automatically generate schemas from your models. Under the hood it converts your models Django fields into
pydantic type annotations. `ModelSchema` inherits from `Schema`, and is just a `Schema` with a Django field -> pydantic field conversion step. All other `Schema`
related configuration and inheritance is the same.

### Configuration

To configure a `ModelSchema` you define a `Meta` class attribute just like in Django. This `Meta` class will be validated by `ninja.orm.metaclass.MetaConf`.

```Python
class MetaConf: # summary
model: Django model being used to create the Schema
fields: List of field names in the model to use. Defaults to '__all__' which includes all fields
exclude: List of field names to exclude
optional_fields: List of field names which will be optional, can also take '__all__'
depth: If > 0 schema will also be created for the nested ForeignKeys and Many2Many (with the provided depth of lookup)
primary_key_optional: Defaults to True, controls if django's primary_key=True field in the provided model is required
```

All you need is to set `model` and `fields` attributes on your schema `Meta`:


```python hl_lines="2 5 6 7"
from django.contrib.auth.models import User
from ninja import ModelSchema
Expand All @@ -28,6 +43,117 @@ class UserSchema(ModelSchema):
# last_name: str
```

### Non-Django Model Configuration

The `Meta` class is only used for configuring the interaction between the django model and the underlying
`Schema`. To configure the pydantic model underlying the `Schema` define, `model_config` in your
`ModelSchema` class, or [use the deprecated by pydantic `class Config`](https://docs.pydantic.dev/latest/concepts/config/).

```Python
class UserSlimGetSchema(ModelSchema):
# pydantic config
# --
model_config = {"validate_default": True}
# OR
class Config:
validate_default = True
# --

class Meta:
model = User
fields = ["id", "name"]
```

### Inheritance

Because a `ModelSchema` is just a child of `Schema`, which is in turn just a child of pydantic `BaseModel`, you
can do some convenient inheritance to handle more advanced configuration scenarios.

!!! Warning
Beware that pydantic v2 does not always respect MRO: https://github.com/pydantic/pydantic/issues/9992

```python
from ninja import Schema, ModelSchema
from pydantic import model_serializer
from django.db import models

# <proj_schemas.py>
def _my_magic_serializer(self, handler):
dump = handler(self)
dump["magic"] = "shazam"
return dump


class ProjSchema(Schema):
# pydantic configuration
_my_magic_serilizer = model_serializer(mode="wrap")(_my_magic_serializer)
model_config = {"arbitrary_types_allowed": True}


class ProjModelSchema(ProjSchema, ModelSchema):
# ModelSchema specific configuration
pass


class ProjMeta:
# ModelSchema Meta defaults
primary_key_optional = False

# </proj_schemas.py>


# <models.py>
class Item(models.Model):
name = models.CharField(max_length=64)
type = models.CharField(max_length=64)
desc = models.CharField(max_length=255, blank=True, null=True)

class Meta:
app_label = "test"


class Event(models.Model):
name = models.CharField(max_length=64)
action = models.CharField(max_length=64)

class Meta:
app_label = "test"

# </models.py>


# <schemas.py>
# All schemas will be using the configuration defined in parent Schemas
class ItemSlimGetSchema(ProjModelSchema):
class Meta(ProjMeta):
model = Item
fields = ["id", "name"]


class ItemGetSchema(ItemSlimGetSchema):
class Meta(ItemSlimGetSchema.Meta):
# inherits model, and the parents fields are already set in __annotations__
fields = ["type", "desc"]


class EventGetSchema(ProjModelSchema):
class Meta(ProjMeta):
model = Event
fields = ["id", "name"]


class ItemSummarySchema(ProjSchema):
model_config = {
"title": "Item Summary"
}
name: str
event: EventGetSchema
item: ItemGetSchema

# </schemas.py>
```


### Using ALL model fields

To use all fields from a model - you can pass `__all__` to `fields`:
Expand Down Expand Up @@ -70,7 +196,8 @@ class UserSchema(ModelSchema):

### Overriding fields

To change default annotation for some field, or to add a new field, just use annotated attributes as usual.
To change default annotation for some field, or to add a new field, just use annotated attributes as usual since a `ModelSchema` is
in the end just a `Schema`.

```python hl_lines="1 2 3 4 8"
class GroupSchema(ModelSchema):
Expand Down Expand Up @@ -122,11 +249,8 @@ def patch(request, pk: int, payload: PatchGroupSchema):
setattr(obj, attr, value)

obj.save()


```


### Custom fields types

For each Django field it encounters, `ModelSchema` uses the default `Field.get_internal_type` method
Expand All @@ -144,7 +268,6 @@ class MyModel(models.Modle):
from ninja.orm import register_field

register_field('VectorField', list[float])

```

#### PatchDict
Expand All @@ -170,7 +293,32 @@ def modify_data(request, pk: int, payload: PatchDict[GroupSchema]):
setattr(obj, attr, value)

obj.save()

```

in this example the `payload` argument will be a type of `dict` only fields that were passed in request and validated using `GroupSchema`


### Inheritance

ModelSchemas can utilize inheritance. The `Meta` class is not inherited implicitly and must have an explicit parent if desired.

```Python
class ProjectBaseSchema(Schema):
# global pydantic config, hooks, etc
model_config = {}

class ProjectBaseModelSchema(ModelSchema, ProjectBaseSchema):

class Meta:
primary_key_optional = False

class UserSlimGetSchema(ProjectBaseModelSchema):
class Meta(ProjectBaseModelSchema.Meta):
model = User
fields = ["id", "username"]

class UserFullGetSchema(UserSlimGetSchema):
class Meta(UserSlimGetSchema.Meta):
model = Item
fields = ["id", "slug"]
```
Loading