# How Serializers Are Used in the Plane API: A Complete DRF Implementation Guide

> Learn how Plane API uses Django REST Framework serializers for model-to-JSON conversion and input validation. Explore BaseSerializer and DynamicBaseSerializer classes.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: how-to-guide
- Published: 2026-08-23

---

**Plane uses Django REST Framework serializers located in `apps/api/plane/app/serializers/` to handle model-to-JSON conversion, input validation, and dynamic field expansion through a hierarchy of `BaseSerializer` and `DynamicBaseSerializer` classes.**

Plane is an open-source project management platform built on Django REST Framework (DRF). Every HTTP request and response flows through a family of serializer classes that enforce schema consistency, validate business rules, and optimize payload size through selective field expansion.

## The Serializer Hierarchy: BaseSerializer and DynamicBaseSerializer

All Plane API serializers inherit from a custom foundation defined in [`apps/api/plane/app/serializers/base.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/serializers/base.py). This architecture provides consistent behavior across the entire API surface while enabling advanced response customization.

### BaseSerializer Foundation

The `BaseSerializer` class serves as the universal parent for all model serializers in Plane. It extends DRF's `ModelSerializer` to automatically inject a read-only `id` UUID field and establish common metadata patterns. When you examine [`apps/api/plane/app/serializers/issue.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/serializers/issue.py), you see that `IssueSerializer`, `IssueCreateSerializer`, and `IssueDetailSerializer` all ultimately trace back to this base class.

### Dynamic Field Control with DynamicBaseSerializer

Plane implements sophisticated response shaping through `DynamicBaseSerializer`, located in the same base module. This class intercepts the `fields` and `expand` arguments passed from view code to either prune unnecessary fields or replace foreign-key IDs with fully serialized nested objects. For example, passing `expand=['project', 'assignees']` to `IssueSerializer` triggers `DynamicBaseSerializer` to swap the `project` UUID for a complete `ProjectLiteSerializer` representation and hydrate the `assignees` list with `UserLiteSerializer` objects.

## Core Responsibilities of Plane API Serializers

The serializers used in the Plane API perform three distinct architectural jobs: data marshalling, validation, and dynamic expansion.

### Model-to-JSON Conversion

Each serializer declares a `Meta` inner class specifying the Django model and field whitelist. In [`apps/api/plane/app/serializers/issue.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/serializers/issue.py), the `IssueSerializer` maps the `Issue` model to a comprehensive JSON payload suitable for frontend consumption. The `to_representation` method—provided by DRF but customized through inheritance—handles the actual conversion of Python model instances to JSON-compatible dictionaries.

### Input Validation and Business Rules

Plane serializers enforce complex constraints through custom `validate_*` methods and an overall `validate` method. The `IssueCreateSerializer` demonstrates this pattern by:
- Validating that start dates precede target dates
- Sanitizing HTML content via `validate_html_content` to prevent XSS attacks
- Validating URLs using Django's `URLValidator`
- Translating many-to-many ID lists (assignees, labels) into actual model relationships during the `create` or `update` cycle

System-controlled fields like `workspace` and `created_by` are protected using `Meta.read_only_fields`, ensuring clients cannot modify audit columns.

### Dynamic Field Selection and Expansion

For performance optimization, Plane supports field-level pruning through the `fields` parameter and relationship expansion via `expand`. When a view passes `fields=['id', 'name', 'state_id']` to `IssueSerializer`, `DynamicBaseSerializer` removes all other fields from the output. Conversely, when `expand=['project']` is requested, the serializer replaces the flat `project` UUID with a nested object generated by `ProjectLiteSerializer` from [`apps/api/plane/app/serializers/project.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/serializers/project.py).

## Architectural Flow: From Request to Response

Understanding how serializers integrate with views clarifies the data lifecycle:

1. **View receives a request** (e.g., `GET /api/issues/` or `POST /api/issues/`)
2. **View instantiates the appropriate serializer** (`IssueSerializer` for reads, `IssueCreateSerializer` for writes)
3. **For GET requests**: The serializer's `to_representation` method converts model instances to dictionaries. If expansion is requested, `DynamicBaseSerializer` injects nested serializers like `WorkspaceLiteSerializer` or `UserLiteSerializer`
4. **For POST/PUT requests**: The serializer first executes `validate` logic, then calls `create` or `update` methods which persist the model and handle bulk operations for related many-to-many objects (assignees, labels) to minimize database round-trips

## Security and Performance Patterns

Plane serializers implement several critical security and optimization measures:

