Serialize plugins (default and custom)

Every django CMS plugin is serialized to JSON automatically — no configuration required. This guide shows the default behaviour, then how to take control with a custom serializer when you need a specific shape or fully typed output.

For why it works this way (foreign-key resolution, rich-text JSON, the plugin definitions endpoint), see Plugin serialization.

Default: automatic serialization

Create plugins exactly as you would in any django CMS project. Their model fields are serialized for you. A plugin with no extra fields still serializes its type:

# cms_plugins.py
from cms.models.pluginmodel import CMSPlugin
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool


@plugin_pool.register_plugin
class HelloPlugin(CMSPluginBase):
    model = CMSPlugin
    name = "Hello"
    render_template = "hello_world.html"

The template is still useful in headless mode: it lets editors see the plugin in the django CMS structure board and powers the optional ?html=1 rendering.

Add the plugin to a page, then fetch the placeholder content (note that the specific content type and object primary keys likely are different for your setup, run curl http://localhost:8080/api/en/pages/ to find out what’s correct for you):

curl "http://localhost:8080/api/en/placeholders/18/1/content/"
{
  "slot": "content",
  "content": [
    {
      "id": 1,
      "parent_plugin_type": null,
      "plugin_type": "HelloPlugin"
    }
  ],
  "html": ""
}

Every serialized plugin carries id, parent_plugin_type (the type of its parent plugin, or null at the top level) and plugin_type. A plugin with model fields adds each field by name:

# models.py
from cms.models import CMSPlugin
from cms.models.fields import PageField
from django.db import models


class HeroPluginModel(CMSPlugin):
    title = models.CharField(max_length=200)
    description = models.TextField()
    image = models.ImageField(upload_to="hero_images")
    link = PageField(blank=True, null=True)

A new model needs migrations — create and apply them before using the plugin:

python manage.py makemigrations <your_app>
python manage.py migrate

Its serialized form:

{
  "id": 4,
  "parent_plugin_type": null,
  "plugin_type": "HeroPlugin",
  "title": "A custom page hero",
  "description": "Important teaser content.",
  "image": "http://localhost:8080/media/hero_images/demo.png",
  "link": "http://localhost:8080/api/en/pages/about/"
}

Foreign keys are resolved to API endpoints where possible — note how link (a PageField) becomes the target page’s API URL rather than a raw id. This matches the real behaviour of, for example, a link plugin whose page field resolves to "http://localhost:8080/api/en/pages/". See Plugin serialization for the resolution rules.

Nested plugins

Plugins nested in the structure board (e.g. columns inside a grid) are serialized recursively. Child plugins appear under a children array on the parent, so the JSON mirrors the plugin tree.

Custom: define a serializer_class

Set a serializer_class on the plugin to control exactly which fields appear and how they are typed. Use this to add computed fields, hide internal ones, or shape data for the frontend. The serializer is a standard DRF ModelSerializer:

# serializers.py
from rest_framework import serializers
from .models import HeroPluginModel


class HeroPluginSerializer(serializers.ModelSerializer):
    cta_label = serializers.SerializerMethodField()

    class Meta:
        model = HeroPluginModel
        fields = ["title", "description", "image", "link", "cta_label"]

    def get_cta_label(self, obj) -> str:
        return obj.link_text or "Read more"
# cms_plugins.py
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool

from .models import HeroPluginModel
from .serializers import HeroPluginSerializer


@plugin_pool.register_plugin
class HeroPlugin(CMSPluginBase):
    model = HeroPluginModel
    name = "Hero"
    render_template = "hero.html"
    serializer_class = HeroPluginSerializer

The same serializer_class drives both the runtime content payload and the typed description returned by the /api/plugins/ endpoint — so a custom serializer also produces richer, fully typed client code.

Verify the type definition

curl http://localhost:8080/api/plugins/

The plugin now reports its precise properties (types, formats, help text), which client generators turn into accurate frontend types.

See also