0
# Data Transfer Objects (DTOs)
1
2
Serialization and validation system using DTOs for request/response data transformation. Litestar's DTO system supports dataclasses, Pydantic models, and msgspec structs with automatic validation and conversion.
3
4
## Capabilities
5
6
### Abstract DTO Base Class
7
8
Base class for all DTO implementations providing common functionality.
9
10
```python { .api }
11
class AbstractDTO:
12
config: DTOConfig
13
14
@classmethod
15
def create_for_field_definition(
16
cls,
17
field_definition: FieldDefinition,
18
config: DTOConfig | None = None,
19
) -> type[AbstractDTO]:
20
"""
21
Create a DTO class for a field definition.
22
23
Parameters:
24
- field_definition: Field definition to create DTO for
25
- config: Optional DTO configuration
26
27
Returns:
28
DTO class configured for the field definition
29
"""
30
31
@classmethod
32
def generate_field_definitions(
33
cls,
34
model_type: type[Any],
35
) -> Generator[DTOFieldDefinition, None, None]:
36
"""Generate field definitions from model type."""
37
38
def decode_bytes(self, raw: bytes) -> Any:
39
"""Decode bytes to Python object."""
40
41
def decode_builtins(self, builtins: dict[str, Any] | list[Any]) -> Any:
42
"""Decode builtin types to Python object."""
43
44
def encode_data(self, data: Any) -> dict[str, Any] | list[dict[str, Any]]:
45
"""Encode Python object to serializable data."""
46
```
47
48
### DTO Configuration
49
50
Configuration class for customizing DTO behavior.
51
52
```python { .api }
53
class DTOConfig:
54
def __init__(
55
self,
56
*,
57
exclude: set[str] | None = None,
58
include: set[str] | None = None,
59
rename_fields: dict[str, str] | None = None,
60
forbid_unknown_fields: bool = False,
61
max_nested_depth: int = 1,
62
partial: bool = False,
63
underscore_fields_private: bool = True,
64
experimental_features: list[DTOConfigFeatures] | None = None,
65
):
66
"""
67
Configure DTO behavior.
68
69
Parameters:
70
- exclude: Fields to exclude from serialization
71
- include: Fields to include in serialization (mutually exclusive with exclude)
72
- rename_fields: Mapping of field names to rename
73
- forbid_unknown_fields: Reject unknown fields in input data
74
- max_nested_depth: Maximum depth for nested object serialization
75
- partial: Allow partial updates (fields become optional)
76
- underscore_fields_private: Treat underscore-prefixed fields as private
77
- experimental_features: List of experimental features to enable
78
"""
79
80
def create_for_field_definition(
81
self,
82
field_definition: FieldDefinition,
83
) -> DTOConfig:
84
"""Create config for specific field definition."""
85
```
86
87
### Built-in DTO Implementations
88
89
Pre-built DTO classes for common Python data structures.
90
91
```python { .api }
92
class DataclassDTO(AbstractDTO):
93
"""DTO implementation for dataclasses."""
94
95
@classmethod
96
def create_for_field_definition(
97
cls,
98
field_definition: FieldDefinition,
99
config: DTOConfig | None = None,
100
) -> type[DataclassDTO]:
101
"""Create DataclassDTO for field definition."""
102
103
class MsgspecDTO(AbstractDTO):
104
"""DTO implementation for msgspec Structs."""
105
106
@classmethod
107
def create_for_field_definition(
108
cls,
109
field_definition: FieldDefinition,
110
config: DTOConfig | None = None,
111
) -> type[MsgspecDTO]:
112
"""Create MsgspecDTO for field definition."""
113
```
114
115
### DTO Data Structures
116
117
Core data structures used by the DTO system.
118
119
```python { .api }
120
class DTOData:
121
def __init__(
122
self,
123
*,
124
data_as_builtins: dict[str, Any] | list[dict[str, Any]],
125
data_as_bytes: bytes,
126
data_as_model_instance: Any,
127
):
128
"""
129
Container for DTO data in different formats.
130
131
Parameters:
132
- data_as_builtins: Data as built-in Python types
133
- data_as_bytes: Data as encoded bytes
134
- data_as_model_instance: Data as model instance
135
"""
136
137
@property
138
def as_builtins(self) -> dict[str, Any] | list[dict[str, Any]]:
139
"""Get data as built-in Python types."""
140
141
@property
142
def as_bytes(self) -> bytes:
143
"""Get data as encoded bytes."""
144
145
@property
146
def as_model_instance(self) -> Any:
147
"""Get data as model instance."""
148
149
class DTOFieldDefinition:
150
def __init__(
151
self,
152
dto_field: DTOField,
153
model_name: str,
154
field_definition: FieldDefinition | None = None,
155
default_value: Any = Empty,
156
):
157
"""
158
Definition of a DTO field.
159
160
Parameters:
161
- dto_field: DTO field configuration
162
- model_name: Name of the model this field belongs to
163
- field_definition: Optional field definition
164
- default_value: Default value for the field
165
"""
166
167
@property
168
def serialization_name(self) -> str:
169
"""Get the serialization name for this field."""
170
171
@property
172
def is_required(self) -> bool:
173
"""Check if this field is required."""
174
175
@property
176
def is_excluded(self) -> bool:
177
"""Check if this field is excluded from serialization."""
178
```
179
180
### DTO Field Configuration
181
182
Field-level configuration and marking utilities.
183
184
```python { .api }
185
class DTOField:
186
def __init__(
187
self,
188
mark: Mark | None = None,
189
default: Any = Empty,
190
):
191
"""
192
DTO field configuration.
193
194
Parameters:
195
- mark: Field marking for inclusion/exclusion
196
- default: Default value for the field
197
"""
198
199
def dto_field(
200
mark: Mark | None = None,
201
default: Any = Empty,
202
) -> Any:
203
"""
204
Create a DTO field with configuration.
205
206
Parameters:
207
- mark: Field marking for inclusion/exclusion
208
- default: Default value for the field
209
210
Returns:
211
DTO field configuration
212
"""
213
214
class Mark(Enum):
215
"""Field marking for DTO behavior."""
216
READ_ONLY = "read_only"
217
WRITE_ONLY = "write_only"
218
PRIVATE = "private"
219
```
220
221
### Rename Strategies
222
223
Strategies for field name transformation during serialization.
224
225
```python { .api }
226
class RenameStrategy(str, Enum):
227
"""Field renaming strategies."""
228
UPPER = "upper"
229
LOWER = "lower"
230
CAMEL = "camel"
231
PASCAL = "pascal"
232
233
def convert_case(value: str, strategy: RenameStrategy) -> str:
234
"""
235
Convert field name case according to strategy.
236
237
Parameters:
238
- value: Original field name
239
- strategy: Renaming strategy to apply
240
241
Returns:
242
Transformed field name
243
"""
244
```
245
246
## Usage Examples
247
248
### Basic Dataclass DTO
249
250
```python
251
from litestar import Litestar, post, get
252
from litestar.dto import DataclassDTO, DTOConfig
253
from dataclasses import dataclass
254
from typing import Optional
255
256
@dataclass
257
class User:
258
name: str
259
email: str
260
age: int
261
id: Optional[int] = None
262
263
# Create DTO for User dataclass
264
UserDTO = DataclassDTO[User]
265
266
@post("/users", dto=UserDTO)
267
def create_user(data: User) -> User:
268
# data is automatically validated and converted from request JSON
269
# Generate ID for new user
270
data.id = 123
271
return data
272
273
@get("/users/{user_id:int}", return_dto=UserDTO)
274
def get_user(user_id: int) -> User:
275
# Response is automatically serialized using DTO
276
return User(id=user_id, name="Alice", email="alice@example.com", age=30)
277
278
app = Litestar(route_handlers=[create_user, get_user])
279
```
280
281
### DTO Configuration with Field Exclusion
282
283
```python
284
from litestar.dto import DataclassDTO, DTOConfig
285
286
@dataclass
287
class UserWithPassword:
288
name: str
289
email: str
290
password: str
291
created_at: datetime
292
id: Optional[int] = None
293
294
# Exclude sensitive fields from serialization
295
read_dto_config = DTOConfig(exclude={"password"})
296
UserReadDTO = DataclassDTO[UserWithPassword].with_config(read_dto_config)
297
298
# Only include necessary fields for creation
299
write_dto_config = DTOConfig(include={"name", "email", "password"})
300
UserWriteDTO = DataclassDTO[UserWithPassword].with_config(write_dto_config)
301
302
@post("/users", dto=UserWriteDTO, return_dto=UserReadDTO)
303
def create_user_secure(data: UserWithPassword) -> UserWithPassword:
304
# Input: only name, email, password allowed
305
# Output: password excluded, all other fields included
306
data.id = 123
307
data.created_at = datetime.utcnow()
308
return data
309
```
310
311
### Field Renaming and Case Conversion
312
313
```python
314
from litestar.dto import DTOConfig, RenameStrategy
315
316
@dataclass
317
class APIResponse:
318
user_id: int
319
full_name: str
320
email_address: str
321
is_active: bool
322
323
# Convert snake_case to camelCase for API
324
camel_case_config = DTOConfig(
325
rename_fields={
326
"user_id": "userId",
327
"full_name": "fullName",
328
"email_address": "emailAddress",
329
"is_active": "isActive"
330
}
331
)
332
APIResponseDTO = DataclassDTO[APIResponse].with_config(camel_case_config)
333
334
@get("/api/user/{user_id:int}", return_dto=APIResponseDTO)
335
def get_user_api(user_id: int) -> APIResponse:
336
return APIResponse(
337
user_id=user_id,
338
full_name="Alice Smith",
339
email_address="alice@example.com",
340
is_active=True
341
)
342
# Response JSON: {"userId": 123, "fullName": "Alice Smith", ...}
343
```
344
345
### Partial Updates with DTOs
346
347
```python
348
from litestar.dto import DTOConfig
349
350
@dataclass
351
class User:
352
name: str
353
email: str
354
age: int
355
id: int
356
357
# Allow partial updates - all fields become optional
358
partial_config = DTOConfig(partial=True)
359
UserPartialDTO = DataclassDTO[User].with_config(partial_config)
360
361
@patch("/users/{user_id:int}", dto=UserPartialDTO, return_dto=DataclassDTO[User])
362
def update_user(user_id: int, data: User) -> User:
363
# Only provided fields will be present in data
364
# Merge with existing user data
365
existing_user = get_user_from_db(user_id)
366
367
# Update only provided fields
368
if hasattr(data, 'name') and data.name is not None:
369
existing_user.name = data.name
370
if hasattr(data, 'email') and data.email is not None:
371
existing_user.email = data.email
372
if hasattr(data, 'age') and data.age is not None:
373
existing_user.age = data.age
374
375
return existing_user
376
```
377
378
### Nested Object Serialization
379
380
```python
381
from dataclasses import dataclass
382
from typing import List
383
384
@dataclass
385
class Address:
386
street: str
387
city: str
388
country: str
389
390
@dataclass
391
class Company:
392
name: str
393
address: Address
394
395
@dataclass
396
class Employee:
397
name: str
398
company: Company
399
addresses: List[Address]
400
401
# Configure nested depth
402
nested_config = DTOConfig(max_nested_depth=2)
403
EmployeeDTO = DataclassDTO[Employee].with_config(nested_config)
404
405
@get("/employees/{emp_id:int}", return_dto=EmployeeDTO)
406
def get_employee(emp_id: int) -> Employee:
407
return Employee(
408
name="John Doe",
409
company=Company(
410
name="Tech Corp",
411
address=Address("123 Main St", "City", "Country")
412
),
413
addresses=[
414
Address("456 Home St", "Home City", "Country"),
415
Address("789 Vacation Ave", "Vacation City", "Country")
416
]
417
)
418
```
419
420
### Msgspec Integration
421
422
```python
423
import msgspec
424
from litestar.dto import MsgspecDTO
425
426
@msgspec.defstruct
427
class Product:
428
name: str
429
price: float
430
category: str
431
in_stock: bool = True
432
433
ProductDTO = MsgspecDTO[Product]
434
435
@post("/products", dto=ProductDTO, return_dto=ProductDTO)
436
def create_product(data: Product) -> Product:
437
# msgspec provides very fast serialization
438
return data
439
440
@get("/products", return_dto=MsgspecDTO[List[Product]])
441
def list_products() -> List[Product]:
442
return [
443
Product("Laptop", 999.99, "Electronics"),
444
Product("Book", 29.99, "Education", False)
445
]
446
```
447
448
### Custom DTO Implementation
449
450
```python
451
from litestar.dto import AbstractDTO, DTOConfig
452
from typing import TypeVar, Generic
453
454
T = TypeVar("T")
455
456
class CustomDTO(AbstractDTO, Generic[T]):
457
"""Custom DTO with additional validation."""
458
459
def decode_builtins(self, builtins: dict[str, Any] | list[Any]) -> T:
460
# Custom deserialization logic
461
data = super().decode_builtins(builtins)
462
463
# Add custom validation
464
if hasattr(data, 'email') and '@' not in data.email:
465
raise ValueError("Invalid email format")
466
467
return data
468
469
def encode_data(self, data: T) -> dict[str, Any] | list[dict[str, Any]]:
470
# Custom serialization logic
471
encoded = super().encode_data(data)
472
473
# Add metadata
474
if isinstance(encoded, dict):
475
encoded['_serialized_at'] = datetime.utcnow().isoformat()
476
477
return encoded
478
479
@post("/custom", dto=CustomDTO[User])
480
def create_with_custom_dto(data: User) -> dict:
481
return {"status": "created", "user": data}
482
```
483
484
### Validation and Error Handling
485
486
```python
487
from litestar.exceptions import ValidationException
488
489
@dataclass
490
class CreateUserRequest:
491
name: str
492
email: str
493
age: int
494
495
def __post_init__(self):
496
# Custom validation in dataclass
497
if not self.name.strip():
498
raise ValueError("Name cannot be empty")
499
if self.age < 0:
500
raise ValueError("Age must be positive")
501
if '@' not in self.email:
502
raise ValueError("Invalid email format")
503
504
UserCreateDTO = DataclassDTO[CreateUserRequest]
505
506
@post("/users/validated", dto=UserCreateDTO)
507
def create_user_validated(data: CreateUserRequest) -> dict:
508
# DTO automatically handles validation
509
# Any validation errors are converted to ValidationException
510
return {"message": f"User {data.name} created successfully"}
511
512
# Custom exception handler for DTO validation errors
513
def dto_validation_handler(request: Request, exc: ValidationException) -> Response:
514
return Response(
515
content={
516
"error": "Validation failed",
517
"details": exc.extra,
518
"message": exc.detail
519
},
520
status_code=422
521
)
522
```
523
524
## Types
525
526
```python { .api }
527
# Generic DTO type
528
DTOType = TypeVar("DTOType", bound=AbstractDTO)
529
530
# DTO configuration features
531
class DTOConfigFeatures(str, Enum):
532
EXPERIMENTAL_GENERIC_SUPPORT = "experimental_generic_support"
533
534
# Field definition types
535
FieldDefinition = Any # From litestar._signature module
536
537
# Default values
538
class Empty:
539
"""Sentinel value for empty/unset fields."""
540
541
# Encoding types
542
EncodedData = bytes | str | dict[str, Any] | list[dict[str, Any]]
543
DecodedData = Any
544
```