How to write serializers
This guide provides comprehensive patterns and best practices for writing serializers in Waldur MasterMind, based on analysis of the current codebase architecture.
Core Serializer Architecture Principles
Mixin-Based Composition
Waldur uses extensive mixin composition to build complex serializers with reusable functionality. The recommended order follows Python's Method Resolution Order (MRO):
1 2 3 4 5 6 7 |
|
Key Mixin Classes
- AugmentedSerializerMixin: Core functionality for signal injection and related fields
- RestrictedSerializerMixin: Field-level control to avoid over-fetching
- PermissionFieldFilteringMixin: Security filtering based on user permissions
- SlugSerializerMixin: Slug field management with staff-only editing
- CountrySerializerMixin: Internationalization support
Object Identity and HATEOAS
UUID-Based Identity
All objects are identified by UUIDs rather than database IDs for distributed database support:
1 2 3 4 5 6 |
|
Consistent URL Patterns
- Detail views:
{model_name}-detail
- List views:
{model_name}-list
- Custom actions:
{model_name}-{action}
1 2 3 4 5 6 |
|
Automatic Related Field Generation
Related Paths Pattern
Use related_paths
to automatically generate related object fields:
1 2 3 4 5 6 7 8 9 10 11 |
|
This automatically generates: customer_uuid
, customer_name
, customer_native_name
, customer_abbreviation
, etc.
Security and Permissions
Permission-Based Field Filtering
Always use PermissionFieldFilteringMixin
for related fields to ensure users can only reference objects they have access to:
1 2 3 |
|
Permission List Serializers
For many=True
relationships, use PermissionListSerializer
:
1 2 3 |
|
Staff-Only Fields
Restrict sensitive fields to staff users:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Protected Fields
Use protected_fields
to make fields read-only during updates:
1 2 |
|
Performance Optimization
Eager Loading
Always implement eager_load()
static methods for query optimization:
1 2 3 4 5 6 7 8 9 |
|
RestrictedSerializerMixin
Documentation
The RestrictedSerializerMixin
provides a powerful and flexible way to dynamically control which fields are rendered by a Django REST Framework serializer based on query parameters in the request URL. This is especially useful for optimizing API responses, reducing payload size, and allowing API clients to fetch only the data they need.
The mixin supports two primary modes of operation:
- Restricted Field Rendering (Whitelisting): The client specifies exactly which fields they want, and all others are excluded.
- Optional Fields (Blacklisting by Default): The serializer defines certain "expensive" or non-essential fields that are excluded by default but can be explicitly requested by the client.
Basic Usage
To use the mixin, simply add it to your serializer's inheritance list. The mixin requires the request
object to be in the serializer's context, which DRF views typically provide automatically.
1 2 3 4 5 6 7 |
|
Feature 1: Restricted Field Rendering (Whitelisting)
This is the primary feature. By adding the ?field=
query parameter to the URL, an API client can request a specific subset of fields. The serializer will only render the fields present in the field
parameters.
Example:
Imagine a CustomerSerializer
with the fields uuid
, name
, email
, and created
.
To request only the name
and uuid
of a customer:
URL: /api/customers/123/?field=name&field=uuid
Expected JSON Response:
1 2 3 4 |
|
Feature 2: Optional Fields (Blacklisting by Default)
Some fields can be expensive to compute (e.g., involving extra database queries, aggregations, or external API calls). You can mark these fields as "optional" by overriding the get_optional_fields
method. These fields will not be included in the response unless they are explicitly requested via the ?field=
parameter.
Example:
Let's add projects
(a related field) and billing_price_estimate
(a computed field) to our serializer and mark them as optional.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Behavior Examples
Standard Request
URL: /api/customers/123/
Result: The optional fields (projects
, billing_price_estimate
) are excluded. The expensive get_billing_price_estimate
method is never called.
1 2 3 4 5 6 |
|
Requesting Optional Fields
URL: /api/customers/123/?field=name&field=projects
Result: The response is restricted to name
, and the optional field projects
is included because it was requested.
1 2 3 4 5 6 7 |
|
Advanced Behavior
Nested Serializers
The RestrictedSerializerMixin
is designed to be "nesting-aware." It will only apply its filtering logic to the top-level serializer in a request. Any nested serializers will be rendered completely, ignoring the ?field=
parameters from the URL. This prevents unintentional and undesirable filtering of nested data structures.
Example: A ProjectSerializer
that includes a nested CustomerSerializer
.
URL: /api/projects/abc/?field=name&field=customer
Expected JSON Response: The ProjectSerializer
is filtered to name
and customer
. The nested CustomerSerializer
, however, renders all of its fields (excluding its own optional fields, of course), because it is not the top-level serializer.
1 2 3 4 5 6 7 8 9 |
|
List Views (many=True
)
The mixin works seamlessly with list views. The field filtering is applied individually to each object in the list.
Example:
URL: /api/customers/?field=uuid&field=name
Expected JSON Response:
1 2 3 4 5 6 7 8 9 10 |
|
Complex Validation Patterns
Hierarchical Validation
Implement validation in layers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Dynamic Field Behavior
Use get_fields()
for context-dependent field behavior:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
External API Integration
For external validation (e.g., VAT numbers):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Service Configuration Patterns
Options Pattern for Flexible Configuration
Use the options pattern for service-specific configuration without model changes:
1 2 3 4 5 6 7 8 |
|
Secret Field Management
Protect sensitive configuration data:
1 2 |
|
Complex Resource Orchestration
Transactional Resource Creation
For resources that create multiple related objects:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Advanced Serializer Patterns
Nested Resource Serializers
For complex relationships:
1 2 3 4 5 6 7 8 |
|
Generic Relationships
For polymorphic relationships:
1 2 3 4 5 6 7 8 9 10 |
|
Signal-Based Field Injection
Extensible Serializers
Avoid circular dependencies by using signals for field injection:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Standard Meta Class Configuration
Complete Meta Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Custom Field Types
Specialized Fields
- HTMLCleanField: Automatically sanitizes HTML content
- DictSerializerField: Handles JSON dictionary serialization
- GenericRelatedField: Supports multiple model types in relations
- MappedChoiceField: Maps choice values for API consistency
1 2 3 4 5 6 7 |
|
Testing Serializers
Factory-Based Testing
Use factory classes for test data generation:
1 2 3 4 5 6 7 8 |
|
Permission Testing
Test permission-based filtering:
1 2 3 4 5 6 7 8 |
|
Common Pitfalls and Best Practices
Do's
- Always use UUID lookup fields for all hyperlinked relationships
- Implement eager_load() for any serializer used in list views
- Use PermissionFieldFilteringMixin for all related fields
- Follow the mixin order for consistent behavior
- Use related_paths for automatic related field generation
- Implement comprehensive validation at multiple levels
- Use transactions for multi-resource creation
- Mark expensive fields as optional
Don'ts
- Don't use
fields = '__all__'
- always be explicit - Don't forget lookup_field='uuid' in extra_kwargs
- Don't skip permission filtering for security-sensitive fields
- Don't implement custom field logic without using established patterns
- Don't create circular dependencies - use signal injection instead
- Don't ignore performance - always consider query optimization
- Don't hardcode view names - use consistent naming patterns
Migration from Legacy Patterns
Updating Existing Serializers
When updating legacy serializers:
- Add missing mixins in the correct order
- Implement
eager_load()
static methods - Add
related_paths
for automatic field generation - Add permission filtering with
get_filtered_field_names()
- Use
protected_fields
instead of custom read-only logic - Update to use
lookup_field='uuid'
consistently
This comprehensive guide provides the patterns and practices needed to write maintainable, secure, and performant serializers that follow Waldur's architectural conventions.