- **HTML Sanitization**: All rich text fields pass through `validate_html_content` to strip malicious tags
- **Computed Fields**: `SerializerMethodField` and `ListField` generate calculated values like `sub_issues_count` without database storage
- **Write-Only Fields**: Sensitive inputs use write-only configurations to prevent leakage in responses
- **Batch Operations**: The `create` and `update` methods in `IssueCreateSerializer` handle bulk assignment of labels and assignees using `bulk_create` rather than iterative saves

## Practical Code Examples

### Serializing an Issue for a GET Response

```python
from plane.app.serializers import IssueSerializer
from plane.db.models import Issue

issue = Issue.objects.select_related('project', 'state').get(pk=uuid)
serializer = IssueSerializer(issue, expand=['project', 'assignees'])
json_payload = serializer.data

# → {'id':..., 'name':..., 'project_detail': {...}, 'assignee_details': [{...}, …], ...}

```

### Creating a New Issue via POST

```python
from plane.app.serializers import IssueCreateSerializer

payload = {
    "name": "New feature",
    "description_html": "<p>Details</p>",
    "assignee_ids": ["a1b2c3..."],
    "label_ids": ["d4e5f6..."],
    "state_id": "1234-...",
}
serializer = IssueCreateSerializer(data=payload, context={
    "project_id": project.id,
    "workspace_id": workspace.id,
    "default_assignee_id": default_user.id,
    "allow_triage_state": True,
})
serializer.is_valid(raise_exception=True)
issue = serializer.save()

```

### Using Dynamic Field Selection

```python

# Only return a subset of fields, useful for lightweight list endpoints

serializer = IssueSerializer(issue, fields=['id', 'name', 'state_id'])
compact_json = serializer.data

# → {'id':..., 'name':..., 'state_id':...}

```

### Expanding Related Objects on the Fly

```python
serializer = IssueSerializer(issue, expand=['project', 'labels'])
expanded = serializer.data

# The `project_detail` field now contains the full ProjectLiteSerializer output

# The `labels` field is a list of full LabelSerializer objects

```

## Summary

- **All Plane API payloads flow through DRF serializers** located in `apps/api/plane/app/serializers/`, with base functionality defined in [`base.py`](https://github.com/makeplane/plane/blob/main/base.py)
- **Three core responsibilities** govern the serializer pattern: model-to-JSON conversion, business rule validation, and dynamic field expansion
- **Security is enforced at the serializer layer** through HTML sanitization, URL validation, and read-only field restrictions
- **Performance optimization** occurs via `DynamicBaseSerializer`, which supports field pruning and relationship expansion without separate API endpoints
- **Bulk operations** are handled within `create` and `update` methods to minimize database queries when saving complex objects with many-to-many relationships

## Frequently Asked Questions

### How does Plane handle nested object expansion in API responses?

Plane uses `DynamicBaseSerializer` in [`apps/api/plane/app/serializers/base.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/serializers/base.py) to intercept the `expand` parameter. When a view passes a list like `expand=['project', 'assignees']`, the serializer replaces foreign-key UUIDs with fully serialized representations using `ProjectLiteSerializer` or `UserLiteSerializer`. This occurs in the `to_representation` method, allowing clients to request rich payloads or minimal IDs based on their needs.

### What validation mechanisms protect against malicious input in Plane serializers?

The serializers implement multiple validation layers. Custom `validate_html_content` methods scrub HTML to prevent XSS attacks, while Django's `URLValidator` ensures link integrity. The `validate` method enforces business constraints like date ordering (start date must precede target date), and `Meta.read_only_fields` protects system columns from client modification. Additionally, `IssueCreateSerializer` validates that foreign-key references belong to the requesting user's workspace.

### Can I customize which fields are returned by Plane API endpoints without creating new serializers?

Yes. The `DynamicBaseSerializer` supports dynamic field selection through the `fields` parameter. When instantiating any serializer descended from this base class, pass `fields=['id', 'name', 'state_id']` to receive only those specific attributes. This mechanism is used throughout the codebase to create lightweight list endpoints and detailed single-resource views from the same serializer class.

### Where are export-specific serializers defined in the Plane codebase?

Export-oriented serializers reside in [`apps/api/plane/utils/porters/serializers/issue.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/porters/serializers/issue.py). These specialized classes handle CSV and XLSX generation by formatting data specifically for spreadsheet consumption rather than JSON API responses. The exporter framework in [`apps/api/plane/utils/porters/exporter.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/porters/exporter.py) demonstrates how these serializers plug into DRF's serialization architecture while targeting flat-file output formats.