|
| 1 | +# Litestar-Django |
| 2 | + |
| 3 | +Django model support for Litestar, implemented via Litestar [DTOs](https://docs.litestar.dev/latest/usage/dto/index.html). |
| 4 | + |
| 5 | +```python |
| 6 | +from litestar import get, Litestar |
| 7 | +from litestar_django import DjangoModelPlugin |
| 8 | +from django.db import models |
| 9 | + |
| 10 | +class Author(models.Model): |
| 11 | + name = models.CharField(max_length=100) |
| 12 | + |
| 13 | + |
| 14 | +class Genre(models.Model): |
| 15 | + name = models.CharField(max_length=50) |
| 16 | + |
| 17 | + |
| 18 | +class Book(models.Model): |
| 19 | + name = models.CharField() |
| 20 | + author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books") |
| 21 | + genres = models.ManyToManyField(Genre, related_name="books") |
| 22 | + |
| 23 | + |
| 24 | +@get("/{author_id:int}") |
| 25 | +async def handler(author_id: int) -> Author: |
| 26 | + return await Author.objects.prefetch_related("books").aget(id=author_id) |
| 27 | + |
| 28 | + |
| 29 | +app = Litestar([handler], plugins=[DjangoModelPlugin()]) |
| 30 | +``` |
| 31 | + |
| 32 | +This minimal setup will provide serialization of Django objects returned from handlers, |
| 33 | +complete with OpenAPI schema generation. |
| 34 | + |
| 35 | +## Installation |
| 36 | + |
| 37 | +```bash |
| 38 | +pip install litestar-django |
| 39 | +``` |
| 40 | + |
| 41 | +## Usage |
| 42 | + |
| 43 | +### Directly constructing a DTO |
| 44 | + |
| 45 | +```python |
| 46 | +from litestar import get |
| 47 | +from litestar_django import DjangoModelDTO |
| 48 | +from app.models import Author |
| 49 | + |
| 50 | +@get("/{author_id:int}", dto=DjangoModelDTO[Author]) |
| 51 | +async def handler(author_id: int) -> Author: |
| 52 | + return await Author.objects.prefetch_related("books").aget(id=author_id) |
| 53 | +``` |
| 54 | + |
| 55 | +### Automatically creating DTOs via the plugin |
| 56 | + |
| 57 | +```python |
| 58 | +from litestar import get |
| 59 | +from litestar_django import DjangoModelPlugin |
| 60 | +from app.models import Author |
| 61 | + |
| 62 | +@get("/{author_id:int}") |
| 63 | +async def handler(author_id: int) -> Author: |
| 64 | + return await Author.objects.prefetch_related("books").aget(id=author_id) |
| 65 | + |
| 66 | +app = Litestar([handler], plugins=[DjangoModelPlugin()]) |
| 67 | +``` |
| 68 | + |
| 69 | +### Creating a model instance from a DTO |
| 70 | + |
| 71 | +```python |
| 72 | +from typing import Annotated |
| 73 | +from litestar import post |
| 74 | +from litestar.dto import DTOConfig |
| 75 | +from litestar_django import DjangoModelDTO |
| 76 | +from app.models import Author |
| 77 | + |
| 78 | +@post( |
| 79 | + "/", |
| 80 | + sync_to_thread=True, |
| 81 | + dto=DjangoModelDTO[ |
| 82 | + Annotated[ |
| 83 | + Author, |
| 84 | + # exclude primary key and relationship fields |
| 85 | + DTOConfig(exclude={"id", "books"}) |
| 86 | + ] |
| 87 | + ], |
| 88 | + return_dto=DjangoModelDTO[Author], |
| 89 | +) |
| 90 | +async def handler(data: Author) -> Author: |
| 91 | + await data.asave() |
| 92 | + return data |
| 93 | +``` |
| 94 | + |
| 95 | +## OpenAPI |
| 96 | + |
| 97 | +Full OpenAPI schemas are generated from models based on their field types: |
| 98 | + |
| 99 | +### Type map |
| 100 | + |
| 101 | +| Field | OpenAPI type | OpenAPI format | |
| 102 | +|------------------------|--------------|----------------| |
| 103 | +| `models.JSONField` | `{}` | | |
| 104 | +| `models.DecimalField` | `number` | | |
| 105 | +| `models.DateTimeField` | `string` | `date-time` | |
| 106 | +| `models.DateField` | `string` | `date` | |
| 107 | +| `models.TimeField` | `string` | `duration` | |
| 108 | +| `models.DurationField` | `string` | `duration` | |
| 109 | +| `models.FileField` | `string` | | |
| 110 | +| `models.FilePathField` | `string` | | |
| 111 | +| `models.UUIDField` | `string` | `uuid` | |
| 112 | +| `models.IntegerField` | `integer` | | |
| 113 | +| `models.FloatField` | `number` | | |
| 114 | +| `models.BooleanField` | `boolean` | | |
| 115 | +| `models.CharField` | `string` | | |
| 116 | +| `models.TextField` | `string` | | |
| 117 | +| `models.BinaryField` | `string` | `byte` | |
| 118 | + |
| 119 | +### Additional properties |
| 120 | + |
| 121 | +The following properties are extracted from fields, in addition to its type: |
| 122 | + |
| 123 | + |
| 124 | +| OpenAPI property | From | |
| 125 | +|--------------------|----------------------| |
| 126 | +| `title` | `Field.verbose_name` | |
| 127 | +| `description` | `Field.help_text` | |
| 128 | +| `enum` | `Field.choices` | |
| 129 | +| `exclusiveMinimum` | `MinValueValidator` | |
| 130 | +| `exclusiveMaximum` | `MaxValueValidator` | |
| 131 | +| `minLength` | `MinLengthValidator` | |
| 132 | +| `maxLength` | `MaxLengthValidator` | |
| 133 | + |
| 134 | +### Relationships |
| 135 | + |
| 136 | +Relationships will be represented as individual components, referenced in the schema. |
| 137 | + |
| 138 | + |
| 139 | +## Lazy loading |
| 140 | + |
| 141 | +> [!IMPORTANT] |
| 142 | +> Since lazy-loading is not supported in an async context, you must ensure to always |
| 143 | +> load everything consumed by the DTO. Not doing so will result in a |
| 144 | +> [`SynchronousOnlyOperation`](https://docs.djangoproject.com/en/5.2/ref/exceptions/#django.core.exceptions.SynchronousOnlyOperation) |
| 145 | +> exception being raised by Django |
| 146 | +
|
| 147 | +This can be mitigated by: |
| 148 | + |
| 149 | +1. Setting `include` or `exclude` rules to only include necessary fields ([docs](https://docs.litestar.dev/latest/usage/dto/1-abstract-dto.html#excluding-fields)) |
| 150 | +2. Configuring nested relationships with an appropriate `max_nexted_depth` |
| 151 | + ([docs](https://docs.litestar.dev/latest/usage/dto/1-abstract-dto.html#nested-fields)) |
| 152 | +3. Using [`select_related`](https://docs.djangoproject.com/en/5.2/ref/models/querysets/#select-related) |
| 153 | + and [`prefetch_related`](https://docs.djangoproject.com/en/5.2/ref/models/querysets/#prefetch-related) |
| 154 | + to ensure relationships are fully loaded |
| 155 | + |
| 156 | + |
| 157 | + |
| 158 | +## Contributing |
| 159 | + |
| 160 | +All [Litestar Organization][litestar-org] projects are open for contributions of any |
| 161 | +size and form. |
| 162 | + |
| 163 | +If you have any questions, reach out to us on [Discord][discord] or our org-wide |
| 164 | +[GitHub discussions][litestar-discussions] page. |
| 165 | + |
| 166 | +<!-- markdownlint-disable --> |
| 167 | +<hr /> |
| 168 | +<p align="center"> |
| 169 | + <!-- github-banner-start --> |
| 170 | + <img src="https://raw.githubusercontent.com/litestar-org/branding/main/assets/Branding%20-%20SVG%20-%20Transparent/Organization%20Project%20-%20Banner%20-%20Inline%20-%20Dark.svg" alt="Litestar Logo - Light" width="40%" height="auto" /> |
| 171 | + <br>An official <a href="https://github.com/litestar-org">Litestar Organization</a> Project |
| 172 | + <!-- github-banner-end --> |
| 173 | +</p> |
| 174 | + |
| 175 | +[litestar-org]: https://github.com/litestar-org |
| 176 | +[discord]: https://discord.gg/litestar |
| 177 | +[litestar-discussions]: https://github.com/orgs/litestar-org/discussions |
0 commit comments