Schema
The typed vocabulary of nodes, marks, and attributes.
Without a schema, every consumer of the document has to guess what is legal. Does a heading allow a nested list? Can a link mark carry attributes other than href? A stringly-typed tree gives no guarantees, and the engine would have to compensate at every layer.
A Schema declares the node kinds, mark kinds, and attribute shapes a document may contain. It is not a class; the declarative parts are introspectable and serializable, while validator functions stay local to trusted code.
Worked example
const schema: Schema = createSchema({
id: "editor" as SchemaId,
rootNodeType: "doc" as NodeTypeName,
nodes: {
doc: {
isRoot: true,
allowedChildren: [
"paragraph" as NodeTypeName,
"heading" as NodeTypeName,
"media" as NodeTypeName,
],
markPolicy: "denyAll",
},
paragraph: {
allowedChildren: ["text"],
markPolicy: "allowList",
allowedMarks: [
"strong" as MarkTypeName,
"em" as MarkTypeName,
"link" as MarkTypeName,
],
allowEmptyText: true,
},
heading: {
allowedChildren: ["text"],
markPolicy: "allowList",
allowedMarks: ["strong" as MarkTypeName, "em" as MarkTypeName],
allowEmptyText: true,
},
media: {
allowedChildren: [],
markPolicy: "denyAll",
validateAttrs(attrs) {
return typeof attrs.src === "string"
? null
: "media requires a string src";
},
},
},
marks: {
strong: {},
em: {},
link: {
validateAttrs(attrs) {
return typeof attrs.href === "string"
? null
: "link requires a string href";
},
},
},
});
const result = validateDoc(schema, doc);
if (!result.valid) {
reportSchemaErrors(result.errors);
}
Fields
interface Schema {
readonly id?: SchemaId;
readonly rootNodeType: NodeTypeName;
readonly nodes: Readonly<Record<string, NodeSpec>>;
readonly marks?: Readonly<Record<string, MarkSpec>>;
}
type MarkPolicy = "allowAny" | "denyAll" | "allowList";
type ChildTypeConstraint = "text" | "branch" | NodeTypeName;
Each NodeSpec declares allowed children (text, branch, or a specific NodeTypeName), the mark policy for that node, optional allowed mark names, optional child-count limits, and optional attribute validation. Each MarkSpec can validate its own attributes.
Validation and repair
validateDoc(schema, doc): SchemaValidationResult;
validateFragment(schema, frag): SchemaValidationResult;
repairDoc(schema, doc): SchemaRepairResult<Doc>;
repairFragment(schema, frag): SchemaRepairResult<Fragment>;
Validation reports. Repair rewrites. SchemaValidationResult is a success/failure union; failures carry per-violation SchemaValidationError entries. SchemaRepairResult returns the repaired value, a changed flag, and the validation result after repair. If some violation has no explicit repair semantics, it remains visible in that validation result instead of being hidden.
Repair is the backbone of the two shipped normalizer rules (schema:repair-doc, schema:repair-fragment). The engine runs them after every transaction — see Normalize.
Round-trip
schemaToJSON lowers the declarative parts of a schema into deterministic JSON for snapshots and tooling. Runtime validator functions are intentionally not serializable; if a schema crosses a process boundary, the receiving side must recreate those functions from its own trusted schema module. Documents carry node and mark type names, not references; the schema is the key that gives those names meaning.
API
createSchema(input): Schema;
hasNodeSpec(schema, name): boolean;
hasMarkSpec(schema, name): boolean;
getNodeSpec(schema, name): NodeSpec;
getMarkSpec(schema, name): MarkSpec | undefined;
isChildAllowed(schema, parentType, child): boolean;
isMarkAllowed(schema, nodeType, mark): boolean;
schemaToJSON(schema): JsonValue;