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.
@@ -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