Skip to content

Manuals — Serializers

Overview

The serializers layer in the manuals app defines the API contract for:

  • taxonomy data
  • manuals and chapters
  • sections and notes
  • step groups and steps
  • callouts
  • media uploads
  • ordered chapter items

It is responsible for:

  • input validation
  • output shaping
  • lightweight normalization
  • calling service-layer orchestration for nested writes

It is not responsible for:

  • deep sync logic
  • nested content orchestration
  • media cleanup workflows
  • PDF generation
  • storage lifecycle behavior

Those responsibilities belong to the services layer.


Serializer package structure

The serializers package is split by concern:

  • common.py
  • taxonomy.py
  • media.py
  • callouts.py
  • steps.py
  • sections.py
  • items.py
  • chapters.py
  • manuals.py

This keeps read/write contracts easier to understand and maintain.


High-level dependency flow

flowchart TD

Common --> Taxonomy
Media --> Steps
Media --> Sections
Callouts --> Steps
Callouts --> Sections
Sections --> Items
Callouts --> Items
Steps --> Items
Media --> Chapters
Callouts --> Chapters
Items --> Chapters
Taxonomy --> Manuals
Common --> Manuals
Media --> Manuals
Callouts --> Manuals
Chapters --> Manuals

Common serializers

OrganizationBriefSerializer

Location:

  • serializers/common.py

Purpose:

  • lightweight organization representation inside manual responses

Fields:

  • id
  • name
  • slug

Used by:

  • ManualListSerializer
  • ManualDetailSerializer

Taxonomy serializers

CategorySerializer

Purpose:

  • serialize manual categories

Fields:

  • id
  • name
  • slug

TopicSerializer

Purpose:

  • serialize manual topics

Fields:

  • id
  • name
  • slug
  • color

Media serializers

ManualMediaSerializer

Purpose:

  • read serializer for attached media

Fields:

  • id
  • file
  • file_url
  • caption
  • type
  • order

Notes

file_url is derived dynamically.

If a request object is present in serializer context:

  • absolute URL is returned

Otherwise:

  • raw file URL is returned

ManualMediaUploadSerializer

Purpose:

  • validate and create a media attachment

Write-only parent fields:

  • manual_id
  • chapter_id
  • section_id
  • step_group_id
  • step_id

Validation rules:

  • uploaded file must pass upload validation
  • exactly one parent id must be provided

Responsibilities

This serializer validates:

  • file presence and type constraints
  • attachment target shape

It does not validate org ownership of the parent.
That belongs in the media service / view workflow.


Callout serializers

CalloutPresetSerializer

Purpose:

  • serialize reusable callout style presets

Fields:

  • id
  • name
  • slug
  • icon
  • color
  • is_active
  • order

CalloutSerializer

Purpose:

  • serialize callouts attached to manuals or content blocks

Fields include:

  • parent references
  • order
  • display fields
  • preset reference
  • resolved icon/color metadata

Important computed fields:

  • icon_resolved
  • color_resolved
  • display_name

Notes

display_name resolves to:

  • preset name when preset is present
  • otherwise the callout’s own name

This serializer is mainly a transport serializer.
Parent consistency rules are enforced by the model and service layer.


Step serializers

ManualStepSerializer

Purpose:

  • serialize individual manual steps

Includes nested read-only relations:

  • media
  • callouts

Fields:

  • id
  • order
  • title
  • body_text
  • media
  • callouts

StepGroupSerializer

Purpose:

  • serialize step groups with nested steps

Includes nested read-only relations:

  • steps
  • media
  • callouts

Fields:

  • id
  • order
  • title
  • steps
  • media
  • callouts

Section serializers

SectionSerializer

Purpose:

  • serialize rich sections inside chapters

Includes nested read-only relations:

  • media
  • callouts

Fields:

  • id
  • order
  • title
  • description_json
  • media
  • callouts

Notes

The serializer intentionally exposes description_json rather than HTML.

This matches the current rich-text architecture.


SectionNoteSerializer

Purpose:

  • serialize note blocks

Fields:

  • id
  • order
  • title
  • body

Chapter item serializer

ChapterItemSerializer

Purpose:

  • serialize the ordered display stream for a chapter

Nested read-only targets:

  • section
  • note
  • callout
  • step_group

Computed field:

  • is_media_grid

Fields:

  • id
  • order
  • kind
  • section
  • note
  • callout
  • step_group
  • is_media_grid

Notes

This serializer does not expose write-side target selection logic.

It is read-only and presentation-focused.


Chapter serializer

ChapterSerializer

Purpose:

  • serialize chapter detail including nested content stream

Includes nested read-only relations:

  • media
  • callouts
  • items

Fields:

  • id
  • order
  • title
  • description
  • cover_image
  • media
  • callouts
  • items

Notes

This serializer does not directly expose sections, notes, and step groups as standalone chapter children.
Instead, chapter presentation is primarily driven by items.

ChapterWriteSerializer

Location:

  • serializers/chapters.py

Purpose:

  • dedicated write serializer for chapter create/update operations

Fields:

  • id (read-only)

  • manual (required, FK)

  • order

  • title

  • description

  • cover_image

Notes

This serializer exists to separate:

  • write concerns (manual assignment, basic fields)

  • from read concerns (nested media, callouts, items)

It ensures that:

  • manual is always explicitly provided during creation

  • database integrity constraints are respected

  • viewsets can safely handle create/update without leaking nested read complexity


Chapter serializer split

The chapter API now uses two serializers:

| Serializer | Purpose |

|---|---|

| ChapterSerializer | read-only, nested presentation |

