Plite documents are JSON. Serialization is app code that turns your Plite node shape into another format, and deserialization turns external input back into your node shape.
Read the full persisted document through editor.read.value().
const documentValue = editor.read.value();
const children = documentValue.children;const documentValue = editor.read.value();
For single-root editors, <Plite onValueChange> gives the current mounted-root
value in its callback context. Use editor.read.value() when you need extra
roots or persistent meta fields. See Document Meta.
Use NodeApi.string when plain text is enough.
import { NodeApi, type Descendant } from "@platejs/plite";
const serializePlainText = (nodes: readonly Descendant[]) =>
nodes.map((node) => NodeApi.string(node)).join("\n");import { NodeApi, type Descendant } from "@platejs/plite";
const serializePlainText = (nodes: readonly Descendant[]) =>
nodes.map((node) => NodeApi.string(node)).join("\n");Plain text intentionally drops block type, marks, links, comments, and other schema-specific data.
HTML serialization should match your schema. Plite does not guess how your custom elements map to HTML.
import escapeHtml from "escape-html";
import { ElementApi, TextApi, type Descendant } from "@platejs/plite";
const serializeNodeToHtml = (node: Descendant): string => {
if (TextApi.isText(node)) {
let text = escapeHtml(node.text);
if (node.bold) {
text = `<strong>${text}</strong>`;
}
if (node.italic) {
text = `<em>${text}</em>`;
}
return text;
}
if (!ElementApi.isElement(node)) {
return "";
}
const children = node.children.map(serializeNodeToHtml).join("");
switch (node.type) {
case "quote":
return `<blockquote>${children}</blockquote>`;
case "link":
return `<a href="${escapeHtml(node.url)}">${children}</a>`;
case "paragraph":
return `<p>${children}</p>`;
default:
return children;
}
};
const html = editor.read.children().map(serializeNodeToHtml).join("");import escapeHtml from "escape-html";
import { ElementApi, TextApi, type Descendant } from "@platejs/plite";
const serializeNodeToHtml = (node: Descendant): string => {
if (TextApi.isText(node)) {
let text = escapeHtml(node.text);
if (node.bold) {
text = `<strong>${text}</strong>`;
}
if (node.italic) {
text = `<em>${text}</em>`;
}
return text;
Escape text and attribute values before writing HTML strings. Keep sanitizer, allowed tag, URL, and table policy in your app schema or paste adapter.
Deserialization is the reverse schema mapping. Parse external HTML, decide which tags your editor accepts, and return Plite nodes.
import type { Descendant, Text } from "@platejs/plite";
type CustomText = Text & {
bold?: boolean;
italic?: boolean;
};
type DeserializeMarks = Pick<CustomText, "bold" | "italic">;
const deserializeElement = (
node: Node,
marks: DeserializeMarks = {}
): Descendant[] => {
if (node.nodeType === Node.TEXT_NODE) {
return [{ ...marks, text: node.textContent ?? "" }];
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return [];
}
const element = node as HTMLElement;
const nextMarks = {
...marks,
bold: marks.bold || element.nodeName === "STRONG",
italic: marks.italic || element.nodeName === "EM",
};
const children = Array.from(element.childNodes).flatMap((child) =>
deserializeElement(child, nextMarks)
);
const safeChildren = children.length > 0 ? children : [{ text: "" }];
switch (element.nodeName) {
case "BODY":
return children;
case "BLOCKQUOTE":
return [{ type: "quote", children: safeChildren }];
case "A":
return [
{
children: safeChildren,
type: "link",
url: element.getAttribute("href") ?? "",
},
];
case "P":
return [{ type: "paragraph", children: safeChildren }];
default:
return children;
}
};
const deserializeHtml = (html: string): Descendant[] => {
const document = new DOMParser().parseFromString(html, "text/html");
return deserializeElement(document.body);
};
editor.update.fragment.replace(deserializeHtml(html));import type { Descendant, Text } from "@platejs/plite";
type CustomText = Text & {
bold?: boolean;
italic?: boolean;
};
type DeserializeMarks = Pick<CustomText, "bold" | "italic">;
const deserializeElement = (
node: Node,
marks: DeserializeMarks = {}
): Descendant[] => {
if (node.nodeType === Node.TEXT_NODE) {
@platejs/plite-hyperscript is useful for tests and fixtures. Production
deserialization should use explicit parser code so unsupported tags,
attributes, marks, and unsafe URLs are handled deliberately.
fragment.replace fits known-closed decoded nodes through the compiled schema.
A parser that preserves open structural boundaries returns a validated
ContentSlice and inserts it through editor.update.slice.replace(slice).
Generic slice validation rejects malformed JSON and impossible open depths;
fitting rejects schema vocabulary, property, root, and grammar violations.
Clipboard serializers usually need a narrower policy than file export. A paste adapter might preserve links and marks while rejecting layout-only HTML or tables. A persistence serializer should preserve the full document value:
const persisted = editor.read.value();
await saveDocument(JSON.stringify(persisted));const persisted = editor.read.value();
await saveDocument(JSON.stringify(persisted));A declared schema validates initialValue when the document is loaded. Persist
editor.read.schema.identity() beside the document when the storage system
needs a schema-version boundary. See Schema for compiled identity,
validation, fitting, and atomic migration.
Keep comment bodies, permissions, audit data, and large external records in app or collaboration stores. Store only the ids or state fields that belong to the document model.