Content type management#

The @content-types service lists, configures, creates, copies, and deletes content types. It is available at the site root, and requires the Plone Site Setup: Types permission, like the content settings control panel, as well as permission to use the REST API.

Use the Types service to edit the schema of a content type. The dexterity-types control panel of the Control Panels service remains available.

See also

This page used to show example representations of the default content types. They are now in Representations of the default content types.

Method

Path

Result

GET

/@content-types

Every content type with its settings.

GET

/@content-types/{type}

One content type with its settings.

POST

/@content-types

Create a Dexterity content type, optionally copying another one.

PATCH

/@content-types/{type}

Change some settings of a content type.

DELETE

/@content-types/{type}

Delete a Dexterity content type that has no items.

Encode content type IDs in URLs, for example News%20Item. Unknown content types and extra path segments return 404 Not Found.

List content types#

Send a GET request to /@content-types to list every content type with its settings.

http

GET /plone/@content-types HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X GET http://nohost/plone/@content-types -H "Accept: application/json" --user admin:secret

httpie

http http://nohost/plone/@content-types Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/@content-types', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))

The response contains @id, items, and items_total. Each item has the representation of a single content type, described below.

Get a content type#

Send a GET request to /@content-types/{type} to get one content type.

http

GET /plone/@content-types/Event HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X GET http://nohost/plone/@content-types/Event -H "Accept: application/json" --user admin:secret

httpie

http http://nohost/plone/@content-types/Event Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/@content-types/Event', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

{
    "@id": "http://localhost:55001/plone/@content-types/Event",
    "content_count": 0,
    "data": {
        "allow_discussion": false,
        "allowed_content_types": [],
        "allowed_content_types_mode": {
            "title": "No types",
            "token": "none"
        },
        "default_page": true,
        "description": "Events can be shown in calendars.",
        "global_allow": true,
        "in_navigation": true,
        "searchable": true,
        "title": "Event",
        "versioning": {
            "title": "Automatic",
            "token": "automatic"
        }
    },
    "id": "Event",
    "schema": {
        "fieldsets": [
            {
                "fields": [
                    "title",
                    "description"
                ],
                "id": "general",
                "title": "General"
            },
            {
                "fields": [
                    "global_allow",
                    "allowed_content_types_mode",
                    "allowed_content_types"
                ],
                "id": "contents",
                "title": "Contents"
            },
            {
                "fields": [
                    "searchable",
                    "default_page",
                    "in_navigation"
                ],
                "id": "visibility",
                "title": "Visibility"
            },
            {
                "fields": [
                    "allow_discussion"
                ],
                "id": "comments",
                "title": "Comments"
            },
            {
                "fields": [
                    "versioning"
                ],
                "id": "versioning",
                "title": "Versioning"
            }
        ],
        "properties": {
            "allow_discussion": {
                "description": "",
                "factory": "Yes/No",
                "title": "Allow comments",
                "type": "boolean"
            },
            "allowed_content_types": {
                "additionalItems": true,
                "default": [],
                "description": "Kept while all types are allowed",
                "factory": "Multiple Choice",
                "items": {
                    "description": "",
                    "factory": "Choice",
                    "title": "",
                    "type": "string",
                    "vocabulary": {
                        "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.PortalTypes"
                    }
                },
                "title": "Selected types",
                "type": "array",
                "uniqueItems": true
            },
            "allowed_content_types_mode": {
                "choices": [
                    [
                        "all",
                        "All types"
                    ],
                    [
                        "selected",
                        "Selected types"
                    ],
                    [
                        "none",
                        "No types"
                    ]
                ],
                "description": "Whether all types, only the selected types, or no types can be added inside content of this type",
                "enum": [
                    "all",
                    "selected",
                    "none"
                ],
                "enumNames": [
                    "All types",
                    "Selected types",
                    "No types"
                ],
                "factory": "Choice",
                "title": "Allowed content types",
                "type": "string",
                "vocabulary": {
                    "@id": "http://localhost:55001/plone/@sources/allowed_content_types_mode"
                }
            },
            "default_page": {
                "description": "Content of this type can be a folder's default page",
                "factory": "Yes/No",
                "title": "Usable as a default page",
                "type": "boolean"
            },
            "description": {
                "default": "",
                "description": "",
                "factory": "Text",
                "title": "Description",
                "type": "string",
                "widget": "textarea"
            },
            "global_allow": {
                "description": "Allow adding content of this type in any folder",
                "factory": "Yes/No",
                "title": "Globally addable",
                "type": "boolean"
            },
            "in_navigation": {
                "description": "Items of this type are listed in the site's navigation",
                "factory": "Yes/No",
                "title": "Shown in navigation",
                "type": "boolean"
            },
            "searchable": {
                "description": "",
                "factory": "Yes/No",
                "title": "Shown in search results",
                "type": "boolean"
            },
            "title": {
                "description": "",
                "factory": "Text line (String)",
                "minLength": 1,
                "title": "Title",
                "type": "string"
            },
            "versioning": {
                "choices": [
                    [
                        "off",
                        "Off"
                    ],
                    [
                        "manual",
                        "Manual"
                    ],
                    [
                        "automatic",
                        "Automatic"
                    ]
                ],
                "description": "Manual versioning saves a version on request, automatic versioning also on every edit",
                "enum": [
                    "off",
                    "manual",
                    "automatic"
                ],
                "enumNames": [
                    "Off",
                    "Manual",
                    "Automatic"
                ],
                "factory": "Choice",
                "title": "Versioning",
                "type": "string",
                "vocabulary": {
                    "@id": "http://localhost:55001/plone/@sources/versioning"
                }
            }
        },
        "required": [
            "title",
            "global_allow",
            "allowed_content_types_mode",
            "searchable",
            "default_page",
            "in_navigation",
            "allow_discussion",
            "versioning"
        ],
        "type": "object"
    },
    "title": "Event",
    "workflow": {
        "follows_default": true,
        "id": "simple_publication_workflow"
    }
}

