Plate compiles the installed plugins and application schema into one document contract. The contract controls element structure, properties, roots, construction, and insertion fitting.
import { createEditor, definePlugin, property, schema } from 'platejs';
const NoticePlugin = definePlugin('notice', {
schema: {
element: {
...schema.element.textBlock(),
properties: {
tone: property.enum(['info', 'warning'], { default: 'info'
import { createEditor, definePlugin, property, schema } from 'platejs';
const NoticePlugin = definePlugin('notice', {
schema: {
element: {
...schema.element.textBlock(),
properties: {
tone: property.enum(['info', 'warning'], { default: 'info' }),
},
},
},
});
const editor = createEditor({
plugins: [NoticePlugin],
});schema.element.textBlock() declares an ordinary editable block. Element properties stay flat on the element. Marks and cross-cutting properties belong to their owning feature plugin; see Plugin Configuration.
The plugin tuple supplies vocabulary. The editor's schema option owns root grammar, overrides, and optional persisted identity.
const EditorSchema = {
root: schema.content.element(NoticePlugin, { min: 1 }),
} as const;
const editor = createEditor({
plugins: [NoticePlugin],
schema: EditorSchema,
});const EditorSchema = {
root: schema.content.element(NoticePlugin, { min: 1 }),
} as const;
const editor = createEditor({
plugins: [NoticePlugin],
schema: EditorSchema,
});Omit root for Plate's standard nonempty paragraph grammar. A custom root requires a positive integer min. Pass the installed plugin descriptor to schema.content.element; use schema.content.elements for several permitted element families. Its first descriptor supplies the construction default.
Structural children use the same content grammar. Set blockContent: false on internal structures that must not participate in normal-flow block content. Use plugins.blockContent(...) when a container accepts normal-flow blocks.
| Builder | Accepted content |
|---|---|
schema.content.text(...) | Text children |
schema.content.element(Plugin, ...) | One installed element family |
schema.content.elements(plugins, ...) | Any listed installed element family |
schema.content.type(type, ...) | A persisted type selected at a dynamic boundary |
schema.content.types(types, ...) | Any listed persisted type |
schema.content.group(group, ...) | Members of a compiled group |
schema.content.any(rules, ...) | At least one nested rule |
schema.content.all(rules, ...) | Every nested rule |
schema.content.not(rule, ...) | Content outside the nested rule |
min, max, and default control cardinality and construction. A required rule needs a constructable default. The compiled schema uses the same grammar to construct nodes, find wrapping, validate values, and fit imported content.
not is a complement and can admit undeclared types outside its excluded rule. Inspect allowsUnknownElements, allowedElementTypes, and allowsText on compiled element content when an integration needs to distinguish open and closed grammar.
The compiler owns structural groups such as element, text, block, inline, and textBlock. Application groups can extend other groups. Do not duplicate derived membership in every plugin.
Property descriptors define JSON value laws: property.boolean, string, number, enum, json, and set. Use a validator for a custom JSON value. Defaults and structural equality are part of the descriptor's contract; { default, omitDefault: true } can omit the default value from stored nodes.
Declare element-owned fields in schema.element.properties. Declare a primary mark in schema.mark. Use the plugin's keyed schema.properties for a feature that owns several properties. Their placement and targets determine where those values are accepted.
Pass plugin descriptors to schema-aware construction and lookup APIs. An installed element portal exposes its persisted type through plugin.schema.type; a primary-mark portal exposes plugin.schema.key. The plugin's name identifies the capability and is not a substitute for persisted identity.
Ordinary domain code reads the node's field directly. Generic code can inspect a compiled property discovered at runtime. Reading a default never writes that default into the document.
Set role: 'metadata' on a property placement when the value is bookkeeping that must not make a node meaningfully nonempty or appear in content-only serialization. Property lifecycle policy decides whether a value survives a type change; defaults and target checks still apply.
editor.read.schema.assertDocument(document);
editor.read.schema.assertFragment(children);editor.read.schema.assertDocument(document);
editor.read.schema.assertFragment(children);Validation checks the complete document or closed children against the compiled schema. It does not publish content. Unknown vocabulary, invalid property values, illegal root ownership, and grammar violations cannot become a partially accepted document.
Use editor.read.schema.create(...) to construct valid nodes and isMarkableVoid, isInline, isVoid, isIsolating, and isKeyboardSelectable for behavior queries. The exact read methods live in Editor API.
| Input | Operation |
|---|---|
| Complete document snapshot | tx.value.replace(...) |
| Targeted named-root change | tx.roots.create, replace, or remove |
| Closed decoded nodes | tx.fragment.replace(...) |
| Open clipboard or transport content | ContentSlice and tx.slice.replace(...) |
| Detached fitting preview | state.slice.fitContent(...) |
A ContentSlice preserves open boundaries and detached-root payloads. Generic validation checks its JSON shape and open depths; fitting checks the target grammar and may wrap or reject content according to that grammar. The preview does not commit. Publication fits at the actual transaction target.
Keep structural content in normal children. Use contentRoots when an element needs a separately addressed editable region with its own mount lifecycle or shared ownership.
Each slot declares its grammar and exclusive or shared ownership. The element stores the key under childRoots; the content lives in the document's roots. These links participate in validation, clipboard fitting, history, and persistence. See Roots.
editor.read.schema.identity() returns a derived identity or a named identity. Omit both id and version for derived identity; provide both for an application-owned lineage. The fingerprint includes compiled semantics, including root grammar.
Change the schema version when stored data needs different semantics. Save { document, schema } and use defineDocumentMigrations and migrateDocument from platejs/migrations to upgrade persisted envelopes before current-schema fitting. Do not use normalizers as historical migration runners.
For runtime-selected plugin configuration, candidate schema validation and any accompanying document migration finish before publication. A failed candidate publishes neither the configuration nor the document. Creation-time configuration preserves static inference; dynamic installation does not rewrite the editor's TypeScript type.
state.schema.delta() reports compiled schema differences for consumers that need them. Use exact identities and migration inputs; matching names alone do not prove that two schemas accept the same document.