toeverything/AFFiNE · error · SchemaValidateError

Root block cannot have parent.

Error message

Root block cannot have parent.

What it means

Schema.validate refuses any schema whose model.role === 'root' when a parentFlavour is supplied. Root blocks (for example a doc root like affine:page) are top-level by definition, so nesting one under another block is an invalid hierarchy and validation stops immediately.

Source

Thrown at blocksuite/framework/store/src/schema/schema.ts:80

  ): void => {
    const schema = this.flavourSchemaMap.get(flavour);
    if (!schema) {
      throw new SchemaValidateError(flavour, SCHEMA_NOT_FOUND_MESSAGE);
    }

    const validateChildren = () => {
      childFlavours?.forEach(childFlavour => {
        const childSchema = this.flavourSchemaMap.get(childFlavour);
        if (!childSchema) {
          throw new SchemaValidateError(childFlavour, SCHEMA_NOT_FOUND_MESSAGE);
        }
        this.validateSchema(childSchema, schema);
      });
    };

    if (schema.model.role === 'root') {
      if (parentFlavour) {
        throw new SchemaValidateError(
          schema.model.flavour,
          'Root block cannot have parent.'
        );
      }

      validateChildren();
      return;
    }

    if (!parentFlavour) {
      throw new SchemaValidateError(
        schema.model.flavour,
        'None root block must have parent.'
      );
    }

    const parentSchema = this.flavourSchemaMap.get(parentFlavour);
    if (!parentSchema) {

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Create root blocks without a parent: doc.addBlock(rootFlavour).
  2. Use a non-root container flavour when you need nesting under other blocks.
  3. When importing snapshots, drop parent ids attached to root-role blocks.

Example fix

// before
await doc.addBlock('affine:page', {}, parentId); // root under a parent

// after
await doc.addBlock('affine:page'); // root blocks are added without a parent
Defensive patterns

Strategy: validation

Validate before calling

if (doc.schema.get(flavour)?.model.role === 'root') {
  await doc.addBlock(flavour); // root: never pass a parent
} else {
  await doc.addBlock(flavour, props, parentId);
}

Type guard

const isRootFlavour = (schema: Schema, flavour: string): boolean =>
  schema.get(flavour)?.model.role === 'root';

Try / catch

try {
  await doc.addBlock(flavour, props, parentId);
} catch (e) {
  if (e instanceof SchemaValidateError && e.message.includes('Root block cannot have parent')) {
    // retry without parent
    await doc.addBlock(flavour);
  } else throw e;
}

Prevention

When it happens

Trigger: doc.addBlock('affine:page', {}, someParentId) — creating a root-flavoured block under another block; moving a root model into a container; snapshot import that places a root block under a parent.

Common situations: Treating the page/root flavour as a generic container; hand-built snapshots with wrong parent links; copy-paste logic that re-parents whatever block was copied, including roots.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/ed05423fb1cd868a. Report an issue: GitHub.