Property

Description

@id

URL of the content type in this service.

id

Content type ID.

title

Translated title.

content_count

Number of items of this type, including those the current user cannot view.

schema

JSON schema of the settings, with properties, required, and fieldsets

data

Current values of the settings, keyed by field name.

workflow

Read-only summary of the assigned workflow.

The schema has the format described in Types Schema. Each fieldset has an id, a title, and the names of its fields. Fields marked readOnly in the schema cannot be changed. Choice values have a token and a title, like in control panels and content.

workflow.id is the first workflow of the type's chain, or null when the type has no workflow. workflow.follows_default is true when the type uses the site's default workflow, and false when a workflow, or no workflow, is assigned to it explicitly.

Core settings#

Fieldset

Field

Value

general

title

Title, not empty.

general

description

Description.

contents

global_allow

Whether items of this type can be added inside items of the types that allow every type, with allowed_content_types_mode set to all.

contents

allowed_content_types_mode

Which types can be added inside items of this type: all, selected, or none.

contents

allowed_content_types

IDs of the selected types, from the plone.app.vocabularies.PortalTypes vocabulary.

visibility

searchable

Whether items of this type are shown in search results.

visibility

default_page

Whether items of this type can be a folder's default page.

visibility

in_navigation

Whether items of this type are shown in the site's navigation.

comments

allow_discussion

Whether items of this type allow comments. Only when plone.app.discussion is installed.

versioning

versioning

off, manual, or automatic.

allowed_content_types_mode and allowed_content_types work together:

  • all allows every type. allowed_content_types keeps the selection, which applies when you change the mode to selected later.

  • selected allows only the types in allowed_content_types, which can't be empty.

  • none allows no types, and allowed_content_types must be empty.

The service checks these rules against the stored values too, so changing only the mode to none fails while types are selected.

manual versioning saves versions on request, and automatic versioning also saves a version on every edit. Changing the versioning updates the repository policies of the type and its versioning behavior. Enabling versioning also enables locking, and disabling versioning keeps locking. Repository policies added by add-ons are kept.

Change settings#

Send a PATCH request to /@content-types/{type} with the fields to change. Fields you don't send keep their values.

http

PATCH /plone/@content-types/conference_event HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json

{
    "allowed_content_types": [
        "Image",
        "File"
    ],
    "allowed_content_types_mode": "selected",
    "global_allow": false,
    "searchable": true,
    "versioning": "manual"
}

curl

curl -i -X PATCH http://nohost/plone/@content-types/conference_event -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"allowed_content_types": ["Image", "File"], "allowed_content_types_mode": "selected", "global_allow": false, "searchable": true, "versioning": "manual"}' --user admin:secret

httpie

echo '{
  "allowed_content_types": [
    "Image",
    "File"
  ],
  "allowed_content_types_mode": "selected",
  "global_allow": false,
  "searchable": true,
  "versioning": "manual"
}' | http PATCH http://nohost/plone/@content-types/conference_event Accept:application/json Content-Type:application/json -a admin:secret

python-requests

requests.patch('http://nohost/plone/@content-types/conference_event', headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, json={'allowed_content_types': ['Image', 'File'], 'allowed_content_types_mode': 'selected', 'global_allow': False, 'searchable': True, 'versioning': 'manual'}, auth=('admin', 'secret'))
HTTP/1.1 204 No Content

A choice value can be its token, or an object with a token, as returned in data.

The service validates every change before it stores any of them. Invalid values, unknown fields, and read-only fields return 400 Bad Request with the field names and messages.

Create a content type#

Send a POST request to /@content-types to create a Dexterity container type with an empty schema. The body requires a title, and accepts an id and a description. Without an id, the service derives one from the title.

