diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7c24081186..79a37e7587 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,6 +1,9 @@
 {
   "editor.defaultFormatter": "esbenp.prettier-vscode",
   "editor.formatOnSave": true,
+  "editor.codeActionsOnSave": {
+    "source.addMissingImports": "explicit"
+  },
   "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
   "[typescriptreact]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
diff --git a/examples/01-basic/01-minimal/package.json b/examples/01-basic/01-minimal/package.json
index bbf86f0736..c44135bd8f 100644
--- a/examples/01-basic/01-minimal/package.json
+++ b/examples/01-basic/01-minimal/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/01-basic/02-block-objects/package.json b/examples/01-basic/02-block-objects/package.json
index 39c9a0307b..7039e7673b 100644
--- a/examples/01-basic/02-block-objects/package.json
+++ b/examples/01-basic/02-block-objects/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/01-basic/03-multi-column/package.json b/examples/01-basic/03-multi-column/package.json
index e0326a2f50..6f47510954 100644
--- a/examples/01-basic/03-multi-column/package.json
+++ b/examples/01-basic/03-multi-column/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "@blocknote/xl-multi-column": "latest"
diff --git a/examples/01-basic/04-default-blocks/package.json b/examples/01-basic/04-default-blocks/package.json
index 546687894e..07bd550965 100644
--- a/examples/01-basic/04-default-blocks/package.json
+++ b/examples/01-basic/04-default-blocks/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/01-basic/05-removing-default-blocks/package.json b/examples/01-basic/05-removing-default-blocks/package.json
index 192f80ec02..84bea27041 100644
--- a/examples/01-basic/05-removing-default-blocks/package.json
+++ b/examples/01-basic/05-removing-default-blocks/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/01-basic/06-block-manipulation/package.json b/examples/01-basic/06-block-manipulation/package.json
index 977f2d333d..aa5d3d3660 100644
--- a/examples/01-basic/06-block-manipulation/package.json
+++ b/examples/01-basic/06-block-manipulation/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/01-basic/07-selection-blocks/package.json b/examples/01-basic/07-selection-blocks/package.json
index b9721da77c..0ad8dcd672 100644
--- a/examples/01-basic/07-selection-blocks/package.json
+++ b/examples/01-basic/07-selection-blocks/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/01-basic/08-ariakit/package.json b/examples/01-basic/08-ariakit/package.json
index 1d246711f8..5b3b14ccbe 100644
--- a/examples/01-basic/08-ariakit/package.json
+++ b/examples/01-basic/08-ariakit/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/01-basic/09-shadcn/package.json b/examples/01-basic/09-shadcn/package.json
index 65e0647624..8d842252d1 100644
--- a/examples/01-basic/09-shadcn/package.json
+++ b/examples/01-basic/09-shadcn/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "tailwindcss": "^4.1.14",
diff --git a/examples/01-basic/10-localization/package.json b/examples/01-basic/10-localization/package.json
index 3df0f75296..839e85bc1a 100644
--- a/examples/01-basic/10-localization/package.json
+++ b/examples/01-basic/10-localization/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/01-basic/11-custom-placeholder/package.json b/examples/01-basic/11-custom-placeholder/package.json
index fe75d85dae..8eea656745 100644
--- a/examples/01-basic/11-custom-placeholder/package.json
+++ b/examples/01-basic/11-custom-placeholder/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/01-basic/12-multi-editor/package.json b/examples/01-basic/12-multi-editor/package.json
index 531ec98b2c..01d46a5515 100644
--- a/examples/01-basic/12-multi-editor/package.json
+++ b/examples/01-basic/12-multi-editor/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/01-basic/13-custom-paste-handler/package.json b/examples/01-basic/13-custom-paste-handler/package.json
index bfcea4ce3d..047bc04b60 100644
--- a/examples/01-basic/13-custom-paste-handler/package.json
+++ b/examples/01-basic/13-custom-paste-handler/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/01-basic/testing/package.json b/examples/01-basic/testing/package.json
index e9ee6710fa..1d7ad1ef14 100644
--- a/examples/01-basic/testing/package.json
+++ b/examples/01-basic/testing/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/02-backend/01-file-uploading/package.json b/examples/02-backend/01-file-uploading/package.json
index 4421054008..8fa3abe662 100644
--- a/examples/02-backend/01-file-uploading/package.json
+++ b/examples/02-backend/01-file-uploading/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/02-backend/02-saving-loading/package.json b/examples/02-backend/02-saving-loading/package.json
index b8af2de7f2..33c35e1db5 100644
--- a/examples/02-backend/02-saving-loading/package.json
+++ b/examples/02-backend/02-saving-loading/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/02-backend/03-s3/package.json b/examples/02-backend/03-s3/package.json
index 4e75860468..4f0aea247d 100644
--- a/examples/02-backend/03-s3/package.json
+++ b/examples/02-backend/03-s3/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "@aws-sdk/client-s3": "^3.609.0",
diff --git a/examples/02-backend/04-rendering-static-documents/package.json b/examples/02-backend/04-rendering-static-documents/package.json
index ed960b24d2..bf5f3099d8 100644
--- a/examples/02-backend/04-rendering-static-documents/package.json
+++ b/examples/02-backend/04-rendering-static-documents/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "@blocknote/server-util": "latest"
diff --git a/examples/03-ui-components/01-ui-elements-remove/package.json b/examples/03-ui-components/01-ui-elements-remove/package.json
index bc949a4c77..88cb38e4ba 100644
--- a/examples/03-ui-components/01-ui-elements-remove/package.json
+++ b/examples/03-ui-components/01-ui-elements-remove/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/package.json b/examples/03-ui-components/02-formatting-toolbar-buttons/package.json
index 359e17aa52..a799ee1920 100644
--- a/examples/03-ui-components/02-formatting-toolbar-buttons/package.json
+++ b/examples/03-ui-components/02-formatting-toolbar-buttons/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/package.json b/examples/03-ui-components/03-formatting-toolbar-block-type-items/package.json
index 36ec799996..dd7baf25c5 100644
--- a/examples/03-ui-components/03-formatting-toolbar-block-type-items/package.json
+++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "react-icons": "^5.2.1"
diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/Alert.tsx b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/Alert.tsx
index 4b2af03fbe..535e6b0d77 100644
--- a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/Alert.tsx
+++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/Alert.tsx
@@ -1,8 +1,9 @@
-import { defaultProps } from "@blocknote/core";
 import { createReactBlockSpec } from "@blocknote/react";
 import { Menu } from "@mantine/core";
 import { MdCancel, MdCheckCircle, MdError, MdInfo } from "react-icons/md";
+import { z } from "zod/v4";
 
+import { createPropSchemaFromZod, defaultZodPropSchema } from "@blocknote/core";
 import "./styles.css";
 
 // The types of alerts that users can choose from.
@@ -53,14 +54,18 @@ export const alertTypes = [
 export const Alert = createReactBlockSpec(
   {
     type: "alert",
-    propSchema: {
-      textAlignment: defaultProps.textAlignment,
-      textColor: defaultProps.textColor,
-      type: {
-        default: "warning",
-        values: ["warning", "error", "info", "success"],
-      },
-    },
+    propSchema: createPropSchemaFromZod(
+      defaultZodPropSchema
+        .pick({
+          textAlignment: true,
+          textColor: true,
+        })
+        .extend({
+          type: z
+            .enum(["warning", "error", "info", "success"])
+            .default("warning"),
+        }),
+    ),
     content: "inline",
   },
   {
diff --git a/examples/03-ui-components/04-side-menu-buttons/package.json b/examples/03-ui-components/04-side-menu-buttons/package.json
index 29ec731747..7ba5a3024e 100644
--- a/examples/03-ui-components/04-side-menu-buttons/package.json
+++ b/examples/03-ui-components/04-side-menu-buttons/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "react-icons": "^5.2.1"
diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/package.json b/examples/03-ui-components/05-side-menu-drag-handle-items/package.json
index 02659f1458..7f180069fb 100644
--- a/examples/03-ui-components/05-side-menu-drag-handle-items/package.json
+++ b/examples/03-ui-components/05-side-menu-drag-handle-items/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "react-icons": "^5.2.1"
diff --git a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/package.json b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/package.json
index c8965682db..23bb23b5a0 100644
--- a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/package.json
+++ b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "react-icons": "^5.2.1"
diff --git a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/package.json b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/package.json
index 25b99a562d..5944d1fc55 100644
--- a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/package.json
+++ b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/package.json b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/package.json
index 777afaa140..487190c478 100644
--- a/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/package.json
+++ b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/package.json b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/package.json
index 68e10b5905..59aba8278f 100644
--- a/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/package.json
+++ b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/03-ui-components/10-suggestion-menus-grid-mentions/package.json b/examples/03-ui-components/10-suggestion-menus-grid-mentions/package.json
index 5dcdb24ae9..6182529517 100644
--- a/examples/03-ui-components/10-suggestion-menus-grid-mentions/package.json
+++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/03-ui-components/10-suggestion-menus-grid-mentions/src/Mention.tsx b/examples/03-ui-components/10-suggestion-menus-grid-mentions/src/Mention.tsx
index b19366ff65..920c74ba31 100644
--- a/examples/03-ui-components/10-suggestion-menus-grid-mentions/src/Mention.tsx
+++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/src/Mention.tsx
@@ -1,14 +1,16 @@
+import { createPropSchemaFromZod } from "@blocknote/core";
 import { createReactInlineContentSpec } from "@blocknote/react";
+import { z } from "zod/v4";
 
 // The Mention inline content.
 export const Mention = createReactInlineContentSpec(
   {
     type: "mention",
-    propSchema: {
-      user: {
-        default: "Unknown",
-      },
-    },
+    propSchema: createPropSchemaFromZod(
+      z.object({
+        user: z.string().default("Unknown"),
+      }),
+    ),
     content: "none",
   },
   {
diff --git a/examples/03-ui-components/11-uppy-file-panel/package.json b/examples/03-ui-components/11-uppy-file-panel/package.json
index 973ccae768..cb1dcd13d3 100644
--- a/examples/03-ui-components/11-uppy-file-panel/package.json
+++ b/examples/03-ui-components/11-uppy-file-panel/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "@uppy/core": "^3.13.1",
diff --git a/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx b/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx
index e73723a9f9..d1c4e2c008 100644
--- a/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx
+++ b/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx
@@ -1,8 +1,11 @@
 import {
-  BlockSchema,
+  baseFileZodPropSchema,
   blockHasType,
+  BlockSchema,
   InlineContentSchema,
+  optionalFileZodPropSchema,
   StyleSchema,
+  createPropSchemaFromZod
 } from "@blocknote/core";
 import {
   useBlockNoteEditor,
@@ -41,7 +44,15 @@ export const FileReplaceButton = () => {
 
   if (
     block === undefined ||
-    !blockHasType(block, editor, "file", { url: "string" }) ||
+    !blockHasType(
+      block,
+      editor,
+      block.type,
+      // TODO
+      createPropSchemaFromZod(baseFileZodPropSchema.extend({
+        ...optionalFileZodPropSchema.pick({ url: true }).shape,
+      })),
+    ) ||
     !editor.isEditable
   ) {
     return null;
diff --git a/examples/03-ui-components/12-static-formatting-toolbar/package.json b/examples/03-ui-components/12-static-formatting-toolbar/package.json
index 582274d79b..0603faaa3c 100644
--- a/examples/03-ui-components/12-static-formatting-toolbar/package.json
+++ b/examples/03-ui-components/12-static-formatting-toolbar/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/03-ui-components/13-custom-ui/package.json b/examples/03-ui-components/13-custom-ui/package.json
index e94e0a9c37..7897783575 100644
--- a/examples/03-ui-components/13-custom-ui/package.json
+++ b/examples/03-ui-components/13-custom-ui/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "@emotion/react": "^11.11.4",
diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/package.json b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/package.json
index 0caf0c2ab1..7711231ee2 100644
--- a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/package.json
+++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/03-ui-components/15-advanced-tables/package.json b/examples/03-ui-components/15-advanced-tables/package.json
index b058bf0b88..28c2e61e10 100644
--- a/examples/03-ui-components/15-advanced-tables/package.json
+++ b/examples/03-ui-components/15-advanced-tables/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/03-ui-components/16-link-toolbar-buttons/package.json b/examples/03-ui-components/16-link-toolbar-buttons/package.json
index dbc39e3c0b..c1954b7111 100644
--- a/examples/03-ui-components/16-link-toolbar-buttons/package.json
+++ b/examples/03-ui-components/16-link-toolbar-buttons/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/03-ui-components/17-advanced-tables-2/package.json b/examples/03-ui-components/17-advanced-tables-2/package.json
index 821d89d21b..96a6c18048 100644
--- a/examples/03-ui-components/17-advanced-tables-2/package.json
+++ b/examples/03-ui-components/17-advanced-tables-2/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/04-theming/01-theming-dom-attributes/package.json b/examples/04-theming/01-theming-dom-attributes/package.json
index ae80460c25..099e9a5be0 100644
--- a/examples/04-theming/01-theming-dom-attributes/package.json
+++ b/examples/04-theming/01-theming-dom-attributes/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/04-theming/02-changing-font/package.json b/examples/04-theming/02-changing-font/package.json
index 2712c03568..4d6c701aca 100644
--- a/examples/04-theming/02-changing-font/package.json
+++ b/examples/04-theming/02-changing-font/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/04-theming/03-theming-css/package.json b/examples/04-theming/03-theming-css/package.json
index 316e6e1660..b6e9e69e06 100644
--- a/examples/04-theming/03-theming-css/package.json
+++ b/examples/04-theming/03-theming-css/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/04-theming/04-theming-css-variables/package.json b/examples/04-theming/04-theming-css-variables/package.json
index 154970eb18..22c4b8c560 100644
--- a/examples/04-theming/04-theming-css-variables/package.json
+++ b/examples/04-theming/04-theming-css-variables/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/04-theming/05-theming-css-variables-code/package.json b/examples/04-theming/05-theming-css-variables-code/package.json
index 201a46de4f..a3f437083e 100644
--- a/examples/04-theming/05-theming-css-variables-code/package.json
+++ b/examples/04-theming/05-theming-css-variables-code/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/04-theming/06-code-block/package.json b/examples/04-theming/06-code-block/package.json
index 7aa6ac20bc..d6e52bbb8c 100644
--- a/examples/04-theming/06-code-block/package.json
+++ b/examples/04-theming/06-code-block/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "@blocknote/code-block": "latest"
diff --git a/examples/04-theming/07-custom-code-block/package.json b/examples/04-theming/07-custom-code-block/package.json
index 61de00be1b..1007569457 100644
--- a/examples/04-theming/07-custom-code-block/package.json
+++ b/examples/04-theming/07-custom-code-block/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "@blocknote/code-block": "latest",
diff --git a/examples/05-interoperability/01-converting-blocks-to-html/package.json b/examples/05-interoperability/01-converting-blocks-to-html/package.json
index 3338a38b05..2d83db1776 100644
--- a/examples/05-interoperability/01-converting-blocks-to-html/package.json
+++ b/examples/05-interoperability/01-converting-blocks-to-html/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/05-interoperability/02-converting-blocks-from-html/package.json b/examples/05-interoperability/02-converting-blocks-from-html/package.json
index 77f69540ce..59a8a16b39 100644
--- a/examples/05-interoperability/02-converting-blocks-from-html/package.json
+++ b/examples/05-interoperability/02-converting-blocks-from-html/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/05-interoperability/03-converting-blocks-to-md/package.json b/examples/05-interoperability/03-converting-blocks-to-md/package.json
index faa7202aa2..80556f5b3a 100644
--- a/examples/05-interoperability/03-converting-blocks-to-md/package.json
+++ b/examples/05-interoperability/03-converting-blocks-to-md/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/05-interoperability/04-converting-blocks-from-md/package.json b/examples/05-interoperability/04-converting-blocks-from-md/package.json
index 642727a0b3..cc15ad88d8 100644
--- a/examples/05-interoperability/04-converting-blocks-from-md/package.json
+++ b/examples/05-interoperability/04-converting-blocks-from-md/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/package.json b/examples/05-interoperability/05-converting-blocks-to-pdf/package.json
index bae781d726..783cf86511 100644
--- a/examples/05-interoperability/05-converting-blocks-to-pdf/package.json
+++ b/examples/05-interoperability/05-converting-blocks-to-pdf/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "@blocknote/xl-pdf-exporter": "latest",
diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/package.json b/examples/05-interoperability/06-converting-blocks-to-docx/package.json
index 542f835b4d..82b63fdab0 100644
--- a/examples/05-interoperability/06-converting-blocks-to-docx/package.json
+++ b/examples/05-interoperability/06-converting-blocks-to-docx/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "@blocknote/xl-docx-exporter": "latest",
diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx b/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx
index 9c91a99aff..1ac704f67a 100644
--- a/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx
+++ b/examples/05-interoperability/06-converting-blocks-to-docx/src/App.tsx
@@ -32,7 +32,7 @@ export default function App() {
   // Creates a new editor instance.
   const editor = useCreateBlockNote({
     // Adds support for page breaks & multi-column blocks.
-    schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())),
+    schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())) as any,
     dropCursor: multiColumnDropCursor,
     dictionary: {
       ...locales.en,
@@ -397,7 +397,10 @@ export default function App() {
 
   // Exports the editor content to DOCX and downloads it.
   const onDownloadClick = async () => {
-    const exporter = new DOCXExporter(editor.schema, docxDefaultSchemaMappings);
+    const exporter = new DOCXExporter(
+      editor.schema,
+      docxDefaultSchemaMappings as any,
+    );
     const blob = await exporter.toBlob(editor.document);
 
     const link = document.createElement("a");
diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/package.json b/examples/05-interoperability/07-converting-blocks-to-odt/package.json
index dbafdd7aee..063e66c0b8 100644
--- a/examples/05-interoperability/07-converting-blocks-to-odt/package.json
+++ b/examples/05-interoperability/07-converting-blocks-to-odt/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "@blocknote/xl-odt-exporter": "latest",
diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx b/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx
index 9ece22d905..6d4361eb59 100644
--- a/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx
+++ b/examples/05-interoperability/07-converting-blocks-to-odt/src/App.tsx
@@ -396,7 +396,10 @@ export default function App() {
 
   // Exports the editor content to ODT and downloads it.
   const onDownloadClick = async () => {
-    const exporter = new ODTExporter(editor.schema, odtDefaultSchemaMappings);
+    const exporter = new ODTExporter(
+      editor.schema,
+      odtDefaultSchemaMappings as any,
+    );
     const blob = await exporter.toODTDocument(editor.document);
 
     const link = document.createElement("a");
diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json
index 0e77d883f5..7e859a9ec6 100644
--- a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json
+++ b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "@blocknote/xl-email-exporter": "latest",
diff --git a/examples/06-custom-schema/01-alert-block/package.json b/examples/06-custom-schema/01-alert-block/package.json
index 67958db05b..9b02d18541 100644
--- a/examples/06-custom-schema/01-alert-block/package.json
+++ b/examples/06-custom-schema/01-alert-block/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "react-icons": "^5.2.1"
diff --git a/examples/06-custom-schema/01-alert-block/src/Alert.tsx b/examples/06-custom-schema/01-alert-block/src/Alert.tsx
index 079cb8bebc..f3f8626ca1 100644
--- a/examples/06-custom-schema/01-alert-block/src/Alert.tsx
+++ b/examples/06-custom-schema/01-alert-block/src/Alert.tsx
@@ -1,7 +1,8 @@
-import { defaultProps } from "@blocknote/core";
+import { createPropSchemaFromZod, defaultZodPropSchema } from "@blocknote/core";
 import { createReactBlockSpec } from "@blocknote/react";
 import { Menu } from "@mantine/core";
 import { MdCancel, MdCheckCircle, MdError, MdInfo } from "react-icons/md";
+import { z } from "zod/v4";
 
 import "./styles.css";
 
@@ -53,14 +54,18 @@ export const alertTypes = [
 export const createAlert = createReactBlockSpec(
   {
     type: "alert",
-    propSchema: {
-      textAlignment: defaultProps.textAlignment,
-      textColor: defaultProps.textColor,
-      type: {
-        default: "warning",
-        values: ["warning", "error", "info", "success"],
-      },
-    },
+    propSchema: createPropSchemaFromZod(
+      defaultZodPropSchema
+        .pick({
+          textAlignment: true,
+          textColor: true,
+        })
+        .extend({
+          type: z
+            .enum(["warning", "error", "info", "success"])
+            .default("warning"),
+        }),
+    ),
     content: "inline",
   },
   {
diff --git a/examples/06-custom-schema/02-suggestion-menus-mentions/package.json b/examples/06-custom-schema/02-suggestion-menus-mentions/package.json
index 7604218654..ad306997fd 100644
--- a/examples/06-custom-schema/02-suggestion-menus-mentions/package.json
+++ b/examples/06-custom-schema/02-suggestion-menus-mentions/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/06-custom-schema/02-suggestion-menus-mentions/src/Mention.tsx b/examples/06-custom-schema/02-suggestion-menus-mentions/src/Mention.tsx
index b19366ff65..920c74ba31 100644
--- a/examples/06-custom-schema/02-suggestion-menus-mentions/src/Mention.tsx
+++ b/examples/06-custom-schema/02-suggestion-menus-mentions/src/Mention.tsx
@@ -1,14 +1,16 @@
+import { createPropSchemaFromZod } from "@blocknote/core";
 import { createReactInlineContentSpec } from "@blocknote/react";
+import { z } from "zod/v4";
 
 // The Mention inline content.
 export const Mention = createReactInlineContentSpec(
   {
     type: "mention",
-    propSchema: {
-      user: {
-        default: "Unknown",
-      },
-    },
+    propSchema: createPropSchemaFromZod(
+      z.object({
+        user: z.string().default("Unknown"),
+      }),
+    ),
     content: "none",
   },
   {
diff --git a/examples/06-custom-schema/03-font-style/package.json b/examples/06-custom-schema/03-font-style/package.json
index 88be55cfdc..d5a38dad6c 100644
--- a/examples/06-custom-schema/03-font-style/package.json
+++ b/examples/06-custom-schema/03-font-style/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "react-icons": "^5.2.1"
diff --git a/examples/06-custom-schema/04-pdf-file-block/package.json b/examples/06-custom-schema/04-pdf-file-block/package.json
index 32641f7b5b..588ce092df 100644
--- a/examples/06-custom-schema/04-pdf-file-block/package.json
+++ b/examples/06-custom-schema/04-pdf-file-block/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "react-icons": "^5.2.1"
diff --git a/examples/06-custom-schema/04-pdf-file-block/src/PDF.tsx b/examples/06-custom-schema/04-pdf-file-block/src/PDF.tsx
index f17619d8ba..3a0dd4785c 100644
--- a/examples/06-custom-schema/04-pdf-file-block/src/PDF.tsx
+++ b/examples/06-custom-schema/04-pdf-file-block/src/PDF.tsx
@@ -1,9 +1,15 @@
-import { FileBlockConfig } from "@blocknote/core";
+import {
+  baseFileZodPropSchema,
+  createPropSchemaFromZod,
+  FileBlockConfig,
+  optionalFileZodPropSchema,
+} from "@blocknote/core";
 import {
   createReactBlockSpec,
   ReactCustomBlockRenderProps,
   ResizableFileBlockWrapper,
 } from "@blocknote/react";
+import { z } from "zod/v4";
 
 import { RiFilePdfFill } from "react-icons/ri";
 
@@ -33,24 +39,16 @@ export const PDFPreview = (
 export const PDF = createReactBlockSpec(
   {
     type: "pdf",
-    propSchema: {
-      name: {
-        default: "" as const,
-      },
-      url: {
-        default: "" as const,
-      },
-      caption: {
-        default: "" as const,
-      },
-      showPreview: {
-        default: true,
-      },
-      previewWidth: {
-        default: undefined,
-        type: "number",
-      },
-    },
+    propSchema: createPropSchemaFromZod(
+      z.object({}).extend({
+        ...baseFileZodPropSchema.shape,
+        ...optionalFileZodPropSchema.pick({
+          url: true,
+          showPreview: true,
+          previewWidth: true,
+        }).shape,
+      }),
+    ),
     content: "none",
   },
   {
diff --git a/examples/06-custom-schema/05-alert-block-full-ux/package.json b/examples/06-custom-schema/05-alert-block-full-ux/package.json
index 3ae14b72a9..06546c5c98 100644
--- a/examples/06-custom-schema/05-alert-block-full-ux/package.json
+++ b/examples/06-custom-schema/05-alert-block-full-ux/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "react-icons": "^5.2.1"
diff --git a/examples/06-custom-schema/05-alert-block-full-ux/src/Alert.tsx b/examples/06-custom-schema/05-alert-block-full-ux/src/Alert.tsx
index 079cb8bebc..f3f8626ca1 100644
--- a/examples/06-custom-schema/05-alert-block-full-ux/src/Alert.tsx
+++ b/examples/06-custom-schema/05-alert-block-full-ux/src/Alert.tsx
@@ -1,7 +1,8 @@
-import { defaultProps } from "@blocknote/core";
+import { createPropSchemaFromZod, defaultZodPropSchema } from "@blocknote/core";
 import { createReactBlockSpec } from "@blocknote/react";
 import { Menu } from "@mantine/core";
 import { MdCancel, MdCheckCircle, MdError, MdInfo } from "react-icons/md";
+import { z } from "zod/v4";
 
 import "./styles.css";
 
@@ -53,14 +54,18 @@ export const alertTypes = [
 export const createAlert = createReactBlockSpec(
   {
     type: "alert",
-    propSchema: {
-      textAlignment: defaultProps.textAlignment,
-      textColor: defaultProps.textColor,
-      type: {
-        default: "warning",
-        values: ["warning", "error", "info", "success"],
-      },
-    },
+    propSchema: createPropSchemaFromZod(
+      defaultZodPropSchema
+        .pick({
+          textAlignment: true,
+          textColor: true,
+        })
+        .extend({
+          type: z
+            .enum(["warning", "error", "info", "success"])
+            .default("warning"),
+        }),
+    ),
     content: "inline",
   },
   {
diff --git a/examples/06-custom-schema/06-toggleable-blocks/package.json b/examples/06-custom-schema/06-toggleable-blocks/package.json
index 39d91e1845..749d94496b 100644
--- a/examples/06-custom-schema/06-toggleable-blocks/package.json
+++ b/examples/06-custom-schema/06-toggleable-blocks/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/06-custom-schema/06-toggleable-blocks/src/Toggle.tsx b/examples/06-custom-schema/06-toggleable-blocks/src/Toggle.tsx
index 244661f841..3fdd52c609 100644
--- a/examples/06-custom-schema/06-toggleable-blocks/src/Toggle.tsx
+++ b/examples/06-custom-schema/06-toggleable-blocks/src/Toggle.tsx
@@ -1,13 +1,11 @@
-import { defaultProps } from "@blocknote/core";
+import { defaultPropSchema } from "@blocknote/core";
 import { createReactBlockSpec, ToggleWrapper } from "@blocknote/react";
 
 // The Toggle block that we want to add to our editor.
 export const ToggleBlock = createReactBlockSpec(
   {
     type: "toggle",
-    propSchema: {
-      ...defaultProps,
-    },
+    propSchema: defaultPropSchema,
     content: "inline",
   },
   {
diff --git a/examples/06-custom-schema/07-configuring-blocks/package.json b/examples/06-custom-schema/07-configuring-blocks/package.json
index 72ff5c631c..6a84292347 100644
--- a/examples/06-custom-schema/07-configuring-blocks/package.json
+++ b/examples/06-custom-schema/07-configuring-blocks/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/06-custom-schema/draggable-inline-content/package.json b/examples/06-custom-schema/draggable-inline-content/package.json
index def570b94f..28d8671212 100644
--- a/examples/06-custom-schema/draggable-inline-content/package.json
+++ b/examples/06-custom-schema/draggable-inline-content/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/06-custom-schema/draggable-inline-content/src/App.tsx b/examples/06-custom-schema/draggable-inline-content/src/App.tsx
index 03af1b7088..93de8f1c33 100644
--- a/examples/06-custom-schema/draggable-inline-content/src/App.tsx
+++ b/examples/06-custom-schema/draggable-inline-content/src/App.tsx
@@ -1,20 +1,25 @@
-import { BlockNoteSchema, defaultInlineContentSpecs } from "@blocknote/core";
+import {
+  BlockNoteSchema,
+  createPropSchemaFromZod,
+  defaultInlineContentSpecs,
+} from "@blocknote/core";
 import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
 import {
   createReactInlineContentSpec,
   useCreateBlockNote,
 } from "@blocknote/react";
-import { BlockNoteView } from "@blocknote/mantine";
-import "@blocknote/mantine/style.css";
+import { z } from "zod/v4";
 
 const draggableButton = createReactInlineContentSpec(
   {
     type: "draggableButton",
-    propSchema: {
-      title: {
-        default: "",
-      },
-    },
+    propSchema: createPropSchemaFromZod(
+      z.object({
+        title: z.string().default(""),
+      }),
+    ),
     content: "none",
   },
   {
diff --git a/examples/06-custom-schema/react-custom-blocks/package.json b/examples/06-custom-schema/react-custom-blocks/package.json
index 980b1f4b4c..b44e75db5c 100644
--- a/examples/06-custom-schema/react-custom-blocks/package.json
+++ b/examples/06-custom-schema/react-custom-blocks/package.json
@@ -19,6 +19,7 @@
     "@mantine/core": "^8.3.4",
     "@mantine/hooks": "^8.3.4",
     "@mantine/utils": "^6.0.22",
+    "zod": "^4.0.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },
diff --git a/examples/06-custom-schema/react-custom-blocks/src/App.tsx b/examples/06-custom-schema/react-custom-blocks/src/App.tsx
index dd0573877d..17ae045fd6 100644
--- a/examples/06-custom-schema/react-custom-blocks/src/App.tsx
+++ b/examples/06-custom-schema/react-custom-blocks/src/App.tsx
@@ -1,12 +1,15 @@
 import {
   BlockNoteSchema,
+  createPropSchemaFromZod,
   defaultBlockSpecs,
-  defaultProps,
+  defaultPropSchema,
+  defaultZodPropSchema,
 } from "@blocknote/core";
 import "@blocknote/core/fonts/inter.css";
-import { createReactBlockSpec, useCreateBlockNote } from "@blocknote/react";
 import { BlockNoteView } from "@blocknote/mantine";
 import "@blocknote/mantine/style.css";
+import { createReactBlockSpec, useCreateBlockNote } from "@blocknote/react";
+import { z } from "zod/v4";
 
 import "./styles.css";
 
@@ -37,14 +40,18 @@ const alertTypes = {
 export const alertBlock = createReactBlockSpec(
   {
     type: "alert",
-    propSchema: {
-      textAlignment: defaultProps.textAlignment,
-      textColor: defaultProps.textColor,
-      type: {
-        default: "warning",
-        values: ["warning", "error", "info", "success"],
-      } as const,
-    },
+    propSchema: createPropSchemaFromZod(
+      defaultZodPropSchema
+        .pick({
+          textAlignment: true,
+          textColor: true,
+        })
+        .extend({
+          type: z
+            .enum(["warning", "error", "info", "success"])
+            .default("warning"),
+        }),
+    ),
     content: "inline",
   },
   {
@@ -76,15 +83,48 @@ export const alertBlock = createReactBlockSpec(
   },
 );
 
+// TODO
+export const advancedBlock = createReactBlockSpec(
+  {
+    type: "advanced",
+    propSchema: createPropSchemaFromZod(
+      z.object({
+        nested: z.object({
+          type: z.enum(["warning", "error", "info", "success"]),
+          message: z.string(),
+        }),
+      }),
+    ),
+    content: "inline",
+  },
+  {
+    render: (props) => (
+      
+        
{props.block.props.nested.message}
+        
+      
${externalHTMLExporter.exportInlineContent(
-      ic as any,
+      ic,
       {},
     )}
`;
   } else if (isWithinBlockContent) {
diff --git a/packages/core/src/api/exporters/html/externalHTMLExporter.ts b/packages/core/src/api/exporters/html/externalHTMLExporter.ts
index 2149c884e7..fb6208d79a 100644
--- a/packages/core/src/api/exporters/html/externalHTMLExporter.ts
+++ b/packages/core/src/api/exporters/html/externalHTMLExporter.ts
@@ -1,12 +1,13 @@
 import { DOMSerializer, Schema } from "prosemirror-model";
 
-import { PartialBlock } from "../../../blocks/defaultBlocks.js";
+import { Block } from "../../../blocks/defaultBlocks.js";
 import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
 import {
   BlockSchema,
   InlineContent,
   InlineContentSchema,
   StyleSchema,
+  TableContent,
 } from "../../../schema/index.js";
 import {
   serializeBlocksExternalHTML,
@@ -40,7 +41,7 @@ export const createExternalHTMLExporter = <
 
   return {
     exportBlocks: (
-      blocks: PartialBlock[],
+      blocks: Block[],
       options: { document?: Document },
     ) => {
       const html = serializeBlocksExternalHTML(
@@ -57,12 +58,12 @@ export const createExternalHTMLExporter = <
     },
 
     exportInlineContent: (
-      inlineContent: InlineContent[],
+      inlineContent: InlineContent[] | TableContent,
       options: { document?: Document },
     ) => {
       const domFragment = serializeInlineContentExternalHTML(
         editor,
-        inlineContent as any,
+        inlineContent,
         serializer,
         options,
       );
diff --git a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts
index 5b3003cf55..3f1da10613 100644
--- a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts
+++ b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts
@@ -1,5 +1,6 @@
 import { DOMSerializer, Schema } from "prosemirror-model";
-import { PartialBlock } from "../../../blocks/defaultBlocks.js";
+
+import { Block } from "../../../blocks/index.js";
 import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
 import {
   BlockSchema,
@@ -28,7 +29,7 @@ export const createInternalHTMLSerializer = <
 
   return {
     serializeBlocks: (
-      blocks: PartialBlock[],
+      blocks: Block[],
       options: { document?: Document },
     ) => {
       return serializeBlocksInternalHTML(editor, blocks, serializer, options)
diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
index 17ab49fe69..923dddc7d1 100644
--- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
@@ -1,12 +1,13 @@
 import { DOMSerializer, Fragment, Node } from "prosemirror-model";
-
-import { PartialBlock } from "../../../../blocks/defaultBlocks.js";
+import { Block } from "../../../../blocks/index.js";
 import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
 import {
   BlockImplementation,
   BlockSchema,
+  InlineContent,
   InlineContentSchema,
   StyleSchema,
+  TableContent,
 } from "../../../../schema/index.js";
 import { UnreachableCaseError } from "../../../../util/typescript.js";
 import {
@@ -34,8 +35,8 @@ export function serializeInlineContentExternalHTML<
   I extends InlineContentSchema,
   S extends StyleSchema,
 >(
-  editor: BlockNoteEditor,
-  blockContent: PartialBlock["content"],
+  editor: BlockNoteEditor,
+  blockContent: InlineContent[] | TableContent,
   serializer: DOMSerializer,
   options?: { document?: Document },
 ) {
@@ -44,8 +45,6 @@ export function serializeInlineContentExternalHTML<
   // TODO: reuse function from nodeconversions?
   if (!blockContent) {
     throw new Error("blockContent is required");
-  } else if (typeof blockContent === "string") {
-    nodes = inlineContentToNodes([blockContent], editor.pmSchema);
   } else if (Array.isArray(blockContent)) {
     nodes = inlineContentToNodes(blockContent, editor.pmSchema);
   } else if (blockContent.type === "tableContent") {
@@ -166,7 +165,7 @@ function serializeBlock<
 >(
   fragment: DocumentFragment,
   editor: BlockNoteEditor,
-  block: PartialBlock,
+  block: Block,
   serializer: DOMSerializer,
   orderedListItemBlockTypes: Set,
   unorderedListItemBlockTypes: Set,
@@ -175,20 +174,10 @@ function serializeBlock<
   const doc = options?.document ?? document;
   const BC_NODE = editor.pmSchema.nodes["blockContainer"];
 
-  // set default props in case we were passed a partial block
-  const props = block.props || {};
-  for (const [name, spec] of Object.entries(
-    editor.schema.blockSchema[block.type as any].propSchema,
-  )) {
-    if (!(name in props) && spec.default !== undefined) {
-      (props as any)[name] = spec.default;
-    }
-  }
-
   const bc = BC_NODE.spec?.toDOM?.(
     BC_NODE.create({
       id: block.id,
-      ...props,
+      ...block.props,
     }),
   ) as {
     dom: HTMLElement;
@@ -204,14 +193,10 @@ function serializeBlock<
   const ret =
     blockImplementation.toExternalHTML?.call(
       {},
-      { ...block, props } as any,
+      { ...block } as any,
       editor as any,
     ) ||
-    blockImplementation.render.call(
-      {},
-      { ...block, props } as any,
-      editor as any,
-    );
+    blockImplementation.render.call({}, { ...block } as any, editor as any);
 
   const elementFragment = doc.createDocumentFragment();
 
@@ -240,11 +225,10 @@ function serializeBlock<
   } else {
     elementFragment.append(ret.dom);
   }
-
   if (ret.contentDOM && block.content) {
     const ic = serializeInlineContentExternalHTML(
       editor,
-      block.content as any, // TODO
+      block.content,
       serializer,
       options,
     );
@@ -265,11 +249,11 @@ function serializeBlock<
 
       if (
         listType === "OL" &&
-        "start" in props &&
-        props.start &&
-        props?.start !== 1
+        "start" in block.props &&
+        block.props.start &&
+        block.props?.start !== 1
       ) {
-        list.setAttribute("start", props.start + "");
+        list.setAttribute("start", block.props.start + "");
       }
       fragment.append(list);
     }
@@ -319,7 +303,7 @@ const serializeBlocksToFragment = <
 >(
   fragment: DocumentFragment,
   editor: BlockNoteEditor,
-  blocks: PartialBlock[],
+  blocks: Block[],
   serializer: DOMSerializer,
   orderedListItemBlockTypes: Set,
   unorderedListItemBlockTypes: Set,
@@ -344,7 +328,7 @@ export const serializeBlocksExternalHTML = <
   S extends StyleSchema,
 >(
   editor: BlockNoteEditor,
-  blocks: PartialBlock[],
+  blocks: Block[],
   serializer: DOMSerializer,
   orderedListItemBlockTypes: Set,
   unorderedListItemBlockTypes: Set,
diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
index 0f890b77ab..48f0dcf5b9 100644
--- a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
@@ -1,11 +1,13 @@
 import { DOMSerializer, Fragment, Node } from "prosemirror-model";
 
-import { PartialBlock } from "../../../../blocks/defaultBlocks.js";
+import { Block } from "../../../../blocks/defaultBlocks.js";
 import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
 import {
   BlockSchema,
+  InlineContent,
   InlineContentSchema,
   StyleSchema,
+  TableContent,
 } from "../../../../schema/index.js";
 import { UnreachableCaseError } from "../../../../util/typescript.js";
 import {
@@ -19,8 +21,8 @@ export function serializeInlineContentInternalHTML<
   I extends InlineContentSchema,
   S extends StyleSchema,
 >(
-  editor: BlockNoteEditor,
-  blockContent: PartialBlock["content"],
+  editor: BlockNoteEditor,
+  blockContent: InlineContent[] | TableContent,
   serializer: DOMSerializer,
   blockType?: string,
   options?: { document?: Document },
@@ -30,8 +32,6 @@ export function serializeInlineContentInternalHTML<
   // TODO: reuse function from nodeconversions?
   if (!blockContent) {
     throw new Error("blockContent is required");
-  } else if (typeof blockContent === "string") {
-    nodes = inlineContentToNodes([blockContent], editor.pmSchema, blockType);
   } else if (Array.isArray(blockContent)) {
     nodes = inlineContentToNodes(blockContent, editor.pmSchema, blockType);
   } else if (blockContent.type === "tableContent") {
@@ -132,37 +132,26 @@ function serializeBlock<
   S extends StyleSchema,
 >(
   editor: BlockNoteEditor,
-  block: PartialBlock,
+  block: Block,
   serializer: DOMSerializer,
   options?: { document?: Document },
 ) {
   const BC_NODE = editor.pmSchema.nodes["blockContainer"];
 
-  // set default props in case we were passed a partial block
-  const props = block.props || {};
-  for (const [name, spec] of Object.entries(
-    editor.schema.blockSchema[block.type as any].propSchema,
-  )) {
-    if (!(name in props) && spec.default !== undefined) {
-      (props as any)[name] = spec.default;
-    }
-  }
-  const children = block.children || [];
-
   const impl = editor.blockImplementations[block.type as any].implementation;
   const ret = impl.render.call(
     {
       renderType: "dom",
       props: undefined,
     },
-    { ...block, props, children } as any,
+    block,
     editor as any,
   );
 
   if (ret.contentDOM && block.content) {
     const ic = serializeInlineContentInternalHTML(
       editor,
-      block.content as any, // TODO
+      block.content,
       serializer,
       block.type,
       options,
@@ -190,7 +179,7 @@ function serializeBlock<
   const bc = BC_NODE.spec?.toDOM?.(
     BC_NODE.create({
       id: block.id,
-      ...props,
+      ...block.props,
     }),
   ) as {
     dom: HTMLElement;
@@ -213,7 +202,7 @@ function serializeBlocks<
   S extends StyleSchema,
 >(
   editor: BlockNoteEditor,
-  blocks: PartialBlock[],
+  blocks: Block[],
   serializer: DOMSerializer,
   options?: { document?: Document },
 ) {
@@ -234,7 +223,7 @@ export const serializeBlocksInternalHTML = <
   S extends StyleSchema,
 >(
   editor: BlockNoteEditor,
-  blocks: PartialBlock[],
+  blocks: Block[],
   serializer: DOMSerializer,
   options?: { document?: Document },
 ) => {
diff --git a/packages/core/src/api/exporters/markdown/markdownExporter.ts b/packages/core/src/api/exporters/markdown/markdownExporter.ts
index 23aad8db7c..6950f17254 100644
--- a/packages/core/src/api/exporters/markdown/markdownExporter.ts
+++ b/packages/core/src/api/exporters/markdown/markdownExporter.ts
@@ -5,7 +5,7 @@ import remarkGfm from "remark-gfm";
 import remarkStringify from "remark-stringify";
 import { unified } from "unified";
 
-import { PartialBlock } from "../../../blocks/defaultBlocks.js";
+import { Block } from "../../../blocks/defaultBlocks.js";
 import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
 import {
   BlockSchema,
@@ -13,9 +13,9 @@ import {
   StyleSchema,
 } from "../../../schema/index.js";
 import { createExternalHTMLExporter } from "../html/externalHTMLExporter.js";
-import { removeUnderlines } from "./util/removeUnderlinesRehypePlugin.js";
 import { addSpacesToCheckboxes } from "./util/addSpacesToCheckboxesRehypePlugin.js";
 import { convertVideoToMarkdown } from "./util/convertVideoToMarkdownRehypePlugin.js";
+import { removeUnderlines } from "./util/removeUnderlinesRehypePlugin.js";
 
 // Needs to be sync because it's used in drag handler event (SideMenuPlugin)
 export function cleanHTMLToMarkdown(cleanHTMLString: string) {
@@ -39,7 +39,7 @@ export function blocksToMarkdown<
   I extends InlineContentSchema,
   S extends StyleSchema,
 >(
-  blocks: PartialBlock[],
+  blocks: Block[],
   schema: Schema,
   editor: BlockNoteEditor,
   options: { document?: Document },
diff --git a/packages/core/src/api/getBlocksChangedByTransaction.ts b/packages/core/src/api/getBlocksChangedByTransaction.ts
index c45af4cb71..de184ed5b4 100644
--- a/packages/core/src/api/getBlocksChangedByTransaction.ts
+++ b/packages/core/src/api/getBlocksChangedByTransaction.ts
@@ -171,7 +171,7 @@ function collectSnapshot<
     if (!childrenByParent[key]) {
       childrenByParent[key] = [];
     }
-    const block = nodeToBlock(node, pmSchema);
+    const block = nodeToBlock(node, pmSchema);
     byId[node.attrs.id] = { block, parentId };
     childrenByParent[key].push(node.attrs.id);
     return true;
diff --git a/packages/core/src/api/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts
index 660d97406e..6aff41d076 100644
--- a/packages/core/src/api/nodeConversions/blockToNode.ts
+++ b/packages/core/src/api/nodeConversions/blockToNode.ts
@@ -1,22 +1,22 @@
 import { Attrs, Fragment, Mark, Node, Schema } from "@tiptap/pm/model";
 
-import UniqueID from "../../extensions/UniqueID/UniqueID.js";
 import type {
+  BlockSchema,
+  CustomInlineContentFromConfig,
+  InlineContent,
   InlineContentSchema,
-  PartialCustomInlineContentFromConfig,
-  PartialInlineContent,
-  PartialLink,
-  PartialTableContent,
+  Link,
   StyleSchema,
   StyledText,
+  TableContent,
 } from "../../schema";
 
-import type { PartialBlock } from "../../blocks/defaultBlocks";
+import type { Block } from "../../blocks/index.js";
 import {
-  isPartialLinkInlineContent,
+  isLinkInlineContent,
   isStyledTextInlineContent,
 } from "../../schema/inlineContent/types.js";
-import { getColspan, isPartialTableCell } from "../../util/table.js";
+import { getColspan, isTableCell } from "../../util/table.js";
 import { UnreachableCaseError } from "../../util/typescript.js";
 import { getAbsoluteTableCells } from "../blockManipulation/tables/tables.js";
 import { getStyleSchema } from "../pmUtil.js";
@@ -83,7 +83,7 @@ function styledTextToNodes(
  * prosemirror text nodes with the appropriate marks
  */
 function linkToNodes(
-  link: PartialLink,
+  link: Link,
   schema: Schema,
   styleSchema: StyleSchema,
 ): Node[] {
@@ -110,6 +110,7 @@ function linkToNodes(
  * prosemirror text nodes with the appropriate marks
  */
 function styledTextArrayToNodes(
+  // this is lenient to "partial" inline content. FIXME: let's simplify and remove support for `string` here
   content: string | StyledText[],
   schema: Schema,
   styleSchema: S,
@@ -144,7 +145,8 @@ export function inlineContentToNodes<
   I extends InlineContentSchema,
   S extends StyleSchema,
 >(
-  blockContent: PartialInlineContent,
+  // this is lenient to "partial" inline content. FIXME: let's simplify and remove support for `string[]` here
+  blockContent: string[] | InlineContent[],
   schema: Schema,
   blockType?: string,
   styleSchema: S = getStyleSchema(schema),
@@ -153,10 +155,11 @@ export function inlineContentToNodes<
 
   for (const content of blockContent) {
     if (typeof content === "string") {
+      // TODO: remove?
       nodes.push(
         ...styledTextArrayToNodes(content, schema, styleSchema, blockType),
       );
-    } else if (isPartialLinkInlineContent(content)) {
+    } else if (isLinkInlineContent(content)) {
       nodes.push(...linkToNodes(content, schema, styleSchema));
     } else if (isStyledTextInlineContent(content)) {
       nodes.push(
@@ -178,7 +181,7 @@ export function tableContentToNodes<
   I extends InlineContentSchema,
   S extends StyleSchema,
 >(
-  tableContent: PartialTableContent,
+  tableContent: TableContent,
   schema: Schema,
   styleSchema: StyleSchema = getStyleSchema(schema),
 ): Node[] {
@@ -227,7 +230,7 @@ export function tableContentToNodes<
         // No-op
       } else if (typeof cell === "string") {
         content = schema.text(cell);
-      } else if (isPartialTableCell(cell)) {
+      } else if (isTableCell(cell)) {
         if (cell.content) {
           content = inlineContentToNodes(
             cell.content,
@@ -258,7 +261,7 @@ export function tableContentToNodes<
         isHeaderCol || isHeaderRow ? "tableHeader" : "tableCell"
       ].createChecked(
         {
-          ...(isPartialTableCell(cell) ? cell.props : {}),
+          ...(isTableCell(cell) ? cell.props : {}),
           colwidth,
         },
         schema.nodes["tableParagraph"].createChecked(attrs, content),
@@ -273,19 +276,12 @@ export function tableContentToNodes<
 }
 
 function blockOrInlineContentToContentNode(
-  block:
-    | PartialBlock
-    | PartialCustomInlineContentFromConfig,
+  block: Block | CustomInlineContentFromConfig,
   schema: Schema,
   styleSchema: StyleSchema,
 ) {
+  const type = block.type;
   let contentNode: Node;
-  let type = block.type;
-
-  // TODO: needed? came from previous code
-  if (type === undefined) {
-    type = "paragraph";
-  }
 
   if (!schema.nodes[type]) {
     throw new Error(`node type ${type} not found in schema`);
@@ -321,17 +317,15 @@ function blockOrInlineContentToContentNode(
 /**
  * Converts a BlockNote block to a Prosemirror node.
  */
-export function blockToNode(
-  block: PartialBlock,
+export function blockToNode<
+  BSchema extends BlockSchema,
+  I extends InlineContentSchema,
+  S extends StyleSchema,
+>(
+  block: Block,
   schema: Schema,
-  styleSchema: StyleSchema = getStyleSchema(schema),
+  styleSchema: S = getStyleSchema(schema),
 ) {
-  let id = block.id;
-
-  if (id === undefined) {
-    id = UniqueID.options.generateID();
-  }
-
   const children: Node[] = [];
 
   if (block.children) {
@@ -360,7 +354,7 @@ export function blockToNode(
 
     return schema.nodes["blockContainer"].createChecked(
       {
-        id: id,
+        id: block.id,
         ...block.props,
       },
       groupNode ? [contentNode, groupNode] : contentNode,
@@ -369,7 +363,7 @@ export function blockToNode(
     // this is a bnBlock node like Column or ColumnList that directly translates to a prosemirror node
     return schema.nodes[block.type].createChecked(
       {
-        id: id,
+        id: block.id,
         ...block.props,
       },
       children,
diff --git a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts
index 724b552bda..878c2eb032 100644
--- a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts
+++ b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts
@@ -1,6 +1,6 @@
 import { Fragment } from "@tiptap/pm/model";
+import type { Block } from "../../blocks/index.js";
 import {
-  BlockNoDefaults,
   BlockSchema,
   InlineContentSchema,
   StyleSchema,
@@ -18,7 +18,7 @@ export function fragmentToBlocks<
 >(fragment: Fragment) {
   // first convert selection to blocknote-style blocks, and then
   // pass these to the exporter
-  const blocks: BlockNoDefaults[] = [];
+  const blocks: Block[] = [];
   fragment.descendants((node) => {
     const pmSchema = getPmSchema(node);
     if (node.type.name === "blockContainer") {
diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts
index 1f5d2c75d4..907df635d4 100644
--- a/packages/core/src/api/nodeConversions/nodeToBlock.ts
+++ b/packages/core/src/api/nodeConversions/nodeToBlock.ts
@@ -1,4 +1,5 @@
 import { Mark, Node, Schema, Slice } from "@tiptap/pm/model";
+import * as z from "zod/v4/core";
 import type { Block } from "../../blocks/defaultBlocks.js";
 import UniqueID from "../../extensions/UniqueID/UniqueID.js";
 import type {
@@ -25,7 +26,6 @@ import {
   getInlineContentSchema,
   getStyleSchema,
 } from "../pmUtil.js";
-
 /**
  * Converts an internal (prosemirror) table node contentto a BlockNote Tablecontent
  */
@@ -355,7 +355,7 @@ export function nodeToCustomInlineContent<
       throw Error("ic node is of an unrecognized type: " + node.type.name);
     }
 
-    const propSchema = icConfig.propSchema;
+    const propSchema = icConfig.propSchema._zodSource._zod.def.shape;
 
     if (attr in propSchema) {
       props[attr] = value;
@@ -424,20 +424,12 @@ export function nodeToBlock<
     throw Error("Block is of an unrecognized type: " + blockInfo.blockNoteType);
   }
 
-  const props: any = {};
-  for (const [attr, value] of Object.entries({
+  const rawAttrs = {
     ...node.attrs,
     ...(blockInfo.isBlockContainer ? blockInfo.blockContent.node.attrs : {}),
-  })) {
-    const propSchema = blockSpec.propSchema;
+  };
 
-    if (
-      attr in propSchema &&
-      !(propSchema[attr].default === undefined && value === undefined)
-    ) {
-      props[attr] = value;
-    }
-  }
+  const props = z.parse(blockSpec.propSchema._zodSource, rawAttrs);
 
   const blockConfig = blockSchema[blockInfo.blockNoteType];
 
diff --git a/packages/core/src/api/pmUtil.ts b/packages/core/src/api/pmUtil.ts
index e47a3f2a75..0f899acd50 100644
--- a/packages/core/src/api/pmUtil.ts
+++ b/packages/core/src/api/pmUtil.ts
@@ -1,8 +1,8 @@
 import type { Node, Schema } from "prosemirror-model";
 import { Transform } from "prosemirror-transform";
 import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
-import { BlockNoteSchema } from "../blocks/BlockNoteSchema.js";
 import type { BlockSchema } from "../schema/blocks/types.js";
+import { CustomBlockNoteSchema } from "../schema/CustomBlockNoteSchema.js";
 import type { InlineContentSchema } from "../schema/inlineContent/types.js";
 import type { StyleSchema } from "../schema/styles/types.js";
 
@@ -25,12 +25,8 @@ export function getBlockNoteSchema<
   BSchema extends BlockSchema,
   I extends InlineContentSchema,
   S extends StyleSchema,
->(schema: Schema): BlockNoteSchema {
-  return getBlockNoteEditor(schema).schema as unknown as BlockNoteSchema<
-    BSchema,
-    I,
-    S
-  >;
+>(schema: Schema): CustomBlockNoteSchema {
+  return getBlockNoteEditor(schema).schema as any;
 }
 
 export function getBlockSchema(
diff --git a/packages/core/src/blocks/Audio/block.ts b/packages/core/src/blocks/Audio/block.ts
index f271fcb16a..65e35d034a 100644
--- a/packages/core/src/blocks/Audio/block.ts
+++ b/packages/core/src/blocks/Audio/block.ts
@@ -1,10 +1,15 @@
-import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
+import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
 import {
-  BlockFromConfig,
+  type BlockFromConfig,
   createBlockConfig,
   createBlockSpec,
+  createPropSchemaFromZod,
 } from "../../schema/index.js";
-import { defaultProps, parseDefaultProps } from "../defaultProps.js";
+import {
+  baseFileZodPropSchema,
+  optionalFileZodPropSchema,
+} from "../defaultFileProps.js";
+import { defaultZodPropSchema, parseDefaultProps } from "../defaultProps.js";
 import { parseFigureElement } from "../File/helpers/parse/parseFigureElement.js";
 import { createFileBlockWrapper } from "../File/helpers/render/createFileBlockWrapper.js";
 import { createFigureWithCaption } from "../File/helpers/toExternalHTML/createFigureWithCaption.js";
@@ -20,29 +25,23 @@ export interface AudioOptions {
 
 export type AudioBlockConfig = ReturnType;
 
+const audioZodPropSchema = defaultZodPropSchema
+  .pick({
+    backgroundColor: true,
+  })
+  .extend({
+    ...baseFileZodPropSchema.shape,
+    ...optionalFileZodPropSchema.pick({
+      url: true,
+      showPreview: true,
+    }).shape,
+  });
+
 export const createAudioBlockConfig = createBlockConfig(
   (_ctx: AudioOptions) =>
     ({
       type: "audio" as const,
-      propSchema: {
-        backgroundColor: defaultProps.backgroundColor,
-        // File name.
-        name: {
-          default: "" as const,
-        },
-        // File url.
-        url: {
-          default: "" as const,
-        },
-        // File caption.
-        caption: {
-          default: "" as const,
-        },
-
-        showPreview: {
-          default: true,
-        },
-      },
+      propSchema: createPropSchemaFromZod(audioZodPropSchema),
       content: "none",
     }) as const,
 );
diff --git a/packages/core/src/blocks/BlockNoteSchema.ts b/packages/core/src/blocks/BlockNoteSchema.ts
index 37b60220da..3b2a29b11f 100644
--- a/packages/core/src/blocks/BlockNoteSchema.ts
+++ b/packages/core/src/blocks/BlockNoteSchema.ts
@@ -38,7 +38,7 @@ export class BlockNoteSchema<
      * A list of custom Styles that should be available in the editor.
      */
     styleSpecs?: SSpecs;
-  }): BlockNoteSchema<
+  }): CustomBlockNoteSchema<
     BSpecs extends undefined
       ? BlockSchemaFromSpecs
       : BlockSchemaFromSpecs>,
@@ -49,7 +49,7 @@ export class BlockNoteSchema<
       ? StyleSchemaFromSpecs
       : StyleSchemaFromSpecs>
   > {
-    return new BlockNoteSchema({
+    return new CustomBlockNoteSchema({
       blockSpecs: options?.blockSpecs ?? defaultBlockSpecs,
       inlineContentSpecs:
         options?.inlineContentSpecs ?? defaultInlineContentSpecs,
diff --git a/packages/core/src/blocks/Code/block.ts b/packages/core/src/blocks/Code/block.ts
index 1458257e48..ca858e676a 100644
--- a/packages/core/src/blocks/Code/block.ts
+++ b/packages/core/src/blocks/Code/block.ts
@@ -1,8 +1,13 @@
 import type { HighlighterGeneric } from "@shikijs/types";
+import { DOMParser } from "@tiptap/pm/model";
+import { z } from "zod/v4";
 import { createBlockNoteExtension } from "../../editor/BlockNoteExtension.js";
-import { createBlockConfig, createBlockSpec } from "../../schema/index.js";
+import {
+  createBlockConfig,
+  createBlockSpec,
+  createPropSchemaFromZod,
+} from "../../schema/index.js";
 import { lazyShikiPlugin } from "./shiki.js";
-import { DOMParser } from "@tiptap/pm/model";
 
 export type CodeBlockOptions = {
   /**
@@ -57,11 +62,11 @@ export const createCodeBlockConfig = createBlockConfig(
   ({ defaultLanguage = "text" }: CodeBlockOptions) =>
     ({
       type: "codeBlock" as const,
-      propSchema: {
-        language: {
-          default: defaultLanguage,
-        },
-      },
+      propSchema: createPropSchemaFromZod(
+        z.object({
+          language: z.string().default(defaultLanguage),
+        }),
+      ),
       content: "inline",
     }) as const,
 );
diff --git a/packages/core/src/blocks/Divider/block.ts b/packages/core/src/blocks/Divider/block.ts
index 3de2211d3f..6443ac1164 100644
--- a/packages/core/src/blocks/Divider/block.ts
+++ b/packages/core/src/blocks/Divider/block.ts
@@ -1,5 +1,10 @@
+import { z } from "zod/v4";
 import { createBlockNoteExtension } from "../../editor/BlockNoteExtension.js";
-import { createBlockConfig, createBlockSpec } from "../../schema/index.js";
+import {
+  createBlockConfig,
+  createBlockSpec,
+  createPropSchemaFromZod,
+} from "../../schema/index.js";
 
 export type DividerBlockConfig = ReturnType;
 
@@ -7,7 +12,7 @@ export const createDividerBlockConfig = createBlockConfig(
   () =>
     ({
       type: "divider" as const,
-      propSchema: {},
+      propSchema: createPropSchemaFromZod(z.object({})),
       content: "none",
     }) as const,
 );
diff --git a/packages/core/src/blocks/File/block.ts b/packages/core/src/blocks/File/block.ts
index a506cc45a3..d409e4995c 100644
--- a/packages/core/src/blocks/File/block.ts
+++ b/packages/core/src/blocks/File/block.ts
@@ -1,5 +1,13 @@
-import { createBlockConfig, createBlockSpec } from "../../schema/index.js";
-import { defaultProps, parseDefaultProps } from "../defaultProps.js";
+import {
+  createBlockConfig,
+  createBlockSpec,
+  createPropSchemaFromZod,
+} from "../../schema/index.js";
+import {
+  baseFileZodPropSchema,
+  optionalFileZodPropSchema,
+} from "../defaultFileProps.js";
+import { defaultZodPropSchema, parseDefaultProps } from "../defaultProps.js";
 import { parseEmbedElement } from "./helpers/parse/parseEmbedElement.js";
 import { parseFigureElement } from "./helpers/parse/parseFigureElement.js";
 import { createFileBlockWrapper } from "./helpers/render/createFileBlockWrapper.js";
@@ -7,25 +15,22 @@ import { createLinkWithCaption } from "./helpers/toExternalHTML/createLinkWithCa
 
 export type FileBlockConfig = ReturnType;
 
+const fileZodPropSchema = defaultZodPropSchema
+  .pick({
+    backgroundColor: true,
+  })
+  .extend({
+    ...baseFileZodPropSchema.shape,
+    ...optionalFileZodPropSchema.pick({
+      url: true,
+    }).shape,
+  });
+
 export const createFileBlockConfig = createBlockConfig(
   () =>
     ({
       type: "file" as const,
-      propSchema: {
-        backgroundColor: defaultProps.backgroundColor,
-        // File name.
-        name: {
-          default: "" as const,
-        },
-        // File url.
-        url: {
-          default: "" as const,
-        },
-        // File caption.
-        caption: {
-          default: "" as const,
-        },
-      },
+      propSchema: createPropSchemaFromZod(fileZodPropSchema),
       content: "none" as const,
     }) as const,
 );
diff --git a/packages/core/src/blocks/File/helpers/render/createAddFileButton.ts b/packages/core/src/blocks/File/helpers/render/createAddFileButton.ts
index 227856b4ac..f322f2614d 100644
--- a/packages/core/src/blocks/File/helpers/render/createAddFileButton.ts
+++ b/packages/core/src/blocks/File/helpers/render/createAddFileButton.ts
@@ -1,11 +1,8 @@
 import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
-import {
-  BlockConfig,
-  BlockFromConfigNoChildren,
-} from "../../../../schema/index.js";
+import type { Block } from "../../../index.js";
 
 export const createAddFileButton = (
-  block: BlockFromConfigNoChildren, any, any>,
+  block: Block,
   editor: BlockNoteEditor,
   buttonIcon?: HTMLElement,
 ) => {
diff --git a/packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts b/packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts
index cbb347acff..a97de01b81 100644
--- a/packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts
+++ b/packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts
@@ -1,22 +1,24 @@
 import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
-import {
+import type {
   BlockConfig,
-  BlockFromConfigNoChildren,
+  BlockFromConfig,
+  PropSchemaFromZod,
 } from "../../../../schema/index.js";
+import {
+  baseFileZodPropSchema,
+  optionalFileZodPropSchema,
+} from "../../../defaultFileProps.js";
 import { createAddFileButton } from "./createAddFileButton.js";
 import { createFileNameWithIcon } from "./createFileNameWithIcon.js";
 
+const requiredZodPropSchema = baseFileZodPropSchema.extend({
+  ...optionalFileZodPropSchema.pick({ url: true }).shape,
+});
 export const createFileBlockWrapper = (
-  block: BlockFromConfigNoChildren<
+  block: BlockFromConfig<
     BlockConfig<
       string,
-      {
-        backgroundColor: { default: "default" };
-        name: { default: "" };
-        url: { default: "" };
-        caption: { default: "" };
-        showPreview?: { default: true };
-      },
+      PropSchemaFromZod,
       "none"
     >,
     any,
@@ -58,7 +60,7 @@ export const createFileBlockWrapper = (
   const ret: { dom: HTMLElement; destroy?: () => void } = { dom: wrapper };
 
   // Show the file preview, or the file name and icon.
-  if (block.props.showPreview === false || !element) {
+  if ((block.props as any).showPreview === false || !element) {
     // Show file name and icon.
     const fileNameWithIcon = createFileNameWithIcon(block);
     wrapper.appendChild(fileNameWithIcon.dom);
diff --git a/packages/core/src/blocks/File/helpers/render/createFileNameWithIcon.ts b/packages/core/src/blocks/File/helpers/render/createFileNameWithIcon.ts
index 05ba0d6281..c9a4f8ff7c 100644
--- a/packages/core/src/blocks/File/helpers/render/createFileNameWithIcon.ts
+++ b/packages/core/src/blocks/File/helpers/render/createFileNameWithIcon.ts
@@ -1,17 +1,17 @@
 import {
   BlockConfig,
-  BlockFromConfigNoChildren,
+  BlockFromConfig,
+  PropSchemaFromZod,
 } from "../../../../schema/index.js";
+import { baseFileZodPropSchema } from "../../../defaultFileProps.js";
 
 export const FILE_ICON_SVG = ``;
 
 export const createFileNameWithIcon = (
-  block: BlockFromConfigNoChildren<
+  block: BlockFromConfig<
     BlockConfig<
       string,
-      {
-        name: { default: "" };
-      },
+      PropSchemaFromZod,
       "none"
     >,
     any,
diff --git a/packages/core/src/blocks/File/helpers/render/createResizableFileBlockWrapper.ts b/packages/core/src/blocks/File/helpers/render/createResizableFileBlockWrapper.ts
index 0ee0a18394..4a16a03280 100644
--- a/packages/core/src/blocks/File/helpers/render/createResizableFileBlockWrapper.ts
+++ b/packages/core/src/blocks/File/helpers/render/createResizableFileBlockWrapper.ts
@@ -1,23 +1,28 @@
 import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
 import {
   BlockConfig,
-  BlockFromConfigNoChildren,
+  BlockFromConfig,
+  PropSchemaFromZod,
 } from "../../../../schema/index.js";
+import {
+  baseFileZodPropSchema,
+  optionalFileZodPropSchema,
+} from "../../../defaultFileProps.js";
 import { createFileBlockWrapper } from "./createFileBlockWrapper.js";
 
+const requiredZodPropSchema = baseFileZodPropSchema.extend({
+  ...optionalFileZodPropSchema.pick({
+    url: true,
+    previewWidth: true,
+    showPreview: true,
+  }).shape,
+});
+
 export const createResizableFileBlockWrapper = (
-  block: BlockFromConfigNoChildren<
+  block: BlockFromConfig<
     BlockConfig<
       string,
-      {
-        backgroundColor: { default: "default" };
-        name: { default: "" };
-        url: { default: "" };
-        caption: { default: "" };
-        showPreview?: { default: true };
-        previewWidth?: { default: number };
-        textAlignment?: { default: "left" };
-      },
+      PropSchemaFromZod,
       "none"
     >,
     any,
@@ -69,7 +74,7 @@ export const createResizableFileBlockWrapper = (
         initialClientX: number;
       }
     | undefined;
-  let width = block.props.previewWidth! as number;
+  let width = block.props.previewWidth;
 
   // Updates the element width with an updated width depending on the cursor X
   // offset from when the resize began, and which resize handle is being used.
@@ -92,7 +97,7 @@ export const createResizableFileBlockWrapper = (
     const clientX =
       "touches" in event ? event.touches[0].clientX : event.clientX;
 
-    if (block.props.textAlignment === "center") {
+    if ((block.props as any).textAlignment === "center") {
       if (resizeParams.handleUsed === "left") {
         newWidth =
           resizeParams.initialWidth +
diff --git a/packages/core/src/blocks/Heading/block.ts b/packages/core/src/blocks/Heading/block.ts
index 7181298d1f..4880f9fea0 100644
--- a/packages/core/src/blocks/Heading/block.ts
+++ b/packages/core/src/blocks/Heading/block.ts
@@ -1,8 +1,13 @@
-import { createBlockConfig, createBlockSpec } from "../../schema/index.js";
+import { z } from "zod/v4";
 import { createBlockNoteExtension } from "../../editor/BlockNoteExtension.js";
+import {
+  createBlockConfig,
+  createBlockSpec,
+  createPropSchemaFromZod,
+} from "../../schema/index.js";
 import {
   addDefaultPropsExternalHTML,
-  defaultProps,
+  defaultZodPropSchema,
   parseDefaultProps,
 } from "../defaultProps.js";
 import { createToggleWrapper } from "../ToggleWrapper/createToggleWrapper.js";
@@ -26,13 +31,16 @@ export const createHeadingBlockConfig = createBlockConfig(
   }: HeadingOptions = {}) =>
     ({
       type: "heading" as const,
-      propSchema: {
-        ...defaultProps,
-        level: { default: defaultLevel, values: levels },
-        ...(allowToggleHeadings
-          ? { isToggleable: { default: false, optional: true } as const }
-          : {}),
-      },
+      propSchema: createPropSchemaFromZod(
+        defaultZodPropSchema.extend({
+          level: z
+            .union(levels.map((level) => z.literal(level)))
+            .default(defaultLevel),
+          ...(allowToggleHeadings
+            ? { isToggleable: z.boolean().default(false) }
+            : {}),
+        }),
+      ),
       content: "inline",
     }) as const,
 );
diff --git a/packages/core/src/blocks/Image/block.ts b/packages/core/src/blocks/Image/block.ts
index 83138c8842..c66968411d 100644
--- a/packages/core/src/blocks/Image/block.ts
+++ b/packages/core/src/blocks/Image/block.ts
@@ -3,8 +3,13 @@ import {
   BlockFromConfig,
   createBlockConfig,
   createBlockSpec,
+  createPropSchemaFromZod,
 } from "../../schema/index.js";
-import { defaultProps, parseDefaultProps } from "../defaultProps.js";
+import {
+  baseFileZodPropSchema,
+  optionalFileZodPropSchema,
+} from "../defaultFileProps.js";
+import { defaultZodPropSchema, parseDefaultProps } from "../defaultProps.js";
 import { parseFigureElement } from "../File/helpers/parse/parseFigureElement.js";
 import { createResizableFileBlockWrapper } from "../File/helpers/render/createResizableFileBlockWrapper.js";
 import { createFigureWithCaption } from "../File/helpers/toExternalHTML/createFigureWithCaption.js";
@@ -20,35 +25,25 @@ export interface ImageOptions {
 
 export type ImageBlockConfig = ReturnType;
 
+const imageZodPropSchema = defaultZodPropSchema
+  .pick({
+    textAlignment: true,
+    backgroundColor: true,
+  })
+  .extend({
+    ...baseFileZodPropSchema.shape,
+    ...optionalFileZodPropSchema.pick({
+      url: true,
+      showPreview: true,
+      previewWidth: true,
+    }).shape,
+  });
+
 export const createImageBlockConfig = createBlockConfig(
   (_ctx: ImageOptions = {}) =>
     ({
       type: "image" as const,
-      propSchema: {
-        textAlignment: defaultProps.textAlignment,
-        backgroundColor: defaultProps.backgroundColor,
-        // File name.
-        name: {
-          default: "" as const,
-        },
-        // File url.
-        url: {
-          default: "" as const,
-        },
-        // File caption.
-        caption: {
-          default: "" as const,
-        },
-
-        showPreview: {
-          default: true,
-        },
-        // File preview width in px.
-        previewWidth: {
-          default: undefined,
-          type: "number" as const,
-        },
-      },
+      propSchema: createPropSchemaFromZod(imageZodPropSchema),
       content: "none" as const,
     }) as const,
 );
diff --git a/packages/core/src/blocks/ListItem/BulletListItem/block.ts b/packages/core/src/blocks/ListItem/BulletListItem/block.ts
index 029538d4a3..b9262f8e83 100644
--- a/packages/core/src/blocks/ListItem/BulletListItem/block.ts
+++ b/packages/core/src/blocks/ListItem/BulletListItem/block.ts
@@ -3,7 +3,7 @@ import { createBlockNoteExtension } from "../../../editor/BlockNoteExtension.js"
 import { createBlockConfig, createBlockSpec } from "../../../schema/index.js";
 import {
   addDefaultPropsExternalHTML,
-  defaultProps,
+  defaultPropSchema,
   parseDefaultProps,
 } from "../../defaultProps.js";
 import { handleEnter } from "../../utils/listItemEnterHandler.js";
@@ -17,9 +17,7 @@ export const createBulletListItemBlockConfig = createBlockConfig(
   () =>
     ({
       type: "bulletListItem" as const,
-      propSchema: {
-        ...defaultProps,
-      },
+      propSchema: defaultPropSchema,
       content: "inline",
     }) as const,
 );
diff --git a/packages/core/src/blocks/ListItem/CheckListItem/block.ts b/packages/core/src/blocks/ListItem/CheckListItem/block.ts
index 7887144b1b..c1d71a0697 100644
--- a/packages/core/src/blocks/ListItem/CheckListItem/block.ts
+++ b/packages/core/src/blocks/ListItem/CheckListItem/block.ts
@@ -1,8 +1,13 @@
+import { z } from "zod/v4";
 import { createBlockNoteExtension } from "../../../editor/BlockNoteExtension.js";
-import { createBlockConfig, createBlockSpec } from "../../../schema/index.js";
+import {
+  createBlockConfig,
+  createBlockSpec,
+  createPropSchemaFromZod,
+} from "../../../schema/index.js";
 import {
   addDefaultPropsExternalHTML,
-  defaultProps,
+  defaultZodPropSchema,
   parseDefaultProps,
 } from "../../defaultProps.js";
 import { handleEnter } from "../../utils/listItemEnterHandler.js";
@@ -16,10 +21,11 @@ export const createCheckListItemConfig = createBlockConfig(
   () =>
     ({
       type: "checkListItem" as const,
-      propSchema: {
-        ...defaultProps,
-        checked: { default: false, type: "boolean" },
-      },
+      propSchema: createPropSchemaFromZod(
+        defaultZodPropSchema.extend({
+          checked: z.boolean().default(false),
+        }),
+      ),
       content: "inline",
     }) as const,
 );
diff --git a/packages/core/src/blocks/ListItem/NumberedListItem/block.ts b/packages/core/src/blocks/ListItem/NumberedListItem/block.ts
index 70f1bcaa90..bab950698f 100644
--- a/packages/core/src/blocks/ListItem/NumberedListItem/block.ts
+++ b/packages/core/src/blocks/ListItem/NumberedListItem/block.ts
@@ -1,9 +1,14 @@
+import { z } from "zod/v4";
 import { getBlockInfoFromSelection } from "../../../api/getBlockInfoFromPos.js";
 import { createBlockNoteExtension } from "../../../editor/BlockNoteExtension.js";
-import { createBlockConfig, createBlockSpec } from "../../../schema/index.js";
+import {
+  createBlockConfig,
+  createBlockSpec,
+  createPropSchemaFromZod,
+} from "../../../schema/index.js";
 import {
   addDefaultPropsExternalHTML,
-  defaultProps,
+  defaultZodPropSchema,
   parseDefaultProps,
 } from "../../defaultProps.js";
 import { handleEnter } from "../../utils/listItemEnterHandler.js";
@@ -18,10 +23,11 @@ export const createNumberedListItemBlockConfig = createBlockConfig(
   () =>
     ({
       type: "numberedListItem" as const,
-      propSchema: {
-        ...defaultProps,
-        start: { default: undefined, type: "number" } as const,
-      },
+      propSchema: createPropSchemaFromZod(
+        defaultZodPropSchema.extend({
+          start: z.number().optional(),
+        }),
+      ),
       content: "inline",
     }) as const,
 );
diff --git a/packages/core/src/blocks/ListItem/ToggleListItem/block.ts b/packages/core/src/blocks/ListItem/ToggleListItem/block.ts
index e70da1b4b9..3fd9c53d1b 100644
--- a/packages/core/src/blocks/ListItem/ToggleListItem/block.ts
+++ b/packages/core/src/blocks/ListItem/ToggleListItem/block.ts
@@ -2,7 +2,7 @@ import { createBlockNoteExtension } from "../../../editor/BlockNoteExtension.js"
 import { createBlockConfig, createBlockSpec } from "../../../schema/index.js";
 import {
   addDefaultPropsExternalHTML,
-  defaultProps,
+  defaultPropSchema,
 } from "../../defaultProps.js";
 import { createToggleWrapper } from "../../ToggleWrapper/createToggleWrapper.js";
 import { handleEnter } from "../../utils/listItemEnterHandler.js";
@@ -15,9 +15,7 @@ export const createToggleListItemBlockConfig = createBlockConfig(
   () =>
     ({
       type: "toggleListItem" as const,
-      propSchema: {
-        ...defaultProps,
-      },
+      propSchema: defaultPropSchema,
       content: "inline" as const,
     }) as const,
 );
diff --git a/packages/core/src/blocks/PageBreak/block.ts b/packages/core/src/blocks/PageBreak/block.ts
index 49d7d2bd94..fe4c676ae1 100644
--- a/packages/core/src/blocks/PageBreak/block.ts
+++ b/packages/core/src/blocks/PageBreak/block.ts
@@ -1,11 +1,13 @@
+import { z } from "zod/v4";
 import {
   BlockSchema,
   createBlockConfig,
   createBlockSpec,
+  createPropSchemaFromZod,
+  CustomBlockNoteSchema,
   InlineContentSchema,
   StyleSchema,
 } from "../../schema/index.js";
-import { BlockNoteSchema } from "../BlockNoteSchema.js";
 
 export type PageBreakBlockConfig = ReturnType<
   typeof createPageBreakBlockConfig
@@ -15,7 +17,7 @@ export const createPageBreakBlockConfig = createBlockConfig(
   () =>
     ({
       type: "pageBreak" as const,
-      propSchema: {},
+      propSchema: createPropSchemaFromZod(z.object({})),
       content: "none",
     }) as const,
 );
@@ -62,7 +64,7 @@ export const withPageBreak = <
   I extends InlineContentSchema,
   S extends StyleSchema,
 >(
-  schema: BlockNoteSchema,
+  schema: CustomBlockNoteSchema,
 ) => {
   return schema.extend({
     blockSpecs: {
diff --git a/packages/core/src/blocks/Paragraph/block.ts b/packages/core/src/blocks/Paragraph/block.ts
index 09a9cc9ddb..466b7bed9f 100644
--- a/packages/core/src/blocks/Paragraph/block.ts
+++ b/packages/core/src/blocks/Paragraph/block.ts
@@ -2,7 +2,7 @@ import { createBlockNoteExtension } from "../../editor/BlockNoteExtension.js";
 import { createBlockConfig, createBlockSpec } from "../../schema/index.js";
 import {
   addDefaultPropsExternalHTML,
-  defaultProps,
+  defaultPropSchema,
   parseDefaultProps,
 } from "../defaultProps.js";
 
@@ -14,13 +14,13 @@ export const createParagraphBlockConfig = createBlockConfig(
   () =>
     ({
       type: "paragraph" as const,
-      propSchema: defaultProps,
+      propSchema: defaultPropSchema,
       content: "inline" as const,
     }) as const,
 );
 
 export const createParagraphBlockSpec = createBlockSpec(
-  createParagraphBlockConfig,
+  createParagraphBlockConfig(),
   {
     meta: {
       isolating: false,
diff --git a/packages/core/src/blocks/Quote/block.ts b/packages/core/src/blocks/Quote/block.ts
index a0f6d6cb4a..03b67bd92e 100644
--- a/packages/core/src/blocks/Quote/block.ts
+++ b/packages/core/src/blocks/Quote/block.ts
@@ -1,8 +1,12 @@
 import { createBlockNoteExtension } from "../../editor/BlockNoteExtension.js";
-import { createBlockConfig, createBlockSpec } from "../../schema/index.js";
+import {
+  createBlockConfig,
+  createBlockSpec,
+  createPropSchemaFromZod,
+} from "../../schema/index.js";
 import {
   addDefaultPropsExternalHTML,
-  defaultProps,
+  defaultZodPropSchema,
   parseDefaultProps,
 } from "../defaultProps.js";
 
@@ -12,10 +16,12 @@ export const createQuoteBlockConfig = createBlockConfig(
   () =>
     ({
       type: "quote" as const,
-      propSchema: {
-        backgroundColor: defaultProps.backgroundColor,
-        textColor: defaultProps.textColor,
-      },
+      propSchema: createPropSchemaFromZod(
+        defaultZodPropSchema.pick({
+          backgroundColor: true,
+          textColor: true,
+        }),
+      ),
       content: "inline" as const,
     }) as const,
 );
diff --git a/packages/core/src/blocks/Table/block.ts b/packages/core/src/blocks/Table/block.ts
index d101c5144f..4e314a1aac 100644
--- a/packages/core/src/blocks/Table/block.ts
+++ b/packages/core/src/blocks/Table/block.ts
@@ -1,4 +1,4 @@
-import { Node, mergeAttributes } from "@tiptap/core";
+import { mergeAttributes, Node } from "@tiptap/core";
 import { DOMParser, Fragment, Node as PMNode, Schema } from "prosemirror-model";
 import { CellSelection, TableView } from "prosemirror-tables";
 import { NodeView } from "prosemirror-view";
@@ -6,16 +6,19 @@ import { createBlockNoteExtension } from "../../editor/BlockNoteExtension.js";
 import {
   BlockConfig,
   createBlockSpecFromTiptapNode,
+  createPropSchemaFromZod,
   TableContent,
 } from "../../schema/index.js";
 import { mergeCSSClasses } from "../../util/browser.js";
 import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js";
-import { defaultProps } from "../defaultProps.js";
+import { defaultZodPropSchema } from "../defaultProps.js";
 import { EMPTY_CELL_WIDTH, TableExtension } from "./TableExtension.js";
 
-export const tablePropSchema = {
-  textColor: defaultProps.textColor,
-};
+export const tableZodPropSchema = defaultZodPropSchema.pick({
+  textColor: true,
+});
+
+const tablePropSchema = createPropSchemaFromZod(tableZodPropSchema);
 
 const TiptapTableHeader = Node.create<{
   HTMLAttributes: Record;
@@ -372,11 +375,7 @@ function parseTableContent(node: HTMLElement, schema: Schema) {
 
 export type TableBlockConfig = BlockConfig<
   "table",
-  {
-    textColor: {
-      default: "default";
-    };
-  },
+  typeof tablePropSchema,
   "table"
 >;
 
diff --git a/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts b/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts
index 1c80d4346e..b0efc42889 100644
--- a/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts
+++ b/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts
@@ -28,7 +28,11 @@ export const createToggleWrapper = (
   ignoreMutation?: (mutation: ViewMutationRecord) => boolean;
   destroy?: () => void;
 } => {
-  if ("isToggleable" in block.props && !block.props.isToggleable) {
+  // TODO
+  if (
+    "isToggleable" in (block.props as any) &&
+    !(block.props as any).isToggleable
+  ) {
     return {
       dom: renderedElement,
     };
diff --git a/packages/core/src/blocks/Video/block.ts b/packages/core/src/blocks/Video/block.ts
index 026b333ba5..ae6f7164a1 100644
--- a/packages/core/src/blocks/Video/block.ts
+++ b/packages/core/src/blocks/Video/block.ts
@@ -1,5 +1,14 @@
-import { createBlockConfig, createBlockSpec } from "../../schema/index.js";
-import { defaultProps, parseDefaultProps } from "../defaultProps.js";
+import {
+  createBlockConfig,
+  createBlockSpec,
+  createPropSchemaFromZod,
+} from "../../schema/index.js";
+import {
+  baseFileZodPropSchema,
+  optionalFileZodPropSchema,
+} from "../defaultFileProps.js";
+
+import { defaultZodPropSchema, parseDefaultProps } from "../defaultProps.js";
 import { parseFigureElement } from "../File/helpers/parse/parseFigureElement.js";
 import { createResizableFileBlockWrapper } from "../File/helpers/render/createResizableFileBlockWrapper.js";
 import { createFigureWithCaption } from "../File/helpers/toExternalHTML/createFigureWithCaption.js";
@@ -15,18 +24,24 @@ export interface VideoOptions {
 
 export type VideoBlockConfig = ReturnType;
 
+const videoZodPropSchema = defaultZodPropSchema
+  .pick({
+    textAlignment: true,
+    backgroundColor: true,
+  })
+  .extend({
+    ...baseFileZodPropSchema.shape,
+    ...optionalFileZodPropSchema.pick({
+      url: true,
+      showPreview: true,
+      previewWidth: true,
+    }).shape,
+  });
+
 export const createVideoBlockConfig = createBlockConfig(
   (_ctx: VideoOptions) => ({
     type: "video" as const,
-    propSchema: {
-      textAlignment: defaultProps.textAlignment,
-      backgroundColor: defaultProps.backgroundColor,
-      name: { default: "" as const },
-      url: { default: "" as const },
-      caption: { default: "" as const },
-      showPreview: { default: true },
-      previewWidth: { default: undefined, type: "number" as const },
-    },
+    propSchema: createPropSchemaFromZod(videoZodPropSchema),
     content: "none" as const,
   }),
 );
@@ -92,7 +107,9 @@ export const createVideoBlockSpec = createBlockSpec(
       video.controls = true;
       video.contentEditable = "false";
       video.draggable = false;
-      video.width = block.props.previewWidth;
+      if (block.props.previewWidth) {
+        video.width = block.props.previewWidth;
+      }
       videoWrapper.appendChild(video);
 
       return createResizableFileBlockWrapper(
diff --git a/packages/core/src/blocks/defaultBlockHelpers.ts b/packages/core/src/blocks/defaultBlockHelpers.ts
index ccadf93e11..d4c5b0590f 100644
--- a/packages/core/src/blocks/defaultBlockHelpers.ts
+++ b/packages/core/src/blocks/defaultBlockHelpers.ts
@@ -1,12 +1,12 @@
 import { blockToNode } from "../api/nodeConversions/blockToNode.js";
 import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
 import type {
-  BlockNoDefaults,
   BlockSchema,
   InlineContentSchema,
   StyleSchema,
 } from "../schema/index.js";
 import { mergeCSSClasses } from "../util/browser.js";
+import type { Block } from "./index.js";
 
 // Function that creates a ProseMirror `DOMOutputSpec` for a default block.
 // Since all default blocks have the same structure (`blockContent` div with a
@@ -61,7 +61,7 @@ export const defaultBlockToHTML = <
   I extends InlineContentSchema,
   S extends StyleSchema,
 >(
-  block: BlockNoDefaults,
+  block: Block,
   editor: BlockNoteEditor,
 ): {
   dom: HTMLElement;
diff --git a/packages/core/src/blocks/defaultBlockTypeGuards.ts b/packages/core/src/blocks/defaultBlockTypeGuards.ts
index e9a4f8e9b5..c3ca03fdda 100644
--- a/packages/core/src/blocks/defaultBlockTypeGuards.ts
+++ b/packages/core/src/blocks/defaultBlockTypeGuards.ts
@@ -1,35 +1,20 @@
+import { Selection } from "prosemirror-state";
 import { CellSelection } from "prosemirror-tables";
 import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
-import { BlockConfig, PropSchema, PropSpec } from "../schema/index.js";
+import { BlockConfig, PropSchema } from "../schema/index.js";
 import { Block } from "./defaultBlocks.js";
-import { Selection } from "prosemirror-state";
 
+// TODO: matthew review + tests
 export function editorHasBlockWithType<
   BType extends string,
-  Props extends
-    | PropSchema
-    | Record
-    | undefined = undefined,
+  Props extends PropSchema,
 >(
   editor: BlockNoteEditor,
   blockType: BType,
   props?: Props,
 ): editor is BlockNoteEditor<
   {
-    [BT in BType]: Props extends PropSchema
-      ? BlockConfig
-      : Props extends Record
-        ? BlockConfig<
-            BT,
-            {
-              [PN in keyof Props]: {
-                default: undefined;
-                type: Props[PN];
-                values?: any[];
-              };
-            }
-          >
-        : BlockConfig;
+    [BT in BType]: BlockConfig;
   },
   any,
   any
@@ -42,115 +27,36 @@ export function editorHasBlockWithType<
     return true;
   }
 
-  for (const [propName, propSpec] of Object.entries(props)) {
-    if (!(propName in editor.schema.blockSpecs[blockType].config.propSchema)) {
-      return false;
-    }
-
-    if (typeof propSpec === "string") {
-      if (
-        editor.schema.blockSpecs[blockType].config.propSchema[propName]
-          .default !== undefined &&
-        typeof editor.schema.blockSpecs[blockType].config.propSchema[propName]
-          .default !== propSpec
-      ) {
-        return false;
-      }
-
-      if (
-        editor.schema.blockSpecs[blockType].config.propSchema[propName].type !==
-          undefined &&
-        editor.schema.blockSpecs[blockType].config.propSchema[propName].type !==
-          propSpec
-      ) {
-        return false;
-      }
-    } else {
-      if (
-        editor.schema.blockSpecs[blockType].config.propSchema[propName]
-          .default !== propSpec.default
-      ) {
-        return false;
-      }
-
-      if (
-        editor.schema.blockSpecs[blockType].config.propSchema[propName]
-          .default === undefined &&
-        propSpec.default === undefined
-      ) {
-        if (
-          editor.schema.blockSpecs[blockType].config.propSchema[propName]
-            .type !== propSpec.type
-        ) {
-          return false;
-        }
-      }
-
-      if (
-        typeof editor.schema.blockSpecs[blockType].config.propSchema[propName]
-          .values !== typeof propSpec.values
-      ) {
-        return false;
-      }
+  const editorProps: PropSchema =
+    editor.schema.blockSpecs[blockType].config.propSchema;
 
-      if (
-        typeof editor.schema.blockSpecs[blockType].config.propSchema[propName]
-          .values === "object" &&
-        typeof propSpec.values === "object"
-      ) {
-        for (const value of propSpec.values) {
-          if (
-            !editor.schema.blockSpecs[blockType].config.propSchema[
-              propName
-            ].values.includes(value)
-          ) {
-            return false;
-          }
-        }
-      }
-    }
-  }
-
-  return true;
+  // make sure every prop in the requested prop appears in the editor schema block props
+  return Object.entries(props._zodSource._zod.def.shape).every(
+    ([key, value]) => {
+      // we do a JSON Stringify check as Zod doesn't expose
+      // equality / assignability checks
+      return (
+        JSON.stringify(value._zod.def) ===
+        JSON.stringify(editorProps._zodSource._zod.def.shape[key]._zod.def)
+      );
+    },
+  );
 }
 
-export function blockHasType<
-  BType extends string,
-  Props extends
-    | PropSchema
-    | Record
-    | undefined = undefined,
->(
+export function blockHasType(
   block: Block,
   editor: BlockNoteEditor,
   blockType: BType,
   props?: Props,
 ): block is Block<
   {
-    [BT in BType]: Props extends PropSchema
-      ? BlockConfig
-      : Props extends Record
-        ? BlockConfig<
-            BT,
-            {
-              [PN in keyof Props]: PropSpec<
-                Props[PN] extends "boolean"
-                  ? boolean
-                  : Props[PN] extends "number"
-                    ? number
-                    : Props[PN] extends "string"
-                      ? string
-                      : never
-              >;
-            }
-          >
-        : BlockConfig;
+    [BT in BType]: BlockConfig;
   },
   any,
   any
 > {
   return (
-    editorHasBlockWithType(editor, blockType, props) && block.type === blockType
+    block.type === blockType && editorHasBlockWithType(editor, blockType, props)
   );
 }
 
diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts
index 459b987f00..5d7453ff55 100644
--- a/packages/core/src/blocks/defaultBlocks.ts
+++ b/packages/core/src/blocks/defaultBlocks.ts
@@ -1,3 +1,4 @@
+import type { Mark } from "@tiptap/core";
 import Bold from "@tiptap/extension-bold";
 import Code from "@tiptap/extension-code";
 import Italic from "@tiptap/extension-italic";
@@ -7,15 +8,15 @@ import { COLORS_DEFAULT } from "../editor/defaultColors.js";
 import {
   BlockNoDefaults,
   BlockSchema,
+  createStyleSpec,
+  createStyleSpecFromTipTapMark,
+  getInlineContentSchemaFromSpecs,
+  getStyleSchemaFromSpecs,
   InlineContentSchema,
   InlineContentSpecs,
   PartialBlockNoDefaults,
   StyleSchema,
   StyleSpecs,
-  createStyleSpec,
-  createStyleSpecFromTipTapMark,
-  getInlineContentSchemaFromSpecs,
-  getStyleSchemaFromSpecs,
 } from "../schema/index.js";
 import {
   createAudioBlockSpec,
@@ -31,11 +32,12 @@ import {
   createQuoteBlockSpec,
   createToggleListItemBlockSpec,
   createVideoBlockSpec,
-  defaultProps,
+  defaultZodPropSchema,
 } from "./index.js";
 import { createTableBlockSpec } from "./Table/block.js";
 
 export const defaultBlockSpecs = {
+  // To speed up TS compilation, we re-use the type assertions to avoid TS needing to compare types all the time
   audio: createAudioBlockSpec(),
   bulletListItem: createBulletListItemBlockSpec(),
   checkListItem: createCheckListItemBlockSpec(),
@@ -75,7 +77,10 @@ const TextColor = createStyleSpec(
     },
     toExternalHTML: (value) => {
       const span = document.createElement("span");
-      if (value !== defaultProps.textColor.default) {
+      // const defaultValue = defaultProps.parse({}).textColor;
+      const defaultValue =
+        defaultZodPropSchema.shape.textColor.def.defaultValue;
+      if (value !== defaultValue) {
         span.style.color =
           value in COLORS_DEFAULT ? COLORS_DEFAULT[value].text : value;
       }
@@ -111,7 +116,11 @@ const BackgroundColor = createStyleSpec(
     },
     toExternalHTML: (value) => {
       const span = document.createElement("span");
-      if (value !== defaultProps.backgroundColor.default) {
+      // TODO
+      // const defaultValues = defaultProps.parse({});
+      const defaultValue =
+        defaultZodPropSchema.shape.backgroundColor.def.defaultValue;
+      if (value !== defaultValue) {
         span.style.backgroundColor =
           value in COLORS_DEFAULT ? COLORS_DEFAULT[value].background : value;
       }
@@ -132,11 +141,26 @@ const BackgroundColor = createStyleSpec(
 );
 
 export const defaultStyleSpecs = {
-  bold: createStyleSpecFromTipTapMark(Bold, "boolean"),
-  italic: createStyleSpecFromTipTapMark(Italic, "boolean"),
-  underline: createStyleSpecFromTipTapMark(Underline, "boolean"),
-  strike: createStyleSpecFromTipTapMark(Strike, "boolean"),
-  code: createStyleSpecFromTipTapMark(Code, "boolean"),
+  bold: createStyleSpecFromTipTapMark(
+    Bold as Mark & { name: "bold" },
+    "boolean",
+  ),
+  italic: createStyleSpecFromTipTapMark(
+    Italic as Mark & { name: "italic" },
+    "boolean",
+  ),
+  underline: createStyleSpecFromTipTapMark(
+    Underline as Mark & { name: "underline" },
+    "boolean",
+  ),
+  strike: createStyleSpecFromTipTapMark(
+    Strike as Mark & { name: "strike" },
+    "boolean",
+  ),
+  code: createStyleSpecFromTipTapMark(
+    Code as Mark & { name: "code" },
+    "boolean",
+  ),
   textColor: TextColor,
   backgroundColor: BackgroundColor,
 } satisfies StyleSpecs;
diff --git a/packages/core/src/blocks/defaultFileProps.ts b/packages/core/src/blocks/defaultFileProps.ts
new file mode 100644
index 0000000000..596d6b5c51
--- /dev/null
+++ b/packages/core/src/blocks/defaultFileProps.ts
@@ -0,0 +1,18 @@
+import * as z from "zod/v4";
+
+export const baseFileZodPropSchema = z.object({
+  caption: z.string().default(""), // TODO: "" as defaults?
+  name: z.string().default(""),
+});
+
+export const optionalFileZodPropSchema = z.object({
+  // URL is optional, as we also want to accept files with no URL, but for example ids
+  // (ids can be used for files that are resolved on the backend)
+  url: z.string().default(""),
+  // Whether to show the file preview or the name only.
+  // This is useful for some file blocks, but not all
+  // (e.g.: not relevant for default "file" block which doesn;'t show previews)
+  showPreview: z.boolean().default(true),
+  // File preview width in px.
+  previewWidth: z.number().optional(),
+});
diff --git a/packages/core/src/blocks/defaultProps.ts b/packages/core/src/blocks/defaultProps.ts
index 5d55d21d35..384a5cc73d 100644
--- a/packages/core/src/blocks/defaultProps.ts
+++ b/packages/core/src/blocks/defaultProps.ts
@@ -1,28 +1,26 @@
 import { Attribute } from "@tiptap/core";
+import { z } from "zod/v4";
 
 import { COLORS_DEFAULT } from "../editor/defaultColors.js";
-import type { Props, PropSchema } from "../schema/index.js";
+import { createPropSchemaFromZod, type Props } from "../schema/index.js";
 
 // TODO: this system should probably be moved / refactored.
 // The dependency from schema on this file doesn't make sense
 
-export const defaultProps = {
-  backgroundColor: {
-    default: "default" as const,
-  },
-  textColor: {
-    default: "default" as const,
-  },
-  textAlignment: {
-    default: "left" as const,
-    values: ["left", "center", "right", "justify"] as const,
-  },
-} satisfies PropSchema;
+export const defaultZodPropSchema = z.object({
+  backgroundColor: z.string().default("default"),
+  textColor: z.string().default("default"),
+  textAlignment: z.enum(["left", "center", "right", "justify"]).default("left"),
+});
+
+export const defaultPropSchema = createPropSchemaFromZod(defaultZodPropSchema);
+export type DefaultPropSchema = Props;
 
-export type DefaultProps = Props;
+const defaultValues = defaultZodPropSchema.parse({});
 
+// TODO: review below
 export const parseDefaultProps = (element: HTMLElement) => {
-  const props: Partial = {};
+  const props: Partial = {};
 
   // If the `data-` attribute is found, set the prop to the value, as this most
   // likely means the parsed element was exported by BlockNote originally.
@@ -42,22 +40,26 @@ export const parseDefaultProps = (element: HTMLElement) => {
     props.textColor = element.style.color;
   }
 
-  props.textAlignment = defaultProps.textAlignment.values.includes(
-    element.style.textAlign as DefaultProps["textAlignment"],
-  )
-    ? (element.style.textAlign as DefaultProps["textAlignment"])
+  props.textAlignment = defaultZodPropSchema.shape.textAlignment._zod.values
+    .values()
+    .some(
+      (value) =>
+        value ===
+        (element.style.textAlign as DefaultPropSchema["textAlignment"]),
+    )
+    ? (element.style.textAlign as DefaultPropSchema["textAlignment"])
     : undefined;
 
   return props;
 };
 
 export const addDefaultPropsExternalHTML = (
-  props: Partial,
+  props: Partial,
   element: HTMLElement,
 ) => {
   if (
     props.backgroundColor &&
-    props.backgroundColor !== defaultProps.backgroundColor.default
+    props.backgroundColor !== defaultValues.backgroundColor
   ) {
     // The color can be any string. If the string matches one of the default
     // theme color names, set the theme color. Otherwise, set the color as-is
@@ -68,7 +70,7 @@ export const addDefaultPropsExternalHTML = (
         : props.backgroundColor;
   }
 
-  if (props.textColor && props.textColor !== defaultProps.textColor.default) {
+  if (props.textColor && props.textColor !== defaultValues.textColor) {
     // The color can be any string. If the string matches one of the default
     // theme color names, set the theme color. Otherwise, set the color as-is
     // (may be a CSS color name, hex value, RGB value, etc).
@@ -80,7 +82,7 @@ export const addDefaultPropsExternalHTML = (
 
   if (
     props.textAlignment &&
-    props.textAlignment !== defaultProps.textAlignment.default
+    props.textAlignment !== defaultValues.textAlignment
   ) {
     element.style.textAlign = props.textAlignment;
   }
@@ -89,7 +91,7 @@ export const addDefaultPropsExternalHTML = (
 export const getBackgroundColorAttribute = (
   attributeName = "backgroundColor",
 ): Attribute => ({
-  default: defaultProps.backgroundColor.default,
+  default: defaultValues.backgroundColor,
   parseHTML: (element) => {
     if (element.hasAttribute("data-background-color")) {
       return element.getAttribute("data-background-color")!;
@@ -99,10 +101,10 @@ export const getBackgroundColorAttribute = (
       return element.style.backgroundColor;
     }
 
-    return defaultProps.backgroundColor.default;
+    return defaultValues.backgroundColor;
   },
   renderHTML: (attributes) => {
-    if (attributes[attributeName] === defaultProps.backgroundColor.default) {
+    if (attributes[attributeName] === defaultValues.backgroundColor) {
       return {};
     }
 
@@ -115,7 +117,7 @@ export const getBackgroundColorAttribute = (
 export const getTextColorAttribute = (
   attributeName = "textColor",
 ): Attribute => ({
-  default: defaultProps.textColor.default,
+  default: defaultValues.textColor,
   parseHTML: (element) => {
     if (element.hasAttribute("data-text-color")) {
       return element.getAttribute("data-text-color")!;
@@ -125,10 +127,10 @@ export const getTextColorAttribute = (
       return element.style.color;
     }
 
-    return defaultProps.textColor.default;
+    return defaultValues.textColor;
   },
   renderHTML: (attributes) => {
-    if (attributes[attributeName] === defaultProps.textColor.default) {
+    if (attributes[attributeName] === defaultValues.textColor) {
       return {};
     }
 
@@ -141,7 +143,7 @@ export const getTextColorAttribute = (
 export const getTextAlignmentAttribute = (
   attributeName = "textAlignment",
 ): Attribute => ({
-  default: defaultProps.textAlignment.default,
+  default: defaultValues.textAlignment,
   parseHTML: (element) => {
     if (element.hasAttribute("data-text-alignment")) {
       return element.getAttribute("data-text-alignment");
@@ -151,10 +153,10 @@ export const getTextAlignmentAttribute = (
       return element.style.textAlign;
     }
 
-    return defaultProps.textAlignment.default;
+    return defaultValues.textAlignment;
   },
   renderHTML: (attributes) => {
-    if (attributes[attributeName] === defaultProps.textAlignment.default) {
+    if (attributes[attributeName] === defaultValues.textAlignment) {
       return {};
     }
 
diff --git a/packages/core/src/blocks/index.ts b/packages/core/src/blocks/index.ts
index 56f4c6de3c..92911fae07 100644
--- a/packages/core/src/blocks/index.ts
+++ b/packages/core/src/blocks/index.ts
@@ -15,13 +15,14 @@ export * from "./Quote/block.js";
 export * from "./Table/block.js";
 export * from "./Video/block.js";
 
-export { EMPTY_CELL_HEIGHT, EMPTY_CELL_WIDTH } from "./Table/TableExtension.js";
-export * from "./ToggleWrapper/createToggleWrapper.js";
 export * from "./File/helpers/uploadToTmpFilesDotOrg_DEV_ONLY.js";
 export * from "./PageBreak/getPageBreakSlashMenuItems.js";
+export { EMPTY_CELL_HEIGHT, EMPTY_CELL_WIDTH } from "./Table/TableExtension.js";
+export * from "./ToggleWrapper/createToggleWrapper.js";
 
 export * from "./BlockNoteSchema.js";
 export * from "./defaultBlockHelpers.js";
 export * from "./defaultBlocks.js";
 export * from "./defaultBlockTypeGuards.js";
+export * from "./defaultFileProps.js";
 export * from "./defaultProps.js";
diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts
index 9a5451763e..6c2880f7cd 100644
--- a/packages/core/src/editor/BlockNoteEditor.ts
+++ b/packages/core/src/editor/BlockNoteEditor.ts
@@ -37,18 +37,19 @@ import type { TableHandlesProsemirrorPlugin } from "../extensions/TableHandles/T
 import { UniqueID } from "../extensions/UniqueID/UniqueID.js";
 import type { Dictionary } from "../i18n/dictionary.js";
 import { en } from "../i18n/locales/index.js";
-import type {
-  BlockIdentifier,
-  BlockNoteDOMAttributes,
-  BlockSchema,
-  BlockSpecs,
-  CustomBlockNoteSchema,
-  InlineContentSchema,
-  InlineContentSpecs,
-  PartialInlineContent,
-  Styles,
-  StyleSchema,
-  StyleSpecs,
+import {
+  partialBlockToBlock,
+  type BlockIdentifier,
+  type BlockNoteDOMAttributes,
+  type BlockSchema,
+  type BlockSpecs,
+  type CustomBlockNoteSchema,
+  type InlineContentSchema,
+  type InlineContentSpecs,
+  type PartialInlineContent,
+  type Styles,
+  type StyleSchema,
+  type StyleSpecs,
 } from "../schema/index.js";
 import { mergeCSSClasses } from "../util/browser.js";
 import { EventEmitter } from "../util/EventEmitter.js";
@@ -59,13 +60,13 @@ import type { TextCursorPosition } from "./cursorPositionTypes.js";
 import {
   BlockManager,
   CollaborationManager,
-  type CollaborationOptions,
   EventManager,
   ExportManager,
   ExtensionManager,
   SelectionManager,
   StateManager,
   StyleManager,
+  type CollaborationOptions,
 } from "./managers/index.js";
 import type { Selection } from "./selectionTypes.js";
 import { transformPasted } from "./transformPasted.js";
@@ -156,7 +157,7 @@ export type BlockNoteEditorOptions<
    * @remarks `CommentsOptions`
    */
   comments?: {
-    schema?: BlockNoteSchema;
+    schema?: CustomBlockNoteSchema;
     threadStore: ThreadStore;
   };
 
@@ -447,7 +448,7 @@ export class BlockNoteEditor<
   /**
    * The schema of the editor. The schema defines which Blocks, InlineContent, and Styles are available in the editor.
    */
-  public readonly schema: BlockNoteSchema;
+  public readonly schema: CustomBlockNoteSchema;
 
   public readonly blockImplementations: BlockSpecs;
   public readonly inlineContentImplementations: InlineContentSpecs;
@@ -889,7 +890,11 @@ export class BlockNoteEditor<
       }
       const schema = getSchema(tiptapOptions.extensions!);
       const pmNodes = initialContent.map((b) =>
-        blockToNode(b, schema, this.schema.styleSchema).toJSON(),
+        blockToNode(
+          partialBlockToBlock(this.schema, b),
+          schema,
+          this.schema.styleSchema,
+        ).toJSON(),
       );
       const doc = createDocument(
         {
@@ -1499,7 +1504,7 @@ export class BlockNoteEditor<
    * @returns The blocks, serialized as an HTML string.
    */
   public blocksToHTMLLossy(
-    blocks: PartialBlock[] = this.document,
+    blocks: Block[] = this.document,
   ): string {
     return this._exportManager.blocksToHTMLLossy(blocks);
   }
@@ -1514,7 +1519,7 @@ export class BlockNoteEditor<
    * @returns The blocks, serialized as an HTML string.
    */
   public blocksToFullHTML(
-    blocks: PartialBlock[] = this.document,
+    blocks: Block[] = this.document,
   ): string {
     return this._exportManager.blocksToFullHTML(blocks);
   }
@@ -1538,7 +1543,7 @@ export class BlockNoteEditor<
    * @returns The blocks, serialized as a Markdown string.
    */
   public blocksToMarkdownLossy(
-    blocks: PartialBlock[] = this.document,
+    blocks: Block[] = this.document,
   ): string {
     return this._exportManager.blocksToMarkdownLossy(blocks);
   }
diff --git a/packages/core/src/editor/BlockNoteExtension.ts b/packages/core/src/editor/BlockNoteExtension.ts
index f56d6c736a..ecc84c38be 100644
--- a/packages/core/src/editor/BlockNoteExtension.ts
+++ b/packages/core/src/editor/BlockNoteExtension.ts
@@ -2,10 +2,10 @@ import { Plugin } from "prosemirror-state";
 import { EventEmitter } from "../util/EventEmitter.js";
 
 import { AnyExtension } from "@tiptap/core";
+import { PartialBlock } from "../blocks/index.js";
 import {
   BlockSchema,
   InlineContentSchema,
-  PartialBlockNoDefaults,
   StyleSchema,
 } from "../schema/index.js";
 import { BlockNoteEditor } from "./BlockNoteEditor.js";
@@ -93,7 +93,7 @@ export type InputRule = {
      * The editor instance
      */
     editor: BlockNoteEditor;
-  }) => undefined | PartialBlockNoDefaults;
+  }) => undefined | PartialBlock;
 };
 
 /**
diff --git a/packages/core/src/editor/BlockNoteExtensions.ts b/packages/core/src/editor/BlockNoteExtensions.ts
index 754c90b428..b85746a814 100644
--- a/packages/core/src/editor/BlockNoteExtensions.ts
+++ b/packages/core/src/editor/BlockNoteExtensions.ts
@@ -49,6 +49,7 @@ import {
   BlockNoteDOMAttributes,
   BlockSchema,
   BlockSpecs,
+  CustomBlockNoteSchema,
   InlineContentSchema,
   InlineContentSpecs,
   StyleSchema,
@@ -59,7 +60,6 @@ import type {
   BlockNoteEditorOptions,
   SupportedExtension,
 } from "./BlockNoteEditor.js";
-import { BlockNoteSchema } from "../blocks/BlockNoteSchema.js";
 
 type ExtensionOptions<
   BSchema extends BlockSchema,
@@ -94,7 +94,7 @@ type ExtensionOptions<
   >;
   tabBehavior?: "prefer-navigate-ui" | "prefer-indent";
   comments?: {
-    schema?: BlockNoteSchema;
+    schema?: CustomBlockNoteSchema;
     threadStore: ThreadStore;
     resolveUsers?: (userIds: string[]) => Promise;
   };
diff --git a/packages/core/src/editor/managers/CollaborationManager.ts b/packages/core/src/editor/managers/CollaborationManager.ts
index 8273fb5cb4..952701c82c 100644
--- a/packages/core/src/editor/managers/CollaborationManager.ts
+++ b/packages/core/src/editor/managers/CollaborationManager.ts
@@ -1,14 +1,14 @@
-import * as Y from "yjs";
 import { redoCommand, undoCommand } from "y-prosemirror";
-import { CommentsPlugin } from "../../extensions/Comments/CommentsPlugin.js";
-import { CommentMark } from "../../extensions/Comments/CommentMark.js";
+import * as Y from "yjs";
+import type { ThreadStore, User } from "../../comments/index.js";
+import { CursorPlugin } from "../../extensions/Collaboration/CursorPlugin.js";
 import { ForkYDocPlugin } from "../../extensions/Collaboration/ForkYDocPlugin.js";
 import { SyncPlugin } from "../../extensions/Collaboration/SyncPlugin.js";
 import { UndoPlugin } from "../../extensions/Collaboration/UndoPlugin.js";
-import { CursorPlugin } from "../../extensions/Collaboration/CursorPlugin.js";
-import type { ThreadStore, User } from "../../comments/index.js";
+import { CommentMark } from "../../extensions/Comments/CommentMark.js";
+import { CommentsPlugin } from "../../extensions/Comments/CommentsPlugin.js";
+import { CustomBlockNoteSchema } from "../../schema/CustomBlockNoteSchema.js";
 import type { BlockNoteEditor } from "../BlockNoteEditor.js";
-import { CustomBlockNoteSchema } from "../../schema/schema.js";
 
 export interface CollaborationOptions {
   /**
diff --git a/packages/core/src/editor/managers/ExportManager.ts b/packages/core/src/editor/managers/ExportManager.ts
index 3fe1ee2f0e..2f943005bd 100644
--- a/packages/core/src/editor/managers/ExportManager.ts
+++ b/packages/core/src/editor/managers/ExportManager.ts
@@ -11,7 +11,6 @@ import {
   DefaultBlockSchema,
   DefaultInlineContentSchema,
   DefaultStyleSchema,
-  PartialBlock,
 } from "../../blocks/defaultBlocks.js";
 import {
   BlockSchema,
@@ -35,7 +34,7 @@ export class ExportManager<
    * @returns The blocks, serialized as an HTML string.
    */
   public blocksToHTMLLossy(
-    blocks: PartialBlock[] = this.editor.document,
+    blocks: Block[] = this.editor.document,
   ): string {
     const exporter = createExternalHTMLExporter(
       this.editor.pmSchema,
@@ -54,7 +53,7 @@ export class ExportManager<
    * @returns The blocks, serialized as an HTML string.
    */
   public blocksToFullHTML(
-    blocks: PartialBlock[] = this.editor.document,
+    blocks: Block[] = this.editor.document,
   ): string {
     const exporter = createInternalHTMLSerializer(
       this.editor.pmSchema,
@@ -83,7 +82,7 @@ export class ExportManager<
    * @returns The blocks, serialized as a Markdown string.
    */
   public blocksToMarkdownLossy(
-    blocks: PartialBlock[] = this.editor.document,
+    blocks: Block[] = this.editor.document,
   ): string {
     return blocksToMarkdown(blocks, this.editor.pmSchema, this.editor, {});
   }
diff --git a/packages/core/src/editor/managers/StyleManager.ts b/packages/core/src/editor/managers/StyleManager.ts
index e03c46a6d1..bb487d0744 100644
--- a/packages/core/src/editor/managers/StyleManager.ts
+++ b/packages/core/src/editor/managers/StyleManager.ts
@@ -1,18 +1,19 @@
+import { TextSelection } from "@tiptap/pm/state";
 import { insertContentAt } from "../../api/blockManipulation/insertContentAt.js";
 import { inlineContentToNodes } from "../../api/nodeConversions/blockToNode.js";
+import {
+  DefaultBlockSchema,
+  DefaultInlineContentSchema,
+  DefaultStyleSchema,
+} from "../../blocks/defaultBlocks.js";
 import {
   BlockSchema,
   InlineContentSchema,
   PartialInlineContent,
   StyleSchema,
   Styles,
+  partialInlineContentToInlineContent,
 } from "../../schema/index.js";
-import {
-  DefaultBlockSchema,
-  DefaultInlineContentSchema,
-  DefaultStyleSchema,
-} from "../../blocks/defaultBlocks.js";
-import { TextSelection } from "@tiptap/pm/state";
 import { UnreachableCaseError } from "../../util/typescript.js";
 import { BlockNoteEditor } from "../BlockNoteEditor.js";
 
@@ -32,7 +33,11 @@ export class StyleManager<
     content: PartialInlineContent,
     { updateSelection = false }: { updateSelection?: boolean } = {},
   ) {
-    const nodes = inlineContentToNodes(content, this.editor.pmSchema);
+    const fullContent = partialInlineContentToInlineContent(
+      content,
+      this.editor.schema.inlineContentSchema,
+    );
+    const nodes = inlineContentToNodes(fullContent, this.editor.pmSchema);
 
     this.editor.transact((tr) => {
       insertContentAt(
diff --git a/packages/core/src/exporter/Exporter.ts b/packages/core/src/exporter/Exporter.ts
index f42e89e6f4..500d507ef9 100644
--- a/packages/core/src/exporter/Exporter.ts
+++ b/packages/core/src/exporter/Exporter.ts
@@ -1,8 +1,8 @@
-import { BlockNoteSchema } from "../blocks/BlockNoteSchema.js";
 import { COLORS_DEFAULT } from "../editor/defaultColors.js";
 import {
   BlockFromConfig,
   BlockSchema,
+  CustomBlockNoteSchema,
   InlineContent,
   InlineContentSchema,
   StyleSchema,
@@ -45,7 +45,7 @@ export abstract class Exporter<
   TS,
 > {
   public constructor(
-    _schema: BlockNoteSchema, // only used for type inference
+    _schema: CustomBlockNoteSchema, // only used for type inference
     protected readonly mappings: {
       blockMapping: BlockMapping;
       inlineContentMapping: InlineContentMapping;
diff --git a/packages/core/src/exporter/mapping.ts b/packages/core/src/exporter/mapping.ts
index 0dca63ebc3..1a39a0b302 100644
--- a/packages/core/src/exporter/mapping.ts
+++ b/packages/core/src/exporter/mapping.ts
@@ -1,7 +1,7 @@
-import { BlockNoteSchema } from "../blocks/BlockNoteSchema.js";
 import {
-  BlockFromConfigNoChildren,
+  BlockFromConfig,
   BlockSchema,
+  CustomBlockNoteSchema,
   InlineContentFromConfig,
   InlineContentSchema,
   StyleSchema,
@@ -20,7 +20,7 @@ export type BlockMapping<
   RI,
 > = {
   [K in keyof B]: (
-    block: BlockFromConfigNoChildren,
+    block: BlockFromConfig,
     // we don't know the exact types that are supported by the exporter at this point,
     // because the mapping only knows about converting certain types (which might be a subset of the supported types)
     // this is why there are many `any` types here (same for types below)
@@ -64,7 +64,7 @@ export function mappingFactory<
   B extends BlockSchema,
   I extends InlineContentSchema,
   S extends StyleSchema,
->(_schema: BlockNoteSchema) {
+>(_schema: CustomBlockNoteSchema) {
   return {
     createBlockMapping: (mapping: BlockMapping) =>
       mapping,
diff --git a/packages/core/src/extensions/Collaboration/schemaMigration/migrationRules/moveColorAttributes.ts b/packages/core/src/extensions/Collaboration/schemaMigration/migrationRules/moveColorAttributes.ts
index 69dc3b5964..54f31d67e1 100644
--- a/packages/core/src/extensions/Collaboration/schemaMigration/migrationRules/moveColorAttributes.ts
+++ b/packages/core/src/extensions/Collaboration/schemaMigration/migrationRules/moveColorAttributes.ts
@@ -1,7 +1,7 @@
 import * as Y from "yjs";
 
+import { defaultZodPropSchema } from "../../../../blocks/defaultProps.js";
 import { MigrationRule } from "./migrationRule.js";
-import { defaultProps } from "../../../../blocks/defaultProps.js";
 
 // Helper function to recursively traverse a `Y.XMLElement` and its descendant
 // elements.
@@ -45,10 +45,12 @@ export const moveColorAttributes: MigrationRule = (fragment, tr) => {
             backgroundColor: element.getAttribute("backgroundColor"),
           };
 
-          if (colors.textColor === defaultProps.textColor.default) {
+          // TODO: TBD best way to extract defaults
+          const defaultValues = defaultZodPropSchema.parse({});
+          if (colors.textColor === defaultValues.textColor) {
             colors.textColor = undefined;
           }
-          if (colors.backgroundColor === defaultProps.backgroundColor.default) {
+          if (colors.backgroundColor === defaultValues.backgroundColor) {
             colors.backgroundColor = undefined;
           }
 
diff --git a/packages/core/src/extensions/Comments/CommentsPlugin.ts b/packages/core/src/extensions/Comments/CommentsPlugin.ts
index a5d1e24b6d..635bfacd2d 100644
--- a/packages/core/src/extensions/Comments/CommentsPlugin.ts
+++ b/packages/core/src/extensions/Comments/CommentsPlugin.ts
@@ -10,7 +10,7 @@ import type {
 } from "../../comments/index.js";
 import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
 import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
-import { CustomBlockNoteSchema } from "../../schema/schema.js";
+import { CustomBlockNoteSchema } from "../../schema/CustomBlockNoteSchema.js";
 import { UserStore } from "./userstore/UserStore.js";
 
 const PLUGIN_KEY = new PluginKey(`blocknote-comments`);
diff --git a/packages/core/src/extensions/SideMenu/dragging.ts b/packages/core/src/extensions/SideMenu/dragging.ts
index 54dc3eeae2..d3a5779c7f 100644
--- a/packages/core/src/extensions/SideMenu/dragging.ts
+++ b/packages/core/src/extensions/SideMenu/dragging.ts
@@ -201,7 +201,7 @@ export function dragStart<
 
     const externalHTMLExporter = createExternalHTMLExporter(schema, editor);
 
-    const blocks = fragmentToBlocks(selectedSlice.content);
+    const blocks = fragmentToBlocks(selectedSlice.content);
     const externalHTML = externalHTMLExporter.exportBlocks(blocks, {});
 
     const plainText = cleanHTMLToMarkdown(externalHTML);
diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts
index 921d181d1a..df955b55e7 100644
--- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts
+++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts
@@ -1,11 +1,14 @@
 import { Block, PartialBlock } from "../../blocks/defaultBlocks.js";
 import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
 
+import { z } from "zod/v4";
 import { editorHasBlockWithType } from "../../blocks/defaultBlockTypeGuards.js";
+import { optionalFileZodPropSchema } from "../../blocks/defaultFileProps.js";
 import {
   BlockSchema,
   InlineContentSchema,
   StyleSchema,
+  createPropSchemaFromZod,
   isStyledTextInlineContent,
 } from "../../schema/index.js";
 import { formatKeyboardShortcut } from "../../util/browser.js";
@@ -87,44 +90,30 @@ export function getDefaultSlashMenuItems<
 >(editor: BlockNoteEditor) {
   const items: DefaultSuggestionItem[] = [];
 
-  if (editorHasBlockWithType(editor, "heading", { level: "number" })) {
-    items.push(
-      {
-        onItemClick: () => {
-          insertOrUpdateBlock(editor, {
-            type: "heading",
-            props: { level: 1 },
-          });
-        },
-        badge: formatKeyboardShortcut("Mod-Alt-1"),
-        key: "heading",
-        ...editor.dictionary.slash_menu.heading,
-      },
-      {
-        onItemClick: () => {
-          insertOrUpdateBlock(editor, {
-            type: "heading",
-            props: { level: 2 },
-          });
-        },
-        badge: formatKeyboardShortcut("Mod-Alt-2"),
-        key: "heading_2",
-        ...editor.dictionary.slash_menu.heading_2,
-      },
-      {
-        onItemClick: () => {
-          insertOrUpdateBlock(editor, {
-            type: "heading",
-            props: { level: 3 },
-          });
-        },
-        badge: formatKeyboardShortcut("Mod-Alt-3"),
-        key: "heading_3",
-        ...editor.dictionary.slash_menu.heading_3,
-      },
-    );
+  if (
+    editorHasBlockWithType(
+      editor,
+      "heading",
+      createPropSchemaFromZod(z.object({ level: z.number() })),
+    )
+  ) {
+    const headingProps = editor.schema.blockSchema.heading.propSchema;
+    for (const level of [1, 2, 3, 4, 5, 6] as const) {
+      if (z.safeParse(headingProps._zodSource, { level }).success) {
+        items.push({
+          onItemClick: () => {
+            insertOrUpdateBlock(editor, {
+              type: "heading",
+              props: { level },
+            });
+          },
+          badge: formatKeyboardShortcut("Mod-Alt-1"),
+          key: `heading_${level}`,
+          ...editor.dictionary.slash_menu[`heading_${level}`],
+        });
+      }
+    }
   }
-
   if (editorHasBlockWithType(editor, "quote")) {
     items.push({
       onItemClick: () => {
@@ -249,7 +238,14 @@ export function getDefaultSlashMenuItems<
     });
   }
 
-  if (editorHasBlockWithType(editor, "image", { url: "string" })) {
+  if (
+    editorHasBlockWithType(
+      editor,
+      "image",
+      // TODO: review
+      createPropSchemaFromZod(optionalFileZodPropSchema.pick({ url: true })),
+    )
+  ) {
     items.push({
       onItemClick: () => {
         const insertedBlock = insertOrUpdateBlock(editor, {
@@ -268,7 +264,13 @@ export function getDefaultSlashMenuItems<
     });
   }
 
-  if (editorHasBlockWithType(editor, "video", { url: "string" })) {
+  if (
+    editorHasBlockWithType(
+      editor,
+      "video",
+      createPropSchemaFromZod(optionalFileZodPropSchema.pick({ url: true })),
+    )
+  ) {
     items.push({
       onItemClick: () => {
         const insertedBlock = insertOrUpdateBlock(editor, {
@@ -287,7 +289,13 @@ export function getDefaultSlashMenuItems<
     });
   }
 
-  if (editorHasBlockWithType(editor, "audio", { url: "string" })) {
+  if (
+    editorHasBlockWithType(
+      editor,
+      "audio",
+      createPropSchemaFromZod(optionalFileZodPropSchema.pick({ url: true })),
+    )
+  ) {
     items.push({
       onItemClick: () => {
         const insertedBlock = insertOrUpdateBlock(editor, {
@@ -306,7 +314,13 @@ export function getDefaultSlashMenuItems<
     });
   }
 
-  if (editorHasBlockWithType(editor, "file", { url: "string" })) {
+  if (
+    editorHasBlockWithType(
+      editor,
+      "file",
+      createPropSchemaFromZod(optionalFileZodPropSchema.pick({ url: true })),
+    )
+  ) {
     items.push({
       onItemClick: () => {
         const insertedBlock = insertOrUpdateBlock(editor, {
@@ -326,23 +340,32 @@ export function getDefaultSlashMenuItems<
   }
 
   if (
-    editorHasBlockWithType(editor, "heading", {
-      level: "number",
-      isToggleable: "boolean",
-    })
+    editorHasBlockWithType(
+      editor,
+      "heading",
+      createPropSchemaFromZod(
+        z.object({
+          isToggleable: z.boolean().default(false),
+          level: z.number(), // TODO
+        }),
+      ),
+    )
   ) {
-    items.push(
-      {
+    const headingProps = editor.schema.blockSchema.heading.propSchema;
+    if (z.safeParse(headingProps._zodSource, { level: 1 }).success) {
+      items.push({
         onItemClick: () => {
           insertOrUpdateBlock(editor, {
             type: "heading",
             props: { level: 1, isToggleable: true },
           });
         },
-        key: "toggle_heading",
-        ...editor.dictionary.slash_menu.toggle_heading,
-      },
-      {
+        key: "toggle_heading_1",
+        ...editor.dictionary.slash_menu.toggle_heading_1,
+      });
+    }
+    if (z.safeParse(headingProps._zodSource, { level: 2 }).success) {
+      items.push({
         onItemClick: () => {
           insertOrUpdateBlock(editor, {
             type: "heading",
@@ -352,8 +375,10 @@ export function getDefaultSlashMenuItems<
 
         key: "toggle_heading_2",
         ...editor.dictionary.slash_menu.toggle_heading_2,
-      },
-      {
+      });
+    }
+    if (z.safeParse(headingProps._zodSource, { level: 3 }).success) {
+      items.push({
         onItemClick: () => {
           insertOrUpdateBlock(editor, {
             type: "heading",
@@ -362,25 +387,8 @@ export function getDefaultSlashMenuItems<
         },
         key: "toggle_heading_3",
         ...editor.dictionary.slash_menu.toggle_heading_3,
-      },
-    );
-  }
-
-  if (editorHasBlockWithType(editor, "heading", { level: "number" })) {
-    (editor.schema.blockSchema.heading.propSchema.level.values || [])
-      .filter((level): level is 4 | 5 | 6 => level > 3)
-      .forEach((level) => {
-        items.push({
-          onItemClick: () => {
-            insertOrUpdateBlock(editor, {
-              type: "heading",
-              props: { level: level },
-            });
-          },
-          key: `heading_${level}`,
-          ...editor.dictionary.slash_menu[`heading_${level}`],
-        });
       });
+    }
   }
 
   items.push({
diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts
index f90fe9e95b..589eefcead 100644
--- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts
+++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts
@@ -27,14 +27,18 @@ import {
 import { nodeToBlock } from "../../api/nodeConversions/nodeToBlock.js";
 import { getNodeById } from "../../api/nodeUtil.js";
 import {
-  editorHasBlockWithType,
+  blockHasType,
   isTableCellSelection,
 } from "../../blocks/defaultBlockTypeGuards.js";
-import { DefaultBlockSchema } from "../../blocks/defaultBlocks.js";
+import {
+  DefaultBlockSchema,
+  defaultBlockSpecs,
+} from "../../blocks/defaultBlocks.js";
+import { TableBlockConfig } from "../../blocks/index.js";
 import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
 import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
 import {
-  BlockFromConfigNoChildren,
+  BlockFromConfig,
   BlockSchemaWithBlock,
   InlineContentSchema,
   StyleSchema,
@@ -54,7 +58,7 @@ export type TableHandlesState<
   referencePosCell: DOMRect | undefined;
   referencePosTable: DOMRect;
 
-  block: BlockFromConfigNoChildren;
+  block: BlockFromConfig;
   colIndex: number | undefined;
   rowIndex: number | undefined;
 
@@ -164,7 +168,7 @@ export class TableHandlesView<
 
   constructor(
     private readonly editor: BlockNoteEditor<
-      BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>,
+      BlockSchemaWithBlock<"table", TableBlockConfig>,
       I,
       S
     >,
@@ -260,7 +264,7 @@ export class TableHandlesView<
     this.tableElement = blockEl.node;
 
     let tableBlock:
-      | BlockFromConfigNoChildren
+      | BlockFromConfig
       | undefined;
 
     const pmNodeInfo = this.editor.transact((tr) =>
@@ -278,7 +282,7 @@ export class TableHandlesView<
       this.editor.schema.styleSchema,
     );
 
-    if (editorHasBlockWithType(this.editor, "table")) {
+    if (blockHasType(block, this.editor, "table")) {
       this.tablePos = pmNodeInfo.posBeforeNode + 1;
       tableBlock = block;
     }
@@ -535,10 +539,16 @@ export class TableHandlesView<
     }
 
     // Hide handles if the table block has been removed.
-    this.state.block = this.editor.getBlock(this.state.block.id)!;
+    const block = this.editor.getBlock(this.state.block.id);
+
     if (
-      !this.state.block ||
-      this.state.block.type !== "table" ||
+      !block ||
+      !blockHasType(
+        block,
+        this.editor,
+        "table",
+        defaultBlockSpecs.table.config.propSchema,
+      ) ||
       // when collaborating, the table element might be replaced and out of date
       // because yjs replaces the element when for example you change the color via the side menu
       !this.tableElement?.isConnected
@@ -551,6 +561,7 @@ export class TableHandlesView<
       return;
     }
 
+    this.state.block = block;
     const { height: rowCount, width: colCount } = getDimensionsOfTable(
       this.state.block,
     );
@@ -626,7 +637,7 @@ export class TableHandlesProsemirrorPlugin<
 
   constructor(
     private readonly editor: BlockNoteEditor<
-      BlockSchemaWithBlock<"table", DefaultBlockSchema["table"]>,
+      BlockSchemaWithBlock<"table", TableBlockConfig>,
       I,
       S
     >,
@@ -921,7 +932,7 @@ export class TableHandlesProsemirrorPlugin<
   };
 
   getCellsAtRowHandle = (
-    block: BlockFromConfigNoChildren,
+    block: BlockFromConfig,
     relativeRowIndex: RelativeCellIndices["row"],
   ) => {
     return getCellsAtRowHandle(block, relativeRowIndex);
@@ -931,7 +942,7 @@ export class TableHandlesProsemirrorPlugin<
    * Get all the cells in a column of the table block.
    */
   getCellsAtColumnHandle = (
-    block: BlockFromConfigNoChildren,
+    block: BlockFromConfig,
     relativeColumnIndex: RelativeCellIndices["col"],
   ) => {
     return getCellsAtColumnHandle(block, relativeColumnIndex);
@@ -1161,9 +1172,7 @@ export class TableHandlesProsemirrorPlugin<
    * Returns undefined when there is no cell selection, or the selection is not within a table.
    */
   getMergeDirection = (
-    block:
-      | BlockFromConfigNoChildren
-      | undefined,
+    block: BlockFromConfig | undefined,
   ) => {
     return this.editor.transact((tr) => {
       const isSelectingTableCells = isTableCellSelection(tr.selection)
@@ -1194,14 +1203,14 @@ export class TableHandlesProsemirrorPlugin<
   };
 
   cropEmptyRowsOrColumns = (
-    block: BlockFromConfigNoChildren,
+    block: BlockFromConfig,
     removeEmpty: "columns" | "rows",
   ) => {
     return cropEmptyRowsOrColumns(block, removeEmpty);
   };
 
   addRowsOrColumns = (
-    block: BlockFromConfigNoChildren,
+    block: BlockFromConfig,
     addType: "columns" | "rows",
     numToAdd: number,
   ) => {
diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts
index 00574debed..8cee70bff4 100644
--- a/packages/core/src/i18n/locales/ar.ts
+++ b/packages/core/src/i18n/locales/ar.ts
@@ -2,7 +2,7 @@ import type { Dictionary } from "../dictionary.js";
 
 export const ar: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "عنوان 1",
       subtext: "يستخدم لعناوين المستوى الأعلى",
       aliases: ["ع", "عنوان1", "ع1"],
@@ -38,7 +38,7 @@ export const ar: Dictionary = {
       aliases: ["ع6", "عنوان6", "العنوان الفرعي الأدنى"],
       group: "العناوين الفرعية",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "عنوان قابل للطي 1",
       subtext: "عنوان قابل للطي لإظهار وإخفاء المحتوى",
       aliases: ["ع", "عنوان1", "ع1", "قابل للطي", "طي"],
diff --git a/packages/core/src/i18n/locales/de.ts b/packages/core/src/i18n/locales/de.ts
index cac48dd80a..2aaec1c039 100644
--- a/packages/core/src/i18n/locales/de.ts
+++ b/packages/core/src/i18n/locales/de.ts
@@ -2,7 +2,7 @@ import { Dictionary } from "../dictionary.js";
 
 export const de: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Überschrift 1",
       subtext: "Hauptebene Überschrift",
       aliases: ["h", "überschrift1", "h1"],
@@ -38,7 +38,7 @@ export const de: Dictionary = {
       aliases: ["h6", "überschrift6", "unterüberschrift6"],
       group: "Unterüberschriften",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Aufklappbare Überschrift 1",
       subtext: "Aufklappbare Hauptebene Überschrift",
       aliases: ["h", "überschrift1", "h1", "aufklappbar", "einklappbar"],
diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts
index af7927d207..d888608b2f 100644
--- a/packages/core/src/i18n/locales/en.ts
+++ b/packages/core/src/i18n/locales/en.ts
@@ -1,6 +1,6 @@
 export const en = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Heading 1",
       subtext: "Top-level heading",
       aliases: ["h", "heading1", "h1"],
@@ -36,7 +36,7 @@ export const en = {
       aliases: ["h6", "heading6", "subheading6"],
       group: "Subheadings",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Toggle Heading 1",
       subtext: "Toggleable top-level heading",
       aliases: ["h", "heading1", "h1", "collapsable"],
diff --git a/packages/core/src/i18n/locales/es.ts b/packages/core/src/i18n/locales/es.ts
index 9e830b406b..efc2f57aa6 100644
--- a/packages/core/src/i18n/locales/es.ts
+++ b/packages/core/src/i18n/locales/es.ts
@@ -2,7 +2,7 @@ import { Dictionary } from "../dictionary.js";
 
 export const es: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Encabezado 1",
       subtext: "Encabezado de primer nivel",
       aliases: ["h", "encabezado1", "h1"],
@@ -38,7 +38,7 @@ export const es: Dictionary = {
       aliases: ["h6", "encabezado6", "subencabezado6"],
       group: "Subencabezados",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Encabezado Plegable 1",
       subtext: "Encabezado de primer nivel que se puede plegar",
       aliases: ["h", "encabezado1", "h1", "plegable", "contraible"],
diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts
index b56e6942f6..bba08cb2a5 100644
--- a/packages/core/src/i18n/locales/fr.ts
+++ b/packages/core/src/i18n/locales/fr.ts
@@ -2,7 +2,7 @@ import { Dictionary } from "../dictionary.js";
 
 export const fr: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Titre 1",
       subtext: "Utilisé pour un titre de premier niveau",
       aliases: ["h", "titre1", "h1"],
@@ -39,7 +39,7 @@ export const fr: Dictionary = {
       aliases: ["h6", "titre6", "sous-titre6"],
       group: "Sous-titres",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Titre Repliable 1",
       subtext:
         "Titre de premier niveau qui peut être replié pour masquer son contenu",
diff --git a/packages/core/src/i18n/locales/he.ts b/packages/core/src/i18n/locales/he.ts
index 553fc42941..f5cea6168d 100644
--- a/packages/core/src/i18n/locales/he.ts
+++ b/packages/core/src/i18n/locales/he.ts
@@ -2,7 +2,7 @@ import { Dictionary } from "../dictionary.js";
 
 export const he: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "כותרת 1",
       subtext: "כותרת ראשית",
       aliases: ["h", "heading1", "h1"],
@@ -20,7 +20,7 @@ export const he: Dictionary = {
       aliases: ["h3", "heading3", "subheading"],
       group: "כותרות",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "כותרת מתקפלת 1",
       subtext: "כותרת ראשית מתקפלת",
       aliases: ["h", "heading1", "h1", "collapsable"],
diff --git a/packages/core/src/i18n/locales/hr.ts b/packages/core/src/i18n/locales/hr.ts
index 31c0b71159..ea0895903a 100644
--- a/packages/core/src/i18n/locales/hr.ts
+++ b/packages/core/src/i18n/locales/hr.ts
@@ -2,7 +2,7 @@ import { Dictionary } from "../dictionary.js";
 
 export const hr: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Naslov 1",
       subtext: "Glavni naslov",
       aliases: ["h", "naslov1", "h1"],
@@ -38,7 +38,7 @@ export const hr: Dictionary = {
       aliases: ["h6", "naslov6", "podnaslov6"],
       group: "Podnaslovi",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Proširivi Naslov 1",
       subtext: "Proširivi glavni naslov",
       aliases: ["h", "naslov1", "h1", "proširivi"],
diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts
index 25060d651f..d1599e5f6b 100644
--- a/packages/core/src/i18n/locales/is.ts
+++ b/packages/core/src/i18n/locales/is.ts
@@ -2,7 +2,7 @@ import type { Dictionary } from "../dictionary.js";
 
 export const is: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Fyrirsögn 1",
       subtext: "Notað fyrir efstu fyrirsögn",
       aliases: ["h", "fyrirsogn1", "h1"],
@@ -38,7 +38,7 @@ export const is: Dictionary = {
       aliases: ["h6", "fyrirsogn6", "undirfyrirsogn6"],
       group: "Undirfyrirsagnir",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Fellanleg Fyrirsögn 1",
       subtext:
         "Fellanleg efsta fyrirsögn sem hægt er að sýna eða fela innihald",
diff --git a/packages/core/src/i18n/locales/it.ts b/packages/core/src/i18n/locales/it.ts
index 45d9dcd277..5e9fc75290 100644
--- a/packages/core/src/i18n/locales/it.ts
+++ b/packages/core/src/i18n/locales/it.ts
@@ -2,7 +2,7 @@ import { Dictionary } from "../dictionary.js";
 
 export const it: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Intestazione 1",
       subtext: "Intestazione di primo livello",
       aliases: ["h", "intestazione1", "h1"],
@@ -38,7 +38,7 @@ export const it: Dictionary = {
       aliases: ["h6", "intestazione6", "sottotitolo6"],
       group: "Sottotitoli",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Intestazione Espandibile 1",
       subtext:
         "Intestazione di primo livello che può essere espansa o compressa per mostrare il contenuto",
diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts
index 236b834443..fa23229b75 100644
--- a/packages/core/src/i18n/locales/ja.ts
+++ b/packages/core/src/i18n/locales/ja.ts
@@ -2,7 +2,7 @@ import { Dictionary } from "../dictionary.js";
 
 export const ja: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "見出し1",
       subtext: "トップレベルの見出しに使用",
       aliases: ["h", "見出し1", "h1", "大見出し"],
@@ -38,7 +38,7 @@ export const ja: Dictionary = {
       aliases: ["h6", "見出し6", "subheading6", "小見出し6"],
       group: "サブ見出し",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "折りたたみ見出し1",
       subtext: "内容の表示/非表示が切り替え可能なトップレベルの見出し",
       aliases: ["h", "見出し1", "h1", "大見出し", "折りたたみ", "トグル"],
diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts
index cce4c8f7c6..33eb5e1ce7 100644
--- a/packages/core/src/i18n/locales/ko.ts
+++ b/packages/core/src/i18n/locales/ko.ts
@@ -2,7 +2,7 @@ import { Dictionary } from "../dictionary.js";
 
 export const ko: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "제목1",
       subtext: "섹션 제목(대)",
       aliases: ["h", "제목1", "h1", "대제목"],
@@ -38,7 +38,7 @@ export const ko: Dictionary = {
       aliases: ["h6", "제목6", "소제목6"],
       group: "소제목",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "접을 수 있는 제목1",
       subtext: "내용을 표시하거나 숨길 수 있는 섹션 제목(대)",
       aliases: ["h", "제목1", "h1", "대제목", "접기", "토글"],
diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts
index 6d1f48cde2..93776f2f4a 100644
--- a/packages/core/src/i18n/locales/nl.ts
+++ b/packages/core/src/i18n/locales/nl.ts
@@ -2,7 +2,7 @@ import type { Dictionary } from "../dictionary.js";
 
 export const nl: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Kop 1",
       subtext: "Gebruikt voor een hoofdkop",
       aliases: ["h", "kop1", "h1"],
@@ -38,7 +38,7 @@ export const nl: Dictionary = {
       aliases: ["h6", "kop6", "subkop6"],
       group: "Subkoppen",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Uitklapbare Kop 1",
       subtext: "Hoofdkop die kan worden uit- en ingeklapt om inhoud te tonen",
       aliases: ["h", "kop1", "h1", "uitklapbaar", "inklapbaar"],
diff --git a/packages/core/src/i18n/locales/no.ts b/packages/core/src/i18n/locales/no.ts
index c28cac2b9f..6bfbec346e 100644
--- a/packages/core/src/i18n/locales/no.ts
+++ b/packages/core/src/i18n/locales/no.ts
@@ -2,7 +2,7 @@ import { Dictionary } from "../dictionary.js";
 
 export const no: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Overskrift 1",
       subtext: "Toppnivåoverskrift",
       aliases: ["h", "overskrift1", "h1"],
@@ -38,7 +38,7 @@ export const no: Dictionary = {
       aliases: ["h6", "overskrift6", "underoverskrift6"],
       group: "Underoverskrifter",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Sammenleggbar Overskrift 1",
       subtext: "Toppnivåoverskrift som kan vises eller skjules",
       aliases: ["h", "overskrift1", "h1", "sammenleggbar", "toggle"],
diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts
index 35bf1f255a..47de5ccd0d 100644
--- a/packages/core/src/i18n/locales/pl.ts
+++ b/packages/core/src/i18n/locales/pl.ts
@@ -2,7 +2,7 @@ import type { Dictionary } from "../dictionary.js";
 
 export const pl: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Nagłówek 1",
       subtext: "Używany dla nagłówka najwyższego poziomu",
       aliases: ["h", "naglowek1", "h1"],
@@ -38,7 +38,7 @@ export const pl: Dictionary = {
       aliases: ["h6", "naglowek6", "podnaglowek6"],
       group: "Podnagłówki",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Nagłówek rozwijany 1",
       subtext: "Rozwijany nagłówek najwyższego poziomu",
       aliases: ["h", "naglowek1", "h1", "rozwijany"],
diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts
index 7801cd2d36..9a22161de5 100644
--- a/packages/core/src/i18n/locales/pt.ts
+++ b/packages/core/src/i18n/locales/pt.ts
@@ -2,7 +2,7 @@ import type { Dictionary } from "../dictionary.js";
 
 export const pt: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Título",
       subtext: "Usado para um título de nível superior",
       aliases: ["h", "titulo1", "h1"],
@@ -38,7 +38,7 @@ export const pt: Dictionary = {
       aliases: ["h6", "titulo6", "subtitulo6"],
       group: "Subtítulos",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Título Expansível",
       subtext: "Título expansível de nível superior",
       aliases: ["h", "titulo1", "h1", "expansível"],
diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts
index b150359714..28a16d0a75 100644
--- a/packages/core/src/i18n/locales/ru.ts
+++ b/packages/core/src/i18n/locales/ru.ts
@@ -2,7 +2,7 @@ import { Dictionary } from "../dictionary.js";
 
 export const ru: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Заголовок 1 уровня",
       subtext: "Используется для заголовка верхнего уровня",
       aliases: ["h", "heading1", "h1", "заголовок1"],
@@ -38,7 +38,7 @@ export const ru: Dictionary = {
       aliases: ["h6", "heading6", "subheading6", "заголовок6", "подзаголовок6"],
       group: "Подзаголовки",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Сворачиваемый заголовок 1 уровня",
       subtext: "Заголовок верхнего уровня, который можно свернуть/развернуть",
       aliases: ["h", "heading1", "h1", "заголовок1", "сворачиваемый"],
diff --git a/packages/core/src/i18n/locales/sk.ts b/packages/core/src/i18n/locales/sk.ts
index cbdd0b706f..658438eac2 100644
--- a/packages/core/src/i18n/locales/sk.ts
+++ b/packages/core/src/i18n/locales/sk.ts
@@ -1,6 +1,6 @@
 export const sk = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Nadpis 1",
       subtext: "Nadpis najvyššej úrovne",
       aliases: ["h", "nadpis1", "h1"],
@@ -36,7 +36,7 @@ export const sk = {
       aliases: ["h6", "nadpis6", "podnadpis"],
       group: "Podnáslovi",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Rozbaľovací Nadpis 1",
       subtext: "Rozbaľovací nadpis najvyššej úrovne",
       aliases: ["h", "nadpis1", "h1", "rozbaľovací"],
diff --git a/packages/core/src/i18n/locales/uk.ts b/packages/core/src/i18n/locales/uk.ts
index a99a4259c6..5c363fc38d 100644
--- a/packages/core/src/i18n/locales/uk.ts
+++ b/packages/core/src/i18n/locales/uk.ts
@@ -2,7 +2,7 @@ import { Dictionary } from "../dictionary.js";
 
 export const uk: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Заголовок 1",
       subtext: "Заголовок найвищого рівня",
       aliases: ["h", "heading1", "h1", "заголовок1"],
@@ -38,7 +38,7 @@ export const uk: Dictionary = {
       aliases: ["h6", "heading6", "subheading6", "заголовок6", "підзаголовок6"],
       group: "Підзаголовки",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Розгортаємий заголовок 1",
       subtext: "Розгортаємий заголовок найвищого рівня",
       aliases: ["h", "heading1", "h1", "заголовок1", "розгортаємий"],
diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts
index b300fdcfd0..df1b547b16 100644
--- a/packages/core/src/i18n/locales/vi.ts
+++ b/packages/core/src/i18n/locales/vi.ts
@@ -2,7 +2,7 @@ import type { Dictionary } from "../dictionary.js";
 
 export const vi: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "Tiêu đề H1",
       subtext: "Sử dụng cho tiêu đề cấp cao nhất",
       aliases: ["h", "tieude1", "dd1"],
@@ -38,7 +38,7 @@ export const vi: Dictionary = {
       aliases: ["h6", "tieude6", "tieudephu6"],
       group: "Tiêu đề phụ",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "Tiêu đề có thể thu gọn H1",
       subtext: "Tiêu đề cấp cao nhất có thể thu gọn",
       aliases: ["h", "tieude1", "dd1", "thugon"],
diff --git a/packages/core/src/i18n/locales/zh-tw.ts b/packages/core/src/i18n/locales/zh-tw.ts
index e9aa1e8ac6..f8ca3ec3aa 100644
--- a/packages/core/src/i18n/locales/zh-tw.ts
+++ b/packages/core/src/i18n/locales/zh-tw.ts
@@ -2,7 +2,7 @@ import type { Dictionary } from "../dictionary.js";
 
 export const zhTW: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "一級標題",
       subtext: "用於頂級標題",
       aliases: ["h", "heading1", "h1", "標題", "一級標題"],
@@ -38,7 +38,7 @@ export const zhTW: Dictionary = {
       aliases: ["h6", "heading6", "subheading", "標題", "六級標題"],
       group: "副標題",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "可摺疊一級標題",
       subtext: "可摺疊的頂級標題",
       aliases: ["h", "heading1", "h1", "標題", "一級標題", "摺疊"],
diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts
index b44c81aa36..f9be961c22 100644
--- a/packages/core/src/i18n/locales/zh.ts
+++ b/packages/core/src/i18n/locales/zh.ts
@@ -2,7 +2,7 @@ import type { Dictionary } from "../dictionary.js";
 
 export const zh: Dictionary = {
   slash_menu: {
-    heading: {
+    heading_1: {
       title: "一级标题",
       subtext: "用于顶级标题",
       aliases: ["h", "heading1", "h1", "标题", "一级标题"],
@@ -38,7 +38,7 @@ export const zh: Dictionary = {
       aliases: ["h6", "heading6", "subheading6", "六级标题"],
       group: "副标题",
     },
-    toggle_heading: {
+    toggle_heading_1: {
       title: "可折叠一级标题",
       subtext: "可折叠的顶级标题",
       aliases: ["h", "heading1", "h1", "标题", "一级标题", "折叠"],
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 11fe0e5460..9845d206d3 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -55,3 +55,5 @@ export * from "./api/parsers/markdown/parseMarkdown.js";
 // TODO: for ai, remove?
 export * from "./api/blockManipulation/getBlock/getBlock.js";
 export * from "./api/positionMapping.js";
+
+// export type * from "zod";
diff --git a/packages/core/src/schema/schema.ts b/packages/core/src/schema/CustomBlockNoteSchema.ts
similarity index 77%
rename from packages/core/src/schema/schema.ts
rename to packages/core/src/schema/CustomBlockNoteSchema.ts
index 92d0a7ab88..1453a7dc4c 100644
--- a/packages/core/src/schema/schema.ts
+++ b/packages/core/src/schema/CustomBlockNoteSchema.ts
@@ -1,21 +1,20 @@
-import { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
+import type { Block, PartialBlock } from "../blocks/defaultBlocks.js";
+import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
 import { createDependencyGraph, toposortReverse } from "../util/topo-sort.js";
+import { addNodeAndExtensionsToSpec } from "./blocks/createSpec.js";
 import {
-  BlockNoDefaults,
-  BlockSchema,
-  BlockSpecs,
-  InlineContentConfig,
-  InlineContentSchema,
-  InlineContentSpec,
-  InlineContentSpecs,
-  LooseBlockSpec,
-  PartialBlockNoDefaults,
-  StyleSchema,
-  StyleSpecs,
-  addNodeAndExtensionsToSpec,
-  getInlineContentSchemaFromSpecs,
-  getStyleSchemaFromSpecs,
+  type BlockSchema,
+  type BlockSpecs,
+  type InlineContentConfig,
+  type InlineContentSchema,
+  type InlineContentSpec,
+  type InlineContentSpecs,
+  type LooseBlockSpec,
+  type StyleSchema,
+  type StyleSpecs,
 } from "./index.js";
+import { getInlineContentSchemaFromSpecs } from "./inlineContent/internal.js";
+import { getStyleSchemaFromSpecs } from "./styles/internal.js";
 
 function removeUndefined | undefined>(obj: T): T {
   if (!obj) {
@@ -26,23 +25,50 @@ function removeUndefined | undefined>(obj: T): T {
   ) as T;
 }
 
+/**
+ * Do note that this is separate from the `BlockNoteSchema` which is now reduced to a simple factory for instantiating a {@link CustomBlockNoteSchema} instance.
+ * In the future, we will rename the `BlockNoteSchema` to `DefaultBlockNoteSchema` and this will be the default schema that is used when no schema is passed to the editor.
+ * At that time, `CustomBlockNoteSchema` will be renamed to `BlockNoteSchema` and this will be the base class for all schemas.
+ */
+
+/**
+ * The CustomBlockNoteSchema class defines the shape of the schema that BlockNote uses, it defines all of the blocks, inline content, and styles that are available in the editor.
+ * You can create a custom schema by extending the CustomBlockNoteSchema class and passing in the blocks, inline content, and styles that you want to use.
+ *
+ * @example
+ * ```typescript
+ * const schema = new CustomBlockNoteSchema({
+ *   blockSpecs: {
+ *     block: { type: "block", content: "styled" },
+ *   },
+ * });
+ * // extending the schema
+ * const extendedSchema = schema.extend({
+ *   blockSpecs: {
+ *     block: { type: "block", content: "styled" },
+ *   },
+ * });
+ *
+ * // using the schema
+ * const editor = new BlockNoteEditor({
+ *   schema: extendedSchema,
+ * });
+ * ```
+ */
 export class CustomBlockNoteSchema<
-  BSchema extends BlockSchema,
-  ISchema extends InlineContentSchema,
-  SSchema extends StyleSchema,
+  BSchema extends BlockSchema = Record,
+  ISchema extends InlineContentSchema = Record,
+  SSchema extends StyleSchema = Record,
 > {
   // Helper so that you can use typeof schema.BlockNoteEditor
   public readonly BlockNoteEditor: BlockNoteEditor =
     "only for types" as any;
 
-  public readonly Block: BlockNoDefaults =
+  public readonly Block: Block =
     "only for types" as any;
 
-  public readonly PartialBlock: PartialBlockNoDefaults<
-    BSchema,
-    ISchema,
-    SSchema
-  > = "only for types" as any;
+  public readonly PartialBlock: PartialBlock =
+    "only for types" as any;
 
   public inlineContentSpecs: InlineContentSpecs;
   public styleSpecs: StyleSpecs;
diff --git a/packages/core/src/schema/blocks/internal.ts b/packages/core/src/schema/blocks/internal.ts
index 3cfb268d6f..8b4bd37e20 100644
--- a/packages/core/src/schema/blocks/internal.ts
+++ b/packages/core/src/schema/blocks/internal.ts
@@ -1,5 +1,7 @@
 import { Attribute, Attributes, Editor, Node } from "@tiptap/core";
+import * as z from "zod/v4/core";
 import { defaultBlockToHTML } from "../../blocks/defaultBlockHelpers.js";
+
 import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
 import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
 import { mergeCSSClasses } from "../../util/browser.js";
@@ -9,9 +11,9 @@ import { PropSchema, Props } from "../propTypes.js";
 import { StyleSchema } from "../styles/types.js";
 import {
   BlockConfig,
+  BlockFromConfig,
   BlockSchemaWithBlock,
   LooseBlockSpec,
-  SpecificBlock,
 } from "./types.js";
 
 // Function that uses the 'propSchema' of a blockConfig to create a TipTap
@@ -20,62 +22,53 @@ import {
 export function propsToAttributes(propSchema: PropSchema): Attributes {
   const tiptapAttributes: Record = {};
 
-  Object.entries(propSchema).forEach(([name, spec]) => {
-    tiptapAttributes[name] = {
-      default: spec.default,
-      keepOnSplit: true,
-      // Props are displayed in kebab-case as HTML attributes. If a prop's
-      // value is the same as its default, we don't display an HTML
-      // attribute for it.
-      parseHTML: (element) => {
-        const value = element.getAttribute(camelToDataKebab(name));
-
-        if (value === null) {
-          return null;
-        }
-
-        if (
-          (spec.default === undefined && spec.type === "boolean") ||
-          (spec.default !== undefined && typeof spec.default === "boolean")
-        ) {
-          if (value === "true") {
-            return true;
+  Object.entries(propSchema._zodSource._zod.def.shape).forEach(
+    ([name, spec]) => {
+      const def =
+        spec instanceof z.$ZodDefault ? spec._zod.def.defaultValue : undefined;
+
+      tiptapAttributes[name] = {
+        default: def,
+        keepOnSplit: true,
+        // Props are displayed in kebab-case as HTML attributes. If a prop's
+        // value is the same as its default, we don't display an HTML
+        // attribute for it.
+        // TODO: needed? (seems tiptap specific)
+        parseHTML: (element) => {
+          const value = element.getAttribute(camelToDataKebab(name));
+
+          if (value === null) {
+            return null;
           }
 
-          if (value === "false") {
-            return false;
+          // TBD: this might not be fault proof, but it's also ugly to store prop=""..."" for strings
+          try {
+            const jsonValue = JSON.parse(value);
+            // it was a number / boolean / json object stored as attribute
+            return z.parse(spec, jsonValue);
+          } catch (e) {
+            // it might have been a string directly stored as attribute
+            return z.parse(spec, value);
           }
-
-          return null;
-        }
-
-        if (
-          (spec.default === undefined && spec.type === "number") ||
-          (spec.default !== undefined && typeof spec.default === "number")
-        ) {
-          const asNumber = parseFloat(value);
-          const isNumeric =
-            !Number.isNaN(asNumber) && Number.isFinite(asNumber);
-
-          if (isNumeric) {
-            return asNumber;
+        },
+        // TODO: needed? (seems tiptap specific)
+        renderHTML: (attributes) => {
+          // don't render to html if the value is the same as the default
+          if (attributes[name] === def) {
+            return {};
           }
-
-          return null;
-        }
-
-        return value;
-      },
-      renderHTML: (attributes) => {
-        // don't render to html if the value is the same as the default
-        return attributes[name] !== spec.default
-          ? {
-              [camelToDataKebab(name)]: attributes[name],
-            }
-          : {};
-      },
-    };
-  });
+          if (typeof attributes[name] === "object") {
+            return {
+              [camelToDataKebab(name)]: JSON.stringify(attributes[name]),
+            };
+          }
+          return {
+            [camelToDataKebab(name)]: attributes[name],
+          };
+        },
+      };
+    },
+  );
 
   return tiptapAttributes;
 }
@@ -109,9 +102,8 @@ export function getBlockFromPos<
   }
 
   // Gets the block
-  const block = editor.getBlock(blockIdentifier)! as SpecificBlock<
-    BSchema,
-    BType,
+  const block = editor.getBlock(blockIdentifier)! as unknown as BlockFromConfig<
+    Config,
     I,
     S
   >;
@@ -167,10 +159,18 @@ export function wrapInBlockStructure<
   // which are already added as HTML attributes to the parent `blockContent`
   // element (inheritedProps) and props set to their default values.
   for (const [prop, value] of Object.entries(blockProps)) {
-    const spec = propSchema[prop];
-    const defaultValue = spec.default;
+    const spec = propSchema._zodSource._zod.def.shape[prop];
+    const defaultValue =
+      spec instanceof z.$ZodDefault ? spec._zod.def.defaultValue : undefined;
     if (value !== defaultValue) {
-      blockContent.setAttribute(camelToDataKebab(prop), value);
+      if (typeof value === "string") {
+        blockContent.setAttribute(camelToDataKebab(prop), value);
+      } else {
+        blockContent.setAttribute(
+          camelToDataKebab(prop),
+          JSON.stringify(value),
+        );
+      }
     }
   }
   // Adds file block attribute
diff --git a/packages/core/src/schema/blocks/types.ts b/packages/core/src/schema/blocks/types.ts
index 87ec01ab1b..afa252e0bf 100644
--- a/packages/core/src/schema/blocks/types.ts
+++ b/packages/core/src/schema/blocks/types.ts
@@ -12,6 +12,7 @@ import type {
 } from "../inlineContent/types.js";
 import type { PropSchema, Props } from "../propTypes.js";
 import type { StyleSchema } from "../styles/types.js";
+import { PartialTableContent, TableContent } from "./types/tableContent.js";
 
 export type BlockNoteDOMElement =
   | "editor"
@@ -72,7 +73,7 @@ export interface BlockConfig<
   type: T;
   /**
    * The properties that the block supports
-   * @todo will be zod schema in the future
+   * Now uses Zod schema for validation and type inference
    */
   readonly propSchema: PS;
   /**
@@ -222,44 +223,17 @@ export type BlockSpecsFromSchema = {
   };
 };
 
-export type BlockSchemaWithBlock = {
+export type BlockSchemaWithBlock<
+  T extends string,
+  C extends BlockConfig,
+> = NamesMatch<{
   [k in T]: C;
-};
-
-export type TableCellProps = {
-  backgroundColor: string;
-  textColor: string;
-  textAlignment: "left" | "center" | "right" | "justify";
-  colspan?: number;
-  rowspan?: number;
-};
-
-export type TableCell<
-  I extends InlineContentSchema,
-  S extends StyleSchema = StyleSchema,
-> = {
-  type: "tableCell";
-  props: TableCellProps;
-  content: InlineContent[];
-};
-
-export type TableContent<
-  I extends InlineContentSchema,
-  S extends StyleSchema = StyleSchema,
-> = {
-  type: "tableContent";
-  columnWidths: (number | undefined)[];
-  headerRows?: number;
-  headerCols?: number;
-  rows: {
-    cells: InlineContent[][] | TableCell[];
-  }[];
-};
+}>;
 
 // A BlockConfig has all the information to get the type of a Block (which is a specific instance of the BlockConfig.
 // i.e.: paragraphConfig: BlockConfig defines what a "paragraph" is / supports, and BlockFromConfigNoChildren is the shape of a specific paragraph block.
 // (for internal use)
-export type BlockFromConfigNoChildren<
+type BlockFromConfigNoChildren<
   B extends BlockConfig,
   I extends InlineContentSchema,
   S extends StyleSchema,
@@ -297,6 +271,7 @@ type BlocksWithoutChildren<
 
 // Converts each block spec into a Block object without children, merges them
 // into a union type, and adds a children property
+// TODO: should only be exposed internally
 export type BlockNoDefaults<
   BSchema extends BlockSchema,
   I extends InlineContentSchema,
@@ -305,44 +280,6 @@ export type BlockNoDefaults<
   children: BlockNoDefaults[];
 };
 
-export type SpecificBlock<
-  BSchema extends BlockSchema,
-  BType extends keyof BSchema,
-  I extends InlineContentSchema,
-  S extends StyleSchema,
-> = BlocksWithoutChildren[BType] & {
-  children: BlockNoDefaults[];
-};
-
-/** CODE FOR PARTIAL BLOCKS, analogous to above
- *
- * Partial blocks are convenience-wrappers to make it easier to
- *create/update blocks in the editor.
- *
- */
-
-export type PartialTableCell<
-  I extends InlineContentSchema,
-  S extends StyleSchema = StyleSchema,
-> = {
-  type: "tableCell";
-  props?: Partial;
-  content?: PartialInlineContent;
-};
-
-export type PartialTableContent<
-  I extends InlineContentSchema,
-  S extends StyleSchema = StyleSchema,
-> = {
-  type: "tableContent";
-  columnWidths?: (number | undefined)[];
-  headerRows?: number;
-  headerCols?: number;
-  rows: {
-    cells: PartialInlineContent[] | PartialTableCell[];
-  }[];
-};
-
 type PartialBlockFromConfigNoChildren<
   B extends BlockConfig,
   I extends InlineContentSchema,
@@ -376,32 +313,11 @@ export type PartialBlockNoDefaults<
   BSchema extends BlockSchema,
   I extends InlineContentSchema,
   S extends StyleSchema,
-> = PartialBlocksWithoutChildren<
-  BSchema,
-  I,
-  S
->[keyof PartialBlocksWithoutChildren] &
+> = PartialBlocksWithoutChildren[keyof BSchema] &
   Partial<{
     children: PartialBlockNoDefaults[];
   }>;
 
-export type SpecificPartialBlock<
-  BSchema extends BlockSchema,
-  I extends InlineContentSchema,
-  BType extends keyof BSchema,
-  S extends StyleSchema,
-> = PartialBlocksWithoutChildren[BType] & {
-  children?: BlockNoDefaults[];
-};
-
-export type PartialBlockFromConfig<
-  B extends BlockConfig,
-  I extends InlineContentSchema,
-  S extends StyleSchema,
-> = PartialBlockFromConfigNoChildren & {
-  children?: BlockNoDefaults[];
-};
-
 export type BlockIdentifier = { id: string } | string;
 
 export type BlockImplementation<
diff --git a/packages/core/src/schema/blocks/types/tableContent.ts b/packages/core/src/schema/blocks/types/tableContent.ts
new file mode 100644
index 0000000000..ad64e8bb8d
--- /dev/null
+++ b/packages/core/src/schema/blocks/types/tableContent.ts
@@ -0,0 +1,65 @@
+import {
+  InlineContent,
+  InlineContentSchema,
+  PartialInlineContent,
+} from "../../inlineContent/types.js";
+import { StyleSchema } from "../../styles/types.js";
+
+export type TableCellProps = {
+  backgroundColor: string;
+  textColor: string;
+  textAlignment: "left" | "center" | "right" | "justify";
+  colspan?: number;
+  rowspan?: number;
+};
+
+export type TableCell<
+  I extends InlineContentSchema,
+  S extends StyleSchema = StyleSchema,
+> = {
+  type: "tableCell";
+  props: TableCellProps;
+  content: InlineContent[];
+};
+
+export type TableContent<
+  I extends InlineContentSchema,
+  S extends StyleSchema = StyleSchema,
+> = {
+  type: "tableContent";
+  columnWidths: (number | undefined)[];
+  headerRows?: number;
+  headerCols?: number;
+  rows: {
+    cells: InlineContent[][] | TableCell[];
+  }[];
+};
+
+/** CODE FOR PARTIAL BLOCKS, analogous to above
+ *
+ * Partial blocks are convenience-wrappers to make it easier to
+ *create/update blocks in the editor.
+ *
+ */
+
+export type PartialTableCell<
+  I extends InlineContentSchema,
+  S extends StyleSchema = StyleSchema,
+> = {
+  type: "tableCell";
+  props?: Partial;
+  content?: PartialInlineContent;
+};
+
+export type PartialTableContent<
+  I extends InlineContentSchema,
+  S extends StyleSchema = StyleSchema,
+> = {
+  type: "tableContent";
+  columnWidths?: (number | undefined)[];
+  headerRows?: number;
+  headerCols?: number;
+  rows: {
+    cells: PartialInlineContent[] | PartialTableCell[];
+  }[];
+};
diff --git a/packages/core/src/schema/index.ts b/packages/core/src/schema/index.ts
index 05585ab28b..f3cf43f95f 100644
--- a/packages/core/src/schema/index.ts
+++ b/packages/core/src/schema/index.ts
@@ -1,11 +1,13 @@
 export * from "./blocks/createSpec.js";
 export * from "./blocks/internal.js";
 export * from "./blocks/types.js";
+export * from "./blocks/types/tableContent.js";
+export * from "./CustomBlockNoteSchema.js";
 export * from "./inlineContent/createSpec.js";
 export * from "./inlineContent/internal.js";
 export * from "./inlineContent/types.js";
+export * from "./partialBlockToBlock.js";
 export * from "./propTypes.js";
 export * from "./styles/createSpec.js";
 export * from "./styles/internal.js";
 export * from "./styles/types.js";
-export * from "./schema.js";
diff --git a/packages/core/src/schema/inlineContent/createSpec.ts b/packages/core/src/schema/inlineContent/createSpec.ts
index f4522936a4..b53c006190 100644
--- a/packages/core/src/schema/inlineContent/createSpec.ts
+++ b/packages/core/src/schema/inlineContent/createSpec.ts
@@ -5,6 +5,7 @@ import { inlineContentToNodes } from "../../api/nodeConversions/blockToNode.js";
 import { nodeToCustomInlineContent } from "../../api/nodeConversions/nodeToBlock.js";
 import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
 import { propsToAttributes } from "../blocks/internal.js";
+import { partialInlineContentToInlineContent } from "../partialBlockToBlock.js";
 import { Props } from "../propTypes.js";
 import { StyleSchema } from "../styles/types.js";
 import {
@@ -193,7 +194,11 @@ export function createInlineContentSpec<
             editor.schema.styleSchema,
           ) as any as InlineContentFromConfig, // TODO: fix cast
           (update) => {
-            const content = inlineContentToNodes([update], editor.pmSchema);
+            const fullUpdate = partialInlineContentToInlineContent(
+              [update],
+              editor.schema.inlineContentSchema,
+            );
+            const content = inlineContentToNodes(fullUpdate, editor.pmSchema);
 
             const pos = getPos();
 
diff --git a/packages/core/src/schema/inlineContent/internal.ts b/packages/core/src/schema/inlineContent/internal.ts
index 9d10c7cb4e..7692e163a4 100644
--- a/packages/core/src/schema/inlineContent/internal.ts
+++ b/packages/core/src/schema/inlineContent/internal.ts
@@ -1,4 +1,5 @@
 import { KeyboardShortcutCommand, Node } from "@tiptap/core";
+import * as z from "zod/v4/core";
 
 import { camelToDataKebab } from "../../util/string.js";
 import { PropSchema, Props } from "../propTypes.js";
@@ -32,15 +33,18 @@ export function addInlineContentAttributes<
   element.dom.setAttribute("data-inline-content-type", inlineContentType);
   // Adds props as HTML attributes in kebab-case with "data-" prefix. Skips props
   // set to their default values.
-  Object.entries(inlineContentProps)
-    .filter(([prop, value]) => {
-      const spec = propSchema[prop];
-      return value !== spec.default;
-    })
-    .map(([prop, value]) => {
-      return [camelToDataKebab(prop), value];
-    })
-    .forEach(([prop, value]) => element.dom.setAttribute(prop, value));
+  for (const [prop, value] of Object.entries(inlineContentProps)) {
+    const spec = propSchema._zodSource._zod.def.shape[prop];
+    const defaultValue =
+      spec instanceof z.$ZodDefault ? spec._zod.def.defaultValue : undefined;
+    if (value !== defaultValue) {
+      if (typeof value === "string") {
+        element.dom.setAttribute(camelToDataKebab(prop), value);
+      } else {
+        element.dom.setAttribute(camelToDataKebab(prop), JSON.stringify(value));
+      }
+    }
+  }
 
   if (element.contentDOM) {
     element.contentDOM.setAttribute("data-editable", "");
diff --git a/packages/core/src/schema/inlineContent/types.ts b/packages/core/src/schema/inlineContent/types.ts
index 4dca7a0aa3..00565932f4 100644
--- a/packages/core/src/schema/inlineContent/types.ts
+++ b/packages/core/src/schema/inlineContent/types.ts
@@ -1,8 +1,8 @@
 import { Node } from "@tiptap/core";
+import { ViewMutationRecord } from "prosemirror-view";
+import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
 import { PropSchema, Props } from "../propTypes.js";
 import { StyleSchema, Styles } from "../styles/types.js";
-import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
-import { ViewMutationRecord } from "prosemirror-view";
 
 export type CustomInlineContentConfig = {
   type: string;
diff --git a/packages/core/src/schema/partialBlockToBlock.ts b/packages/core/src/schema/partialBlockToBlock.ts
new file mode 100644
index 0000000000..c634e7a544
--- /dev/null
+++ b/packages/core/src/schema/partialBlockToBlock.ts
@@ -0,0 +1,259 @@
+import * as z from "zod/v4/core";
+import type { Block, PartialBlock } from "../blocks/index.js";
+import { UniqueID } from "../extensions/UniqueID/UniqueID.js";
+import { mapTableCell } from "../util/table.js";
+import { UnreachableCaseError } from "../util/typescript.js";
+import type { BlockSchema } from "./blocks/types.js";
+import type {
+  PartialTableContent,
+  TableCell,
+  TableContent,
+} from "./blocks/types/tableContent.js";
+import type { CustomBlockNoteSchema } from "./CustomBlockNoteSchema.js";
+import type {
+  InlineContent,
+  InlineContentSchema,
+  Link,
+  PartialInlineContent,
+  PartialLink,
+  StyledText,
+} from "./inlineContent/types.js";
+import {
+  isPartialLinkInlineContent,
+  isStyledTextInlineContent,
+} from "./inlineContent/types.js";
+import type { Props, PropSchema } from "./propTypes.js";
+import type { StyleSchema } from "./styles/types.js";
+
+function partialPropsToProps(
+  partialProps: Partial> | undefined,
+  propSchema: PropSchema,
+): Props {
+  const props: Props = partialProps || {};
+
+  Object.entries(propSchema._zodSource._zod.def.shape).forEach(
+    ([propKey, propValue]) => {
+      if (props[propKey] === undefined) {
+        if (propValue instanceof z.$ZodDefault) {
+          props[propKey] = propValue._zod.def.defaultValue;
+        }
+        if (propValue instanceof z.$ZodOptional) {
+          props[propKey] = undefined;
+        }
+      }
+    },
+  );
+  return props;
+}
+
+function textStringToStyledText(text: string): StyledText {
+  return {
+    type: "text",
+    styles: {},
+    text,
+  };
+}
+
+function partialLinkToLink(partialLink: PartialLink): Link {
+  return {
+    type: "link",
+    href: partialLink.href,
+    content:
+      typeof partialLink.content === "string"
+        ? [textStringToStyledText(partialLink.content)]
+        : partialLink.content,
+  };
+}
+
+export function partialInlineContentToInlineContent(
+  partialInlineContent:
+    | PartialInlineContent
+    | undefined,
+  inlineContentSchema: InlineContentSchema,
+): InlineContent[] {
+  if (partialInlineContent === undefined) {
+    return [];
+  }
+
+  if (typeof partialInlineContent === "string") {
+    return [textStringToStyledText(partialInlineContent)];
+  }
+
+  return partialInlineContent.map((partialInlineContentElement) => {
+    if (typeof partialInlineContentElement === "string") {
+      return textStringToStyledText(partialInlineContentElement);
+    }
+
+    if (isPartialLinkInlineContent(partialInlineContentElement)) {
+      return partialLinkToLink(partialInlineContentElement);
+    }
+
+    if (isStyledTextInlineContent(partialInlineContentElement)) {
+      return partialInlineContentElement;
+    }
+
+    const content = partialInlineContentElement.content;
+    const inlineContentConfig =
+      inlineContentSchema[partialInlineContentElement.type];
+
+    if (typeof inlineContentConfig === "string") {
+      throw new Error(
+        "unexpected, should be custom inline content (not 'text' or 'link'",
+      );
+    }
+
+    return {
+      type: partialInlineContentElement.type,
+      props: partialPropsToProps(
+        partialInlineContentElement.props,
+        inlineContentConfig.propSchema,
+      ),
+      content:
+        typeof content === "undefined"
+          ? undefined
+          : typeof content === "string"
+            ? [textStringToStyledText(content)]
+            : content,
+    };
+  });
+}
+
+export function partialTableContentToTableContent(
+  partialTableContent: PartialTableContent,
+  inlineContentSchema: InlineContentSchema,
+): TableContent {
+  const rows: {
+    cells: TableCell[];
+  }[] = partialTableContent.rows.map((row) => {
+    return {
+      cells: row.cells.map((cell) => {
+        const fullCell = mapTableCell(cell);
+        // `mapTableCell` doesn't actually convert `PartialInlineContent` to
+        // `InlineContent`, so this is done manually here.
+        fullCell.content = partialInlineContentToInlineContent(
+          fullCell.content,
+          inlineContentSchema,
+        );
+
+        return fullCell;
+      }),
+    };
+  });
+
+  const columnWidths = partialTableContent.columnWidths || [];
+  if (!partialTableContent.columnWidths) {
+    for (const cell of rows[0].cells) {
+      for (let i = 0; i < (cell.props?.colspan || 1); i++) {
+        columnWidths.push(undefined);
+      }
+    }
+  }
+
+  return {
+    type: "tableContent",
+    headerRows: partialTableContent.headerRows,
+    headerCols: partialTableContent.headerCols,
+    columnWidths: columnWidths,
+    rows,
+  };
+}
+
+function partialBlockContentToBlockContent(
+  partialBlockContent:
+    | PartialTableContent
+    | PartialInlineContent
+    | undefined,
+  content: "table" | "inline" | "none",
+  inlineContentSchema: InlineContentSchema,
+):
+  | TableContent
+  | InlineContent[]
+  | undefined {
+  if (content === "table") {
+    partialBlockContent = partialBlockContent || {
+      type: "tableContent",
+      rows: [],
+    };
+
+    if (
+      typeof partialBlockContent !== "object" ||
+      !("type" in partialBlockContent) ||
+      partialBlockContent.type !== "tableContent"
+    ) {
+      throw new Error("Invalid partial block content");
+    }
+
+    return partialTableContentToTableContent(
+      partialBlockContent,
+      inlineContentSchema,
+    );
+  } else if (content === "inline") {
+    partialBlockContent = partialBlockContent || undefined;
+
+    if (
+      typeof partialBlockContent === "object" &&
+      "type" in partialBlockContent
+    ) {
+      throw new Error("Invalid partial block content. Table content passed!?");
+    }
+
+    return partialInlineContentToInlineContent(
+      partialBlockContent,
+      inlineContentSchema,
+    );
+  } else if (content === "none") {
+    return undefined;
+  } else {
+    throw new UnreachableCaseError(content);
+  }
+}
+
+export function partialBlockToBlock<
+  BSchema extends BlockSchema,
+  I extends InlineContentSchema,
+  S extends StyleSchema,
+>(
+  schema: CustomBlockNoteSchema,
+  partialBlock: PartialBlock,
+): Block {
+  const id = partialBlock.id || UniqueID.options.generateID();
+
+  // Note: we might want to make "type" required for partial blocks and remove this default
+  const type: string = partialBlock.type || "paragraph";
+
+  const props = partialPropsToProps(
+    partialBlock.props,
+    schema.blockSchema[type].propSchema,
+  );
+
+  const content = partialBlockContentToBlockContent(
+    partialBlock.content,
+    schema.blockSchema[type].content,
+    schema.inlineContentSchema,
+  );
+
+  const children =
+    partialBlock.children?.map((child) => partialBlockToBlock(schema, child)) ||
+    [];
+
+  return {
+    id,
+    type,
+    props,
+    content,
+    children,
+  } as Block