drf-fastserializers 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- drf_fastserializers-0.2.0/LICENSE +21 -0
- drf_fastserializers-0.2.0/PKG-INFO +513 -0
- drf_fastserializers-0.2.0/README.md +478 -0
- drf_fastserializers-0.2.0/pyproject.toml +94 -0
- drf_fastserializers-0.2.0/src/drf_fastserializers/__init__.py +79 -0
- drf_fastserializers-0.2.0/src/drf_fastserializers/_compat.py +21 -0
- drf_fastserializers-0.2.0/src/drf_fastserializers/_errors.py +17 -0
- drf_fastserializers-0.2.0/src/drf_fastserializers/_payload.py +109 -0
- drf_fastserializers-0.2.0/src/drf_fastserializers/accelerator.py +127 -0
- drf_fastserializers-0.2.0/src/drf_fastserializers/migrate.py +218 -0
- drf_fastserializers-0.2.0/src/drf_fastserializers/models.py +153 -0
- drf_fastserializers-0.2.0/src/drf_fastserializers/parser.py +58 -0
- drf_fastserializers-0.2.0/src/drf_fastserializers/py.typed +0 -0
- drf_fastserializers-0.2.0/src/drf_fastserializers/renderer.py +61 -0
- drf_fastserializers-0.2.0/src/drf_fastserializers/serializer.py +210 -0
- drf_fastserializers-0.2.0/src/drf_fastserializers/spectacular.py +59 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ankit Singh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: drf-fastserializers
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Drop-in pydantic-powered serializers for Django REST Framework. 2-3x faster on large payloads.
|
|
5
|
+
Keywords: django,djangorestframework,drf,pydantic,serializer,performance
|
|
6
|
+
Author: Ankit Singh
|
|
7
|
+
Author-email: Ankit Singh <ankitksrdev@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Web Environment
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: Django :: 5.0
|
|
14
|
+
Classifier: Framework :: Pydantic
|
|
15
|
+
Classifier: Framework :: Pydantic :: 2
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Dist: pydantic>=2.7,<4
|
|
26
|
+
Requires-Dist: djangorestframework>=3.14
|
|
27
|
+
Requires-Dist: drf-spectacular>=0.27 ; extra == 'spectacular'
|
|
28
|
+
Requires-Python: >=3.12
|
|
29
|
+
Project-URL: Homepage, https://github.com/ankitksr/drf-fastserializers
|
|
30
|
+
Project-URL: Repository, https://github.com/ankitksr/drf-fastserializers
|
|
31
|
+
Project-URL: Issues, https://github.com/ankitksr/drf-fastserializers/issues
|
|
32
|
+
Project-URL: Changelog, https://github.com/ankitksr/drf-fastserializers/blob/main/CHANGELOG.md
|
|
33
|
+
Provides-Extra: spectacular
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# drf-fastserializers
|
|
37
|
+
|
|
38
|
+
**DRF serializers, pydantic-core inside.**
|
|
39
|
+
|
|
40
|
+
<p>
|
|
41
|
+
<img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg">
|
|
42
|
+
<img alt="Python" src="https://img.shields.io/badge/python-3.12%2B-blue.svg">
|
|
43
|
+
<img alt="pydantic" src="https://img.shields.io/badge/pydantic-v2%20%7C%20v3-7c3aed.svg">
|
|
44
|
+
<img alt="Django" src="https://img.shields.io/badge/Django-5%2B-44b78b.svg">
|
|
45
|
+
<img alt="DRF" src="https://img.shields.io/badge/DRF-3.14%2B-334155.svg">
|
|
46
|
+
<a href="https://github.com/astral-sh/ruff"><img alt="ruff" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json"></a>
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
> **2-3x faster `.data` on existing endpoints. One line added.**
|
|
50
|
+
|
|
51
|
+
Drop `FastSerializerMixin` into your existing serializer and `.data`
|
|
52
|
+
switches to pydantic-core's Rust JSON encoder. No rewrite. Same DRF
|
|
53
|
+
surface.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from rest_framework import serializers
|
|
57
|
+
from drf_fastserializers import FastSerializerMixin, FastJSONRenderer
|
|
58
|
+
|
|
59
|
+
class TxnSerializer(FastSerializerMixin, serializers.ModelSerializer):
|
|
60
|
+
class Meta:
|
|
61
|
+
model = Txn
|
|
62
|
+
fields = ["id", "name", "amount", "txn_date"]
|
|
63
|
+
|
|
64
|
+
class TxnListView(ListAPIView):
|
|
65
|
+
serializer_class = TxnSerializer # unchanged
|
|
66
|
+
renderer_classes = [FastJSONRenderer] # add this
|
|
67
|
+
queryset = Txn.objects.all()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
That's the migration. If `TxnSerializer` translates cleanly the endpoint
|
|
71
|
+
gets the speedup on the next request. If it doesn't (say, because of a
|
|
72
|
+
`SerializerMethodField`), you get a one-time warning and `.data` falls
|
|
73
|
+
back to standard DRF. The endpoint keeps working either way.
|
|
74
|
+
|
|
75
|
+
## Benchmark
|
|
76
|
+
|
|
77
|
+
<p align="center">
|
|
78
|
+
<img src="docs/bench.svg" alt="drf-fastserializers benchmark: 2.6x faster than stock DRF on a 21k-row payload">
|
|
79
|
+
</p>
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary>Full table</summary>
|
|
83
|
+
|
|
84
|
+
21,393 synthetic rows, ~3 MB JSON, 5 runs each.
|
|
85
|
+
Python 3.12, pydantic 2.13, DRF 3.17.
|
|
86
|
+
|
|
87
|
+
| Strategy | median_ms | min_ms | speedup |
|
|
88
|
+
|---|---:|---:|---:|
|
|
89
|
+
| DRF `Serializer` (stock) | 96 | 96 | 1.00x |
|
|
90
|
+
| **drf-fastserializers (mixin)** | **37** | 35 | **2.64x** |
|
|
91
|
+
| **drf-fastserializers (native)** | **36** | 34 | **2.65x** |
|
|
92
|
+
| Raw dict via `JSONRenderer` (reference floor, no validation) | 20 | 20 | 4.80x |
|
|
93
|
+
|
|
94
|
+
</details>
|
|
95
|
+
|
|
96
|
+
Speedup is anchored on stock DRF. Reproduce on your hardware:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
uv run python -m benchmarks.bench # text output
|
|
100
|
+
uv run python -m benchmarks.plot # regenerate docs/bench.svg
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`benchmarks/bench.py` ships with the repo and uses synthetic data only.
|
|
104
|
+
Real-world gaps widen further on `ModelSerializer` paths (because of
|
|
105
|
+
ORM hydration overhead) and on payloads with nested models. In
|
|
106
|
+
production workloads we've seen 3-4x speedups on `ModelSerializer`
|
|
107
|
+
endpoints.
|
|
108
|
+
|
|
109
|
+
## Install
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
uv add drf-fastserializers
|
|
113
|
+
# or
|
|
114
|
+
pip install drf-fastserializers
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Requires Python 3.12+, pydantic 2.7+ (v3 supported), DRF 3.14+.
|
|
118
|
+
|
|
119
|
+
## How it compares
|
|
120
|
+
|
|
121
|
+
| | stock DRF | drf-pydantic | django-ninja | **drf-fastserializers** |
|
|
122
|
+
|---|:---:|:---:|:---:|:---:|
|
|
123
|
+
| Drop into existing DRF generics | ✅ | ✅ | ❌ | ✅ |
|
|
124
|
+
| Rust JSON encode (pydantic-core) | ❌ | ❌ | ✅ | ✅ |
|
|
125
|
+
| Migrate one endpoint at a time | ✅ | ✅ | ❌ | ✅ |
|
|
126
|
+
| Keeps DRF auth / perms / throttling | ✅ | ✅ | ❌ | ✅ |
|
|
127
|
+
| Strictly typed schemas | ❌ | ✅ | ✅ | ✅ |
|
|
128
|
+
| No serializer rewrite required | ✅ | ❌ | ❌ | ✅ |
|
|
129
|
+
|
|
130
|
+
`drf-pydantic` generates DRF serializers from pydantic models, which
|
|
131
|
+
keeps DRF in the request path and gives no speed win. `django-ninja`
|
|
132
|
+
replaces DRF wholesale. `drf-fastserializers` swaps only the encoder
|
|
133
|
+
inside DRF, so you keep the rest of your stack and migrate per
|
|
134
|
+
endpoint.
|
|
135
|
+
|
|
136
|
+
## Migrating an existing serializer
|
|
137
|
+
|
|
138
|
+
### Drop in the mixin
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from drf_fastserializers import FastSerializerMixin
|
|
142
|
+
|
|
143
|
+
class TxnSerializer(FastSerializerMixin, serializers.ModelSerializer):
|
|
144
|
+
class Meta:
|
|
145
|
+
model = Txn
|
|
146
|
+
fields = ["id", "name", "amount", "txn_date"]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`FastSerializerMixin` must come **first** in the MRO. On the first
|
|
150
|
+
`.data` access it translates the DRF field list into a pydantic schema,
|
|
151
|
+
caches it per class, and switches `.data` to the Rust path. `many=True`
|
|
152
|
+
is handled via a `FastListSerializer` wrapper installed automatically.
|
|
153
|
+
|
|
154
|
+
### Add the renderer
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
REST_FRAMEWORK = {
|
|
158
|
+
"DEFAULT_RENDERER_CLASSES": [
|
|
159
|
+
"drf_fastserializers.FastJSONRenderer",
|
|
160
|
+
],
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`FastJSONRenderer` subclasses `JSONRenderer` and falls back to stock
|
|
165
|
+
encoding for error responses, hand-rolled dicts, the browsable API, and
|
|
166
|
+
anything else it doesn't recognize. Safe as a project-wide default. Set
|
|
167
|
+
`renderer_classes` per view if you want to roll it out gradually.
|
|
168
|
+
|
|
169
|
+
### When auto-translation fails
|
|
170
|
+
|
|
171
|
+
If a serializer has a `SerializerMethodField` or a custom field with an
|
|
172
|
+
overridden `to_representation`, the mixin emits a warning and falls
|
|
173
|
+
back to standard DRF `.data`. Three ways to fix it:
|
|
174
|
+
|
|
175
|
+
**1. Move the computation upstream**, into a queryset annotation or a
|
|
176
|
+
model property. Then drop the `SerializerMethodField`.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
# before
|
|
180
|
+
class TxnSerializer(FastSerializerMixin, serializers.ModelSerializer):
|
|
181
|
+
formatted_amount = serializers.SerializerMethodField()
|
|
182
|
+
|
|
183
|
+
def get_formatted_amount(self, obj):
|
|
184
|
+
return f"${obj.amount:,.2f}"
|
|
185
|
+
|
|
186
|
+
# after
|
|
187
|
+
class Txn(models.Model):
|
|
188
|
+
@property
|
|
189
|
+
def formatted_amount(self) -> str:
|
|
190
|
+
return f"${self.amount:,.2f}"
|
|
191
|
+
|
|
192
|
+
class TxnSerializer(FastSerializerMixin, serializers.ModelSerializer):
|
|
193
|
+
formatted_amount = serializers.CharField(read_only=True)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**2. Switch to explicit translation** via `from_drf` and
|
|
197
|
+
`@computed_field`:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from pydantic import computed_field
|
|
201
|
+
from drf_fastserializers import from_drf
|
|
202
|
+
|
|
203
|
+
_Base = from_drf(TxnSerializer, exclude=("formatted_amount",))
|
|
204
|
+
|
|
205
|
+
class FastTxnOut(_Base):
|
|
206
|
+
@computed_field
|
|
207
|
+
@property
|
|
208
|
+
def formatted_amount(self) -> str:
|
|
209
|
+
return f"${self.amount:,.2f}"
|
|
210
|
+
|
|
211
|
+
class TxnListView(ListAPIView):
|
|
212
|
+
serializer_class = FastTxnOut.drf
|
|
213
|
+
renderer_classes = [FastJSONRenderer]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Or pass `computed=` to skip the subclass step:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
FastTxnOut = from_drf(
|
|
220
|
+
TxnSerializer,
|
|
221
|
+
computed={
|
|
222
|
+
"formatted_amount": (lambda self: f"${self.amount:,.2f}", str),
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Each entry maps a field name to a `(callable, return_type)` tuple. The
|
|
228
|
+
callable receives the validated pydantic instance and its result is
|
|
229
|
+
exposed as a `@computed_field` on the generated schema.
|
|
230
|
+
|
|
231
|
+
**3. Opt out for this serializer.** Set `Meta.fast = False`. The mixin
|
|
232
|
+
stops trying, the warning goes away, and the endpoint stays on DRF.
|
|
233
|
+
|
|
234
|
+
### Field mapping
|
|
235
|
+
|
|
236
|
+
<details>
|
|
237
|
+
<summary>Full DRF to pydantic field mapping table</summary>
|
|
238
|
+
|
|
239
|
+
| DRF field | Pydantic type |
|
|
240
|
+
|---|---|
|
|
241
|
+
| `CharField`, `EmailField`, `URLField`, `SlugField`, `RegexField` | `str` |
|
|
242
|
+
| `IntegerField` | `int` |
|
|
243
|
+
| `FloatField` | `float` |
|
|
244
|
+
| `DecimalField` | `Decimal` |
|
|
245
|
+
| `BooleanField` | `bool` |
|
|
246
|
+
| `DateField` / `DateTimeField` / `TimeField` / `DurationField` | `date` / `datetime` / `time` / `timedelta` |
|
|
247
|
+
| `UUIDField` | `UUID` |
|
|
248
|
+
| `IPAddressField`, `FileField`, `ImageField` | `str` |
|
|
249
|
+
| `ChoiceField` | `str` |
|
|
250
|
+
| `JSONField` | `Any` |
|
|
251
|
+
| `DictField`, `HStoreField` | `dict` |
|
|
252
|
+
| `ListField(child=X)` | `list[mapped(X)]` |
|
|
253
|
+
| `Serializer(...)` (nested) | nested `FastSerializer` (recursive) |
|
|
254
|
+
| `ListSerializer(...)` | `list[nested FastSerializer]` |
|
|
255
|
+
| `PrimaryKeyRelatedField` | `int` |
|
|
256
|
+
| `StringRelatedField`, `HyperlinkedRelatedField`, `SlugRelatedField` | `str` |
|
|
257
|
+
| `SerializerMethodField` | **not supported**, see workarounds above |
|
|
258
|
+
|
|
259
|
+
Field options carried through:
|
|
260
|
+
|
|
261
|
+
| DRF option | Effect on pydantic field |
|
|
262
|
+
|---|---|
|
|
263
|
+
| `required=False` | non-required with `default=None` (or empty container for `ListField`/`DictField`) |
|
|
264
|
+
| `allow_null=True` | type widened to `T \| None` |
|
|
265
|
+
| `default=...` | becomes the pydantic default |
|
|
266
|
+
| `source="a.b.c"` | becomes `AliasPath("a", "b", "c")` |
|
|
267
|
+
|
|
268
|
+
</details>
|
|
269
|
+
|
|
270
|
+
### Pagination
|
|
271
|
+
|
|
272
|
+
Standard DRF pagination works without changes:
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
class TxnListView(ListAPIView):
|
|
276
|
+
serializer_class = TxnSerializer
|
|
277
|
+
renderer_classes = [FastJSONRenderer]
|
|
278
|
+
pagination_class = LimitOffsetPagination
|
|
279
|
+
queryset = Txn.objects.all()
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The renderer recognizes `{"results": <FastPayload>, "next": ..., "count": ...}`
|
|
283
|
+
and splices the Rust-encoded list bytes into the paginator's wrapper.
|
|
284
|
+
|
|
285
|
+
## Deriving schemas from Django models
|
|
286
|
+
|
|
287
|
+
When you'd rather skip the DRF serializer step entirely, derive a
|
|
288
|
+
schema straight from your Django model:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from drf_fastserializers import from_model, FastJSONRenderer
|
|
292
|
+
|
|
293
|
+
TxnOut = from_model(Txn, fields=["id", "name", "amount", "txn_date"])
|
|
294
|
+
|
|
295
|
+
class TxnListView(ListAPIView):
|
|
296
|
+
serializer_class = TxnOut.drf
|
|
297
|
+
renderer_classes = [FastJSONRenderer]
|
|
298
|
+
queryset = Txn.objects.all()
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
`from_model` walks `Model._meta` and maps each concrete Django field to
|
|
302
|
+
its pydantic equivalent (nullability, defaults, FK PK types, callable
|
|
303
|
+
defaults via `default_factory`). Pass `fields="__all__"` to include
|
|
304
|
+
every concrete field, or `exclude=(...)` to drop a subset.
|
|
305
|
+
|
|
306
|
+
## Defining schemas natively (new code)
|
|
307
|
+
|
|
308
|
+
For new endpoints, skip the DRF serializer and define a pydantic schema
|
|
309
|
+
directly. Same renderer, tighter types, less boilerplate.
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
from datetime import date
|
|
313
|
+
from decimal import Decimal
|
|
314
|
+
from drf_fastserializers import FastSerializer, FastJSONRenderer
|
|
315
|
+
|
|
316
|
+
class Flags(FastSerializer):
|
|
317
|
+
is_nsf: bool = False
|
|
318
|
+
is_refund: bool = False
|
|
319
|
+
|
|
320
|
+
class TxnOut(FastSerializer):
|
|
321
|
+
id: int
|
|
322
|
+
name: str
|
|
323
|
+
amount: Decimal | None = None
|
|
324
|
+
txn_date: date
|
|
325
|
+
flags: Flags
|
|
326
|
+
tags: list[str] = []
|
|
327
|
+
|
|
328
|
+
class TxnListView(ListAPIView):
|
|
329
|
+
serializer_class = TxnOut.drf
|
|
330
|
+
renderer_classes = [FastJSONRenderer]
|
|
331
|
+
queryset = Txn.objects.all()
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
`FastSerializer` is a `pydantic.BaseModel`. Everything pydantic does
|
|
335
|
+
(nested models, `@computed_field`, validators, `model_config`, enums)
|
|
336
|
+
works.
|
|
337
|
+
|
|
338
|
+
`TxnOut.drf` is a class-level descriptor returning a `DRFAdapter`
|
|
339
|
+
subclass bound to the schema. It quacks like
|
|
340
|
+
`rest_framework.serializers.Serializer`:
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
serializer = TxnOut.drf(instance=qs, many=True)
|
|
344
|
+
serializer.data # FastPayload, encoded on render
|
|
345
|
+
serializer.is_valid() # validates incoming request data
|
|
346
|
+
serializer.errors # DRF-shape: {"field": ["msg", ...]}
|
|
347
|
+
serializer.validated_data # pydantic instances
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Input validation in a view:
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
class TxnCreateView(APIView):
|
|
354
|
+
def post(self, request):
|
|
355
|
+
serializer = TxnIn.drf(data=request.data)
|
|
356
|
+
serializer.is_valid(raise_exception=True)
|
|
357
|
+
txn = serializer.validated_data
|
|
358
|
+
Txn.objects.create(**txn.model_dump())
|
|
359
|
+
return Response(status=201)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Errors land in DRF's standard shape:
|
|
363
|
+
|
|
364
|
+
```json
|
|
365
|
+
{
|
|
366
|
+
"amount": ["Input should be a valid decimal"],
|
|
367
|
+
"flags.is_nsf": ["Input should be a valid boolean"]
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Partial updates
|
|
372
|
+
|
|
373
|
+
Pass `partial=True` at construction (matches DRF). Every field becomes
|
|
374
|
+
optional with default `None`, and
|
|
375
|
+
`validated_data.model_dump(exclude_unset=True)` returns only the keys
|
|
376
|
+
the client actually sent.
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
class TxnPatchView(APIView):
|
|
380
|
+
def patch(self, request, pk):
|
|
381
|
+
serializer = TxnIn.drf(data=request.data, partial=True)
|
|
382
|
+
serializer.is_valid(raise_exception=True)
|
|
383
|
+
Txn.objects.filter(pk=pk).update(
|
|
384
|
+
**serializer.validated_data.model_dump(exclude_unset=True)
|
|
385
|
+
)
|
|
386
|
+
return Response(status=200)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Rust input path with `FastJSONParser`
|
|
390
|
+
|
|
391
|
+
Stock DRF parses JSON into a Python dict, then `is_valid` re-walks that
|
|
392
|
+
dict to validate it. `FastJSONParser` skips the first pass and hands
|
|
393
|
+
raw request bytes directly to pydantic-core's `validate_json`. Add it
|
|
394
|
+
to `parser_classes` per view, or to `DEFAULT_PARSER_CLASSES` globally.
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
class TxnCreateView(APIView):
|
|
398
|
+
parser_classes = [FastJSONParser]
|
|
399
|
+
renderer_classes = [FastJSONRenderer]
|
|
400
|
+
|
|
401
|
+
def post(self, request):
|
|
402
|
+
serializer = TxnIn.drf(data=request.data)
|
|
403
|
+
serializer.is_valid(raise_exception=True)
|
|
404
|
+
Txn.objects.create(**serializer.validated_data.model_dump())
|
|
405
|
+
return Response(status=201)
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Middleware or tests that read `request.data` still get a dict. The
|
|
409
|
+
parser returns a lazy-decoded proxy that triggers `json.loads` on the
|
|
410
|
+
first non-validator access.
|
|
411
|
+
|
|
412
|
+
### `values_fields()` projection helper
|
|
413
|
+
|
|
414
|
+
Spell the queryset projection once, on the schema:
|
|
415
|
+
|
|
416
|
+
```python
|
|
417
|
+
qs = Txn.objects.values(*TxnOut.values_fields())
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Returns the field names declared on `TxnOut` in declaration order.
|
|
421
|
+
Pass `exclude=(...)` to drop specific fields.
|
|
422
|
+
|
|
423
|
+
### Explicit factory
|
|
424
|
+
|
|
425
|
+
Prefer an explicit import over the `.drf` descriptor? Use `drf_serializer`:
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
from drf_fastserializers import drf_serializer
|
|
429
|
+
|
|
430
|
+
class TxnListView(ListAPIView):
|
|
431
|
+
serializer_class = drf_serializer(TxnOut)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Both forms return the same cached `DRFAdapter` subclass.
|
|
435
|
+
|
|
436
|
+
## OpenAPI schemas via drf-spectacular
|
|
437
|
+
|
|
438
|
+
Install the extra and import the extension module once during app
|
|
439
|
+
startup, typically in your `AppConfig.ready()`:
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
pip install 'drf-fastserializers[spectacular]'
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
# myapp/apps.py
|
|
447
|
+
class MyAppConfig(AppConfig):
|
|
448
|
+
name = "myapp"
|
|
449
|
+
|
|
450
|
+
def ready(self):
|
|
451
|
+
import drf_fastserializers.spectacular # noqa: F401
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Every `FastSerializer`-backed view is then rendered into the OpenAPI
|
|
455
|
+
schema using `model_json_schema()`. Nested models, enum choices,
|
|
456
|
+
validators, and `@computed_field`s all carry through. No
|
|
457
|
+
`@extend_schema` boilerplate needed for the common case.
|
|
458
|
+
|
|
459
|
+
## How it works
|
|
460
|
+
|
|
461
|
+
Stock DRF serializers iterate field objects in Python on every
|
|
462
|
+
response. That overhead dominates response time for endpoints returning
|
|
463
|
+
thousands of rows.
|
|
464
|
+
|
|
465
|
+
`drf-fastserializers` skips DRF's field pipeline. On `.data` access the
|
|
466
|
+
serializer hands back a `FastPayload` marker carrying the validated
|
|
467
|
+
pydantic instances plus a `TypeAdapter`. `FastJSONRenderer` recognizes
|
|
468
|
+
the marker and routes encoding to `TypeAdapter.dump_json`, which is
|
|
469
|
+
implemented in Rust as part of
|
|
470
|
+
[pydantic-core](https://github.com/pydantic/pydantic-core). Bytes go
|
|
471
|
+
straight to the HTTP response. No Python-side `json.dumps` step.
|
|
472
|
+
|
|
473
|
+
```mermaid
|
|
474
|
+
flowchart LR
|
|
475
|
+
View["DRF View"] --> Data["serializer.data"]
|
|
476
|
+
Data --> Marker["FastPayload<br/>(adapter + instances)"]
|
|
477
|
+
Marker --> Renderer["FastJSONRenderer"]
|
|
478
|
+
Renderer -->|Rust dump_json| Bytes[("bytes")]
|
|
479
|
+
Bytes --> Response["HTTP Response"]
|
|
480
|
+
|
|
481
|
+
classDef rust fill:#dea584,stroke:#8b4513,color:#000
|
|
482
|
+
classDef drf fill:#a30000,stroke:#600,color:#fff
|
|
483
|
+
class Renderer,Marker rust
|
|
484
|
+
class View,Data,Response drf
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
For payloads the renderer doesn't recognize (error responses, plain
|
|
488
|
+
dicts, the browsable API, paginated wrappers), it falls back to stock
|
|
489
|
+
`JSONRenderer`. The library never breaks code paths it doesn't
|
|
490
|
+
explicitly handle.
|
|
491
|
+
|
|
492
|
+
## Pydantic v2 and v3
|
|
493
|
+
|
|
494
|
+
Built against the stable pydantic surface used by both v2.7+ and v3.x
|
|
495
|
+
(`BaseModel`, `TypeAdapter`, `ConfigDict`, `ValidationError`,
|
|
496
|
+
`model_dump_json`). The pyproject spec is `pydantic>=2.7,<4`. The
|
|
497
|
+
`PYD_V3` flag is exposed for downstream code that needs to branch.
|
|
498
|
+
|
|
499
|
+
## What this library is *not*
|
|
500
|
+
|
|
501
|
+
* Not a `ModelSerializer` replacement that auto-infers fields from a
|
|
502
|
+
Django model. Use `from_drf(MyModelSerializer)` to lift an existing
|
|
503
|
+
one, or declare fields explicitly in a `FastSerializer`.
|
|
504
|
+
* Not a framework. Keep your DRF generics, viewsets, routers,
|
|
505
|
+
permissions, auth backends, throttling, filtering. Only the
|
|
506
|
+
serializer and renderer paths change. Migrate one endpoint at a time.
|
|
507
|
+
* Not a drf-spectacular replacement. For the common case, install the
|
|
508
|
+
`[spectacular]` extra and let `model_json_schema()` flow through;
|
|
509
|
+
for anything custom, declare response shapes with `@extend_schema`.
|
|
510
|
+
|
|
511
|
+
## License
|
|
512
|
+
|
|
513
|
+
MIT
|