http

POST /plone/@content-types HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json

{
    "description": "An event at a conference.",
    "title": "Conference event"
}

curl

curl -i -X POST http://nohost/plone/@content-types -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"description": "An event at a conference.", "title": "Conference event"}' --user admin:secret

httpie

echo '{
  "description": "An event at a conference.",
  "title": "Conference event"
}' | http POST http://nohost/plone/@content-types Accept:application/json Content-Type:application/json -a admin:secret

python-requests

requests.post('http://nohost/plone/@content-types', headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, json={'description': 'An event at a conference.', 'title': 'Conference event'}, auth=('admin', 'secret'))
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://localhost:55001/plone/@content-types/conference_event

{
    "@id": "http://localhost:55001/plone/@content-types/conference_event",
    "content_count": 0,
    "data": {
        "allow_discussion": false,
        "allowed_content_types": [],
        "allowed_content_types_mode": {
            "title": "No types",
            "token": "none"
        },
        "default_page": false,
        "description": "An event at a conference.",
        "global_allow": true,
        "in_navigation": false,
        "searchable": true,
        "title": "Conference event",
        "versioning": {
            "title": "Off",
            "token": "off"
        }
    },
    "id": "conference_event",
    "schema": {
        "fieldsets": [
            {
                "fields": [
                    "title",
                    "description"
                ],
                "id": "general",
                "title": "General"
            },
            {
                "fields": [
                    "global_allow",
                    "allowed_content_types_mode",
                    "allowed_content_types"
                ],
                "id": "contents",
                "title": "Contents"
            },
            {
                "fields": [
                    "searchable",
                    "default_page",
                    "in_navigation"
                ],
                "id": "visibility",
                "title": "Visibility"
            },
            {
                "fields": [
                    "allow_discussion"
                ],
                "id": "comments",
                "title": "Comments"
            },
            {
                "fields": [
                    "versioning"
                ],
                "id": "versioning",
                "title": "Versioning"
            }
        ],
        "properties": {
            "allow_discussion": {
                "description": "",
                "factory": "Yes/No",
                "title": "Allow comments",
                "type": "boolean"
            },
            "allowed_content_types": {
                "additionalItems": true,
                "default": [],
                "description": "Kept while all types are allowed",
                "factory": "Multiple Choice",
                "items": {
                    "description": "",
                    "factory": "Choice",
                    "title": "",
                    "type": "string",
                    "vocabulary": {
                        "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.PortalTypes"
                    }
                },
                "title": "Selected types",
                "type": "array",
                "uniqueItems": true
            },
            "allowed_content_types_mode": {
                "choices": [
                    [
                        "all",
                        "All types"
                    ],
                    [
                        "selected",
                        "Selected types"
                    ],
                    [
                        "none",
                        "No types"
                    ]
                ],
                "description": "Whether all types, only the selected types, or no types can be added inside content of this type",
                "enum": [
                    "all",
                    "selected",
                    "none"
                ],
                "enumNames": [
                    "All types",
                    "Selected types",
                    "No types"
                ],
                "factory": "Choice",
                "title": "Allowed content types",
                "type": "string",
                "vocabulary": {
                    "@id": "http://localhost:55001/plone/@sources/allowed_content_types_mode"
                }
            },
            "default_page": {
                "description": "Content of this type can be a folder's default page",
                "factory": "Yes/No",
                "title": "Usable as a default page",
                "type": "boolean"
            },
            "description": {
                "default": "",
                "description": "",
                "factory": "Text",
                "title": "Description",
                "type": "string",
                "widget": "textarea"
            },
            "global_allow": {
                "description": "Allow adding content of this type in any folder",
                "factory": "Yes/No",
                "title": "Globally addable",
                "type": "boolean"
            },
            "in_navigation": {
                "description": "Items of this type are listed in the site's navigation",
                "factory": "Yes/No",
                "title": "Shown in navigation",
                "type": "boolean"
            },
            "searchable": {
                "description": "",
                "factory": "Yes/No",
                "title": "Shown in search results",
                "type": "boolean"
            },
            "title": {
                "description": "",
                "factory": "Text line (String)",
                "minLength": 1,
                "title": "Title",
                "type": "string"
            },
            "versioning": {
                "choices": [
                    [
                        "off",
                        "Off"
                    ],
                    [
                        "manual",
                        "Manual"
                    ],
                    [
                        "automatic",
                        "Automatic"
                    ]
                ],
                "description": "Manual versioning saves a version on request, automatic versioning also on every edit",
                "enum": [
                    "off",
                    "manual",
                    "automatic"
                ],
                "enumNames": [
                    "Off",
                    "Manual",
                    "Automatic"
                ],
                "factory": "Choice",
                "title": "Versioning",
                "type": "string",
                "vocabulary": {
                    "@id": "http://localhost:55001/plone/@sources/versioning"
                }
            }
        },
        "required": [
            "title",
            "global_allow",
            "allowed_content_types_mode",
            "searchable",
            "default_page",
            "in_navigation",
            "allow_discussion",
            "versioning"
        ],
        "type": "object"
    },
    "title": "Conference event",
    "workflow": {
        "follows_default": true,
        "id": "simple_publication_workflow"
    }
}