| ChapterWriteSerializer | create/update input |

View behavior

ChapterViewSet dynamically selects serializer:

  • create/update → ChapterWriteSerializer

  • retrieve/list → ChapterSerializer


Architectural improvement

Explicit read vs write separation

Previously:

  • ChapterSerializer was implicitly used for both read and write

  • This caused missing required fields (e.g. manual) during create

Now:

  • read and write paths are clearly separated

  • serializers are smaller and more predictable

  • API contracts are explicit


Impact on serializer responsibilities

Updated serializer responsibilities:

Serializer layer now additionally owns

  • clear separation between read and write contracts

  • enforcing required foreign key relationships on write

Service layer remains responsible for

  • nested orchestration

  • synchronization logic

  • content integrity beyond simple field validation


Best practice update

Add to best practices:

  • use separate write serializers when:

  • required FK fields are not part of read output

  • nested read serializers would pollute write contracts

  • create/update flows differ significantly from read shape



Manual serializers

ManualListSerializer

Purpose:

  • lightweight serializer for manual listings

Includes:

  • taxonomy
  • org summary
  • publishing metadata

Fields:

  • id
  • title
  • description
  • cover_image
  • category
  • topic
  • org
  • is_public_external
  • published_at
  • order

Use case

Used when the API only needs:

  • manual cards
  • list views
  • search/filter results

ManualDetailSerializer

Purpose:

  • full serializer for manual detail and manual write operations

Read fields

Includes nested read-only relations:

  • category
  • topic
  • org
  • media
  • callouts
  • chapters

Write fields

Supports write-only helpers:

  • category_id
  • topic_id
  • chapters_write
  • chapters_strategy
  • callouts_write
  • callouts_strategy

Nested write strategy fields

Supported strategies:

  • merge
  • replace

These determine how nested content sync behaves for:

  • chapters
  • manual-level callouts

Input normalization

The serializer normalizes JSON-ish fields for:

  • chapters_write
  • callouts_write

If the incoming value is a JSON string, it is parsed before normal validation.


Create/update behavior

The serializer delegates nested synchronization to services.

Flow:

  1. validate base fields
  2. parse nested write payloads
  3. create or update the Manual
  4. call service-layer sync functions
  5. refresh the instance
  6. return the updated object

Manual write flow

flowchart TD

A[Incoming manual payload] --> B[ManualDetailSerializer]
B --> C[Validate scalar fields]
C --> D[Parse chapters_write / callouts_write]
D --> E[Create or update Manual]
E --> F[Call sync_chapters]
E --> G[Call sync_manual_callouts]
F --> H[Refresh manual instance]
G --> H
H --> I[Return serialized manual]

Serializer responsibilities by layer

Serializer layer owns

  • field declarations
  • input validation
  • nested payload shape validation
  • JSON-ish payload parsing
  • response shaping
  • lightweight computed fields

Service layer owns

  • chapter synchronization
  • chapter item synchronization
  • step synchronization
  • callout sync
  • inline media cleanup
  • nested manual update orchestration

Model layer owns

  • data constraints
  • field types
  • relational integrity
  • model-level validation

Why the split matters

Before cleanup, the serializer layer was doing too much:

  • parsing
  • orchestration
  • deep nested sync
  • rich content cleanup

That made serializers:

  • too long
  • hard to test
  • hard to reason about
  • tightly coupled to content internals

After the split:

  • serializers define the API surface
  • services perform the heavy synchronization work

This is a much cleaner separation.


Validation patterns

Parent selection validation

ManualMediaUploadSerializer enforces:

  • exactly one parent id must be provided

This prevents ambiguous media placement.


Cover image normalization

ManualDetailSerializer normalizes empty cover values so that:

  • empty string can be treated as null-like input

This is useful for multipart/form-data workflows.


JSON payload normalization

Nested write payloads may arrive as:

  • parsed lists
  • JSON strings

The serializer accepts both by normalizing string payloads first.

This makes frontend integration more robust.


Read-model enrichment

The serializers are designed to support rich nested responses.

Examples:

  • a step includes media and callouts
  • a section includes media and callouts
  • a chapter includes items
  • a manual includes chapters, callouts, media, and taxonomy

This makes the API convenient for manual-rendering clients.


Design principles

Split by domain concern

The serializer package is organized by content type instead of being one large file.


Thin serializers, rich services

Serializers should validate and shape data, not own orchestration.


Read-friendly nested output

The API favors rich nested reads for manual rendering.


Explicit write helpers

Write-only fields such as chapters_write and callouts_write make nested updates explicit.


Keep transport separate from storage

Serializers expose URLs and IDs, but storage and file lifecycle rules stay elsewhere.


Best practices

  • keep serializers focused on transport concerns
  • put nested sync logic in services
  • use read-only nested serializers for rendering-heavy responses
  • keep computed fields lightweight
  • avoid business workflow logic inside serializer methods
  • validate shape here, validate ownership/workflow in services or views

Future extensions

Possible future improvements include:

  • dedicated write serializers for nested chapter content
  • dedicated manual publish/unpublish serializers
  • explicit serializer for chapter item writes
  • separate public/external serializers
  • more compact serializers for mobile/manual preview use cases

Summary

The manuals serializer layer provides the API contract for:

  • taxonomy
  • manuals
  • chapters
  • sections
  • notes
  • step groups
  • steps
  • callouts
  • media
  • chapter display items

It is designed to be:

  • split by concern
  • nested for read convenience
  • thin on orchestration
  • aligned with the service layer

The key idea is:

serializers describe and validate the API shape; services perform the content synchronization.