Document format
The JSON document envelope, its schema version and size limits, what the validator accepts, and what happens to content it rejects.
The value
A document's content is a Plate value: an array of block objects, each with a type and children. EditorValue is that type.
import type { Value } from "platejs";
export type EditorValue = Value;
export const EMPTY_EDITOR_VALUE = [
{
type: "p",
children: [{ text: "" }],
},
] satisfies EditorValue;EMPTY_EDITOR_VALUE is what an editor with no initialValue starts from: one empty paragraph. Both names are re-exported from the entry point.
The stored envelope
What is saved is an envelope around the value. serializeEditorDocument writes exactly these four fields and nothing else.
{
"schemaVersion": 1,
"documentId": "notes",
"revision": 3,
"content": [
{ "type": "h1", "children": [{ "text": "Release notes" }] },
{ "type": "p", "children": [{ "text": "Shipped today." }] }
]
}schemaVersionis the literal1.documentIdis a non-empty string. It matches the id the editor was given.revisionis a non-negative integer. It goes up by one on each save, and it is the value your adapter compares for conflicts.contentis the array of blocks.
import { z } from "zod";
import { EMPTY_EDITOR_VALUE, type EditorValue } from "./editor-value";
export const EDITOR_SCHEMA_VERSION = 1;
export const EDITOR_DOCUMENT_LIMITS = {
maxDepth: 32,
maxNodes: 20_000,
maxBytes: 2_000_000,
} as const;
export const editorDocumentSchema = z.object({
schemaVersion: z.literal(EDITOR_SCHEMA_VERSION),
documentId: z.string().min(1),
revision: z.number().int().nonnegative(),
content: z.custom<EditorValue>((value): value is EditorValue => Array.isArray(value)),
});
export type EditorDocument = z.infer<typeof editorDocumentSchema>;
export function formatBlockLabel(path: readonly number[]): string {
return `Block ${path.map((index) => String(index + 1)).join(".")}`;
}
export function createEditorDocument(
documentId: string,
content: EditorValue = EMPTY_EDITOR_VALUE,
): EditorDocument {
return {
schemaVersion: EDITOR_SCHEMA_VERSION,
documentId,
revision: 0,
content,
};
}
export function serializeEditorDocument(document: EditorDocument): string {
return JSON.stringify({
schemaVersion: document.schemaVersion,
documentId: document.documentId,
revision: document.revision,
content: document.content,
});
}Size limits
maxDepth: 32 levels of nesting.maxNodes: 20,000 nodes.maxBytes: 2,000,000 bytes of JSON.
These are the limits a document is opened against. A document over the byte limit is invalid: the user sees the recovery notice, not a half-loaded editor.
Migration on load
Before a document opens, a bare array is wrapped in an envelope with revision 0 and the document id, so a value you stored before you had an envelope still opens. Any other value passes through unchanged and is validated as it is.
What the validator decides
- ok
- The document opens. Repairs are applied and returned with the result, such as a missing block id, a disallowed value, or a list number out of range. The editor shows no notice for repairs on load; paste shows its repairs in a notice.
- unsupported
- The document uses a block type, attribute, mark or value the allow list does not contain, or a structure the validator rejects. The editor shows a recovery notice with the first five issues and the raw JSON. The user can copy the JSON or start over, and starting over saves the original to recovery storage first.
- invalid
- The stored value is not a document, for example the envelope is missing fields or the JSON is too large. The same recovery notice is shown. Storage that cannot be read shows the same notice.
- future-version
- schemaVersion is higher than 1. The recovery notice says the document is from a newer editor.
The allow lists are EDITOR_ELEMENT_RULES and EDITOR_MARK_RULES in the installed lib/document/editor-document-schema.ts. A value outside them is the difference between a repair and a rejection, so it is worth reading before you hand-write a document.
Block ids
A generated block id is 10 characters. Block ids are normalized on load: a missing id gets a new one, a duplicate gets a new one, and ids already in the document are reserved first so a generated id cannot take one that a later block still owns. Ids are what page links and synced references point at, so they are stable once written.
Structure rules worth knowing
- A code line, a table row or a table cell at the top level makes the document unsupported.
- A footnote definition is top level only; anywhere else it is read as a paragraph.
- A table cannot sit inside a table, and a column group cannot hold another column group.
- Callout and quote children are paragraphs only. Table cells hold paragraphs only.