The new type has the same defaults as one created in the classic "Dexterity Content Types" control panel. The service returns 409 Conflict when a type with the ID exists.

Copy a content type#

Add copy_from with the ID of a Dexterity content type to copy it. The copy has the schema, behaviors, settings, actions, repository policies, and workflow assignment of the source. Without a description, it also has the source's description.

http

POST /plone/@content-types HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json

{
    "copy_from": "Event",
    "description": "An event at a conference.",
    "id": "ConferenceEvent",
    "title": "Conference event"
}

curl

curl -i -X POST http://nohost/plone/@content-types -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"copy_from": "Event", "description": "An event at a conference.", "id": "ConferenceEvent", "title": "Conference event"}' --user admin:secret

httpie

echo '{
  "copy_from": "Event",
  "description": "An event at a conference.",
  "id": "ConferenceEvent",
  "title": "Conference event"
}' | http POST http://nohost/plone/@content-types Accept:application/json Content-Type:application/json -a admin:secret

python-requests

requests.post('http://nohost/plone/@content-types', headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, json={'copy_from': 'Event', 'description': 'An event at a conference.', 'id': 'ConferenceEvent', 'title': 'Conference event'}, auth=('admin', 'secret'))

The response is the new content type, like the response to creating one. Changes to the copy, including to its schema, don't affect the source.

Delete a content type#

Send a DELETE request to /@content-types/{type} to delete a Dexterity content type.

http

DELETE /plone/@content-types/conference_event HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X DELETE http://nohost/plone/@content-types/conference_event -H "Accept: application/json" --user admin:secret

httpie

http DELETE http://nohost/plone/@content-types/conference_event Accept:application/json -a admin:secret

python-requests

requests.delete('http://nohost/plone/@content-types/conference_event', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 204 No Content

Deleting a type also removes its settings, its workflow assignment, and its use as an allowed type of other types. A type created later with the same ID starts with default settings.

While items of the type exist, the service returns 409 Conflict with their number.

HTTP/1.1 409 Conflict
Content-Type: application/json

{
    "content_count": 1,
    "error": {
        "message": "The content type still has items",
        "type": "Conflict"
    }
}

Add settings from an add-on#

Each fieldset comes from a named adapter providing plone.restapi.typesettings.interfaces.ITypeSettings. The core fieldsets use the same mechanism, so an add-on can add its own settings to every content type. Because the response includes a JSON schema, a frontend can show them without knowing the add-on.

An adapter adapts the FTI and the request, and has these attributes and methods:

Name

Description

title

Fieldset title.

order

Sort order of the fieldset, lowest first. The core fieldsets use 10 to 50.

schema

zope.schema interface declaring the fields. Field names must be unique across all fieldsets.

permission

Permission ID needed to see and change the fieldset, or None.

enabled()

Return whether to show the fieldset, for example only when the add-on that uses it is installed. The base class returns True.

read()

Return the current values.

validate(data)

Return a list of errors for the changed values, each with field and message.

write(data)

Store the changed values.

remove()

Delete what the settings store outside the FTI, before the type is deleted.

The service converts values with the registered field serializers and deserializers, and validates them with the field constraints and the schema invariants. It applies plone.autoform directives such as omitted, mode, and order_before. Use flat fields: values of schema.Object fields aren't converted to and from JSON.

Subclass plone.restapi.typesettings.settings.TypeSettings to store settings as FTI properties, or override its methods to store them elsewhere. This example stores a setting in the registry:

from plone.registry.interfaces import IRegistry
from plone.restapi.typesettings.settings import TypeSettings
from zope import schema
from zope.component import getUtility
from zope.interface import Interface


class ISponsorSettings(Interface):
    sponsors_enabled = schema.Bool(title="Show sponsors")


class SponsorSettings(TypeSettings):
    title = "Sponsors"
    order = 100
    schema = ISponsorSettings
    record = "collective.sponsors.types"

    def read(self):
        types = getUtility(IRegistry)[self.record]
        return {"sponsors_enabled": self.context.getId() in types}

    def write(self, data):
        registry = getUtility(IRegistry)
        types = set(registry[self.record]) - {self.context.getId()}
        if data["sponsors_enabled"]:
            types.add(self.context.getId())
        registry[self.record] = sorted(types)

    def remove(self):
        self.write({"sponsors_enabled": False})
<adapter
    factory=".settings.SponsorSettings"
    name="collective.sponsors"
    />