diff --git a/app/Exceptions/ZipExportValidationException.php b/app/Exceptions/ZipExportValidationException.php
deleted file mode 100644
index 2ed567d63..000000000
--- a/app/Exceptions/ZipExportValidationException.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-namespace BookStack\Exceptions;
-
-class ZipExportValidationException extends \Exception
-{
-    public function __construct(
-        public array $errors,
-    ) {
-        parent::__construct();
-    }
-}
diff --git a/app/Exports/Controllers/ImportController.php b/app/Exports/Controllers/ImportController.php
index 4270828ef..5885f7991 100644
--- a/app/Exports/Controllers/ImportController.php
+++ b/app/Exports/Controllers/ImportController.php
@@ -2,6 +2,7 @@
 
 namespace BookStack\Exports\Controllers;
 
+use BookStack\Exports\ZipExports\ZipExportValidator;
 use BookStack\Http\Controller;
 use Illuminate\Http\Request;
 
@@ -26,7 +27,13 @@ class ImportController extends Controller
         ]);
 
         $file = $request->file('file');
-        $file->getRealPath();
+        $zipPath = $file->getRealPath();
+
+        $errors = (new ZipExportValidator($zipPath))->validate();
+        if ($errors) {
+            dd($errors);
+        }
+        dd('passed');
         // TODO - Read existing ZIP upload and send through validator
             // TODO - If invalid, return user with errors
         // TODO - Upload to storage
diff --git a/app/Exports/ZipExports/Models/ZipExportAttachment.php b/app/Exports/ZipExports/Models/ZipExportAttachment.php
index ab1f5ab75..e586b91b0 100644
--- a/app/Exports/ZipExports/Models/ZipExportAttachment.php
+++ b/app/Exports/ZipExports/Models/ZipExportAttachment.php
@@ -47,6 +47,6 @@ class ZipExportAttachment extends ZipExportModel
             'file'  => ['required_without:link', 'nullable', 'string', $context->fileReferenceRule()],
         ];
 
-        return $context->validateArray($data, $rules);
+        return $context->validateData($data, $rules);
     }
 }
diff --git a/app/Exports/ZipExports/Models/ZipExportBook.php b/app/Exports/ZipExports/Models/ZipExportBook.php
index 5a0c5806b..7e1f2d810 100644
--- a/app/Exports/ZipExports/Models/ZipExportBook.php
+++ b/app/Exports/ZipExports/Models/ZipExportBook.php
@@ -6,6 +6,7 @@ use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Page;
 use BookStack\Exports\ZipExports\ZipExportFiles;
+use BookStack\Exports\ZipExports\ZipValidationHelper;
 
 class ZipExportBook extends ZipExportModel
 {
@@ -50,4 +51,24 @@ class ZipExportBook extends ZipExportModel
 
         return $instance;
     }
+
+    public static function validate(ZipValidationHelper $context, array $data): array
+    {
+        $rules = [
+            'id'    => ['nullable', 'int'],
+            'name'  => ['required', 'string', 'min:1'],
+            'description_html' => ['nullable', 'string'],
+            'cover' => ['nullable', 'string', $context->fileReferenceRule()],
+            'tags' => ['array'],
+            'pages' => ['array'],
+            'chapters' => ['array'],
+        ];
+
+        $errors = $context->validateData($data, $rules);
+        $errors['tags'] = $context->validateRelations($data['tags'] ?? [], ZipExportTag::class);
+        $errors['pages'] = $context->validateRelations($data['pages'] ?? [], ZipExportPage::class);
+        $errors['chapters'] = $context->validateRelations($data['chapters'] ?? [], ZipExportChapter::class);
+
+        return $errors;
+    }
 }
diff --git a/app/Exports/ZipExports/Models/ZipExportChapter.php b/app/Exports/ZipExports/Models/ZipExportChapter.php
index cd5765f48..03df31b70 100644
--- a/app/Exports/ZipExports/Models/ZipExportChapter.php
+++ b/app/Exports/ZipExports/Models/ZipExportChapter.php
@@ -5,6 +5,7 @@ namespace BookStack\Exports\ZipExports\Models;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Page;
 use BookStack\Exports\ZipExports\ZipExportFiles;
+use BookStack\Exports\ZipExports\ZipValidationHelper;
 
 class ZipExportChapter extends ZipExportModel
 {
@@ -42,4 +43,22 @@ class ZipExportChapter extends ZipExportModel
             return self::fromModel($chapter, $files);
         }, $chapterArray));
     }
+
+    public static function validate(ZipValidationHelper $context, array $data): array
+    {
+        $rules = [
+            'id'    => ['nullable', 'int'],
+            'name'  => ['required', 'string', 'min:1'],
+            'description_html' => ['nullable', 'string'],
+            'priority' => ['nullable', 'int'],
+            'tags' => ['array'],
+            'pages' => ['array'],
+        ];
+
+        $errors = $context->validateData($data, $rules);
+        $errors['tags'] = $context->validateRelations($data['tags'] ?? [], ZipExportTag::class);
+        $errors['pages'] = $context->validateRelations($data['pages'] ?? [], ZipExportPage::class);
+
+        return $errors;
+    }
 }
diff --git a/app/Exports/ZipExports/Models/ZipExportImage.php b/app/Exports/ZipExports/Models/ZipExportImage.php
index 05d828734..3388c66df 100644
--- a/app/Exports/ZipExports/Models/ZipExportImage.php
+++ b/app/Exports/ZipExports/Models/ZipExportImage.php
@@ -3,7 +3,9 @@
 namespace BookStack\Exports\ZipExports\Models;
 
 use BookStack\Exports\ZipExports\ZipExportFiles;
+use BookStack\Exports\ZipExports\ZipValidationHelper;
 use BookStack\Uploads\Image;
+use Illuminate\Validation\Rule;
 
 class ZipExportImage extends ZipExportModel
 {
@@ -22,4 +24,16 @@ class ZipExportImage extends ZipExportModel
 
         return $instance;
     }
+
+    public static function validate(ZipValidationHelper $context, array $data): array
+    {
+        $rules = [
+            'id'    => ['nullable', 'int'],
+            'name'  => ['required', 'string', 'min:1'],
+            'file'  => ['required', 'string', $context->fileReferenceRule()],
+            'type'  => ['required', 'string', Rule::in(['gallery', 'drawio'])],
+        ];
+
+        return $context->validateData($data, $rules);
+    }
 }
diff --git a/app/Exports/ZipExports/Models/ZipExportPage.php b/app/Exports/ZipExports/Models/ZipExportPage.php
index 8075595f2..2c8b9a88a 100644
--- a/app/Exports/ZipExports/Models/ZipExportPage.php
+++ b/app/Exports/ZipExports/Models/ZipExportPage.php
@@ -5,6 +5,7 @@ namespace BookStack\Exports\ZipExports\Models;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Tools\PageContent;
 use BookStack\Exports\ZipExports\ZipExportFiles;
+use BookStack\Exports\ZipExports\ZipValidationHelper;
 
 class ZipExportPage extends ZipExportModel
 {
@@ -48,4 +49,25 @@ class ZipExportPage extends ZipExportModel
             return self::fromModel($page, $files);
         }, $pageArray));
     }
+
+    public static function validate(ZipValidationHelper $context, array $data): array
+    {
+        $rules = [
+            'id'    => ['nullable', 'int'],
+            'name'  => ['required', 'string', 'min:1'],
+            'html' => ['nullable', 'string'],
+            'markdown' => ['nullable', 'string'],
+            'priority' => ['nullable', 'int'],
+            'attachments' => ['array'],
+            'images' => ['array'],
+            'tags' => ['array'],
+        ];
+
+        $errors = $context->validateData($data, $rules);
+        $errors['attachments'] = $context->validateRelations($data['attachments'] ?? [], ZipExportAttachment::class);
+        $errors['images'] = $context->validateRelations($data['images'] ?? [], ZipExportImage::class);
+        $errors['tags'] = $context->validateRelations($data['tags'] ?? [], ZipExportTag::class);
+
+        return $errors;
+    }
 }
diff --git a/app/Exports/ZipExports/Models/ZipExportTag.php b/app/Exports/ZipExports/Models/ZipExportTag.php
index ad17d5a33..99abb811c 100644
--- a/app/Exports/ZipExports/Models/ZipExportTag.php
+++ b/app/Exports/ZipExports/Models/ZipExportTag.php
@@ -34,6 +34,6 @@ class ZipExportTag extends ZipExportModel
             'order' => ['nullable', 'integer'],
         ];
 
-        return $context->validateArray($data, $rules);
+        return $context->validateData($data, $rules);
     }
 }
diff --git a/app/Exports/ZipExports/ZipExportValidator.php b/app/Exports/ZipExports/ZipExportValidator.php
index 5ad9272de..e56394aca 100644
--- a/app/Exports/ZipExports/ZipExportValidator.php
+++ b/app/Exports/ZipExports/ZipExportValidator.php
@@ -2,62 +2,69 @@
 
 namespace BookStack\Exports\ZipExports;
 
-use BookStack\Exceptions\ZipExportValidationException;
+use BookStack\Exports\ZipExports\Models\ZipExportBook;
+use BookStack\Exports\ZipExports\Models\ZipExportChapter;
+use BookStack\Exports\ZipExports\Models\ZipExportPage;
 use ZipArchive;
 
 class ZipExportValidator
 {
-    protected array $errors = [];
-
     public function __construct(
         protected string $zipPath,
     ) {
     }
 
-    /**
-     * @throws ZipExportValidationException
-     */
-    public function validate()
+    public function validate(): array
     {
-        // TODO - Return type
-        // TODO - extract messages to translations?
-
         // Validate file exists
         if (!file_exists($this->zipPath) || !is_readable($this->zipPath)) {
-            $this->throwErrors("Could not read ZIP file");
+            return ['format' => "Could not read ZIP file"];
         }
 
         // Validate file is valid zip
         $zip = new \ZipArchive();
         $opened = $zip->open($this->zipPath, ZipArchive::RDONLY);
         if ($opened !== true) {
-            $this->throwErrors("Could not read ZIP file");
+            return ['format' => "Could not read ZIP file"];
         }
 
         // Validate json data exists, including metadata
         $jsonData = $zip->getFromName('data.json') ?: '';
         $importData = json_decode($jsonData, true);
         if (!$importData) {
-            $this->throwErrors("Could not decode ZIP data.json content");
+            return ['format' => "Could not find and decode ZIP data.json content"];
         }
 
+        $helper = new ZipValidationHelper($zip);
+
         if (isset($importData['book'])) {
-            // TODO - Validate book
+            $modelErrors = ZipExportBook::validate($helper, $importData['book']);
+            $keyPrefix = 'book';
         } else if (isset($importData['chapter'])) {
-            // TODO - Validate chapter
+            $modelErrors = ZipExportChapter::validate($helper, $importData['chapter']);
+            $keyPrefix = 'chapter';
         } else if (isset($importData['page'])) {
-            // TODO - Validate page
+            $modelErrors = ZipExportPage::validate($helper, $importData['page']);
+            $keyPrefix = 'page';
         } else {
-            $this->throwErrors("ZIP file has no book, chapter or page data");
+            return ['format' => "ZIP file has no book, chapter or page data"];
         }
+
+        return $this->flattenModelErrors($modelErrors, $keyPrefix);
     }
 
-    /**
-     * @throws ZipExportValidationException
-     */
-    protected function throwErrors(...$errorsToAdd): never
+    protected function flattenModelErrors(array $errors, string $keyPrefix): array
     {
-        array_push($this->errors, ...$errorsToAdd);
-        throw new ZipExportValidationException($this->errors);
+        $flattened = [];
+
+        foreach ($errors as $key => $error) {
+            if (is_array($error)) {
+                $flattened = array_merge($flattened, $this->flattenModelErrors($error, $keyPrefix . '.' . $key));
+            } else {
+                $flattened[$keyPrefix . '.' . $key] = $error;
+            }
+        }
+
+        return $flattened;
     }
 }
diff --git a/app/Exports/ZipExports/ZipValidationHelper.php b/app/Exports/ZipExports/ZipValidationHelper.php
index dd41e6f8b..8c285deaf 100644
--- a/app/Exports/ZipExports/ZipValidationHelper.php
+++ b/app/Exports/ZipExports/ZipValidationHelper.php
@@ -2,6 +2,7 @@
 
 namespace BookStack\Exports\ZipExports;
 
+use BookStack\Exports\ZipExports\Models\ZipExportModel;
 use Illuminate\Validation\Factory;
 use ZipArchive;
 
@@ -15,9 +16,15 @@ class ZipValidationHelper
         $this->validationFactory = app(Factory::class);
     }
 
-    public function validateArray(array $data, array $rules): array
+    public function validateData(array $data, array $rules): array
     {
-        return $this->validationFactory->make($data, $rules)->errors()->messages();
+        $messages = $this->validationFactory->make($data, $rules)->errors()->messages();
+
+        foreach ($messages as $key => $message) {
+            $messages[$key] = implode("\n", $message);
+        }
+
+        return $messages;
     }
 
     public function zipFileExists(string $name): bool
@@ -29,4 +36,24 @@ class ZipValidationHelper
     {
         return new ZipFileReferenceRule($this);
     }
+
+    /**
+     * Validate an array of relation data arrays that are expected
+     * to be for the given ZipExportModel.
+     * @param class-string<ZipExportModel> $model
+     */
+    public function validateRelations(array $relations, string $model): array
+    {
+        $results = [];
+
+        foreach ($relations as $key => $relationData) {
+            if (is_array($relationData)) {
+                $results[$key] = $model::validate($this, $relationData);
+            } else {
+                $results[$key] = [trans('validation.zip_model_expected', ['type' => gettype($relationData)])];
+            }
+        }
+
+        return $results;
+    }
 }
diff --git a/lang/en/validation.php b/lang/en/validation.php
index 6971edc02..9cf5d78b6 100644
--- a/lang/en/validation.php
+++ b/lang/en/validation.php
@@ -105,7 +105,8 @@ return [
     'url'                  => 'The :attribute format is invalid.',
     'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',
 
-    'zip_file'            => 'The :attribute needs to reference a file within the ZIP.',
+    'zip_file' => 'The :attribute needs to reference a file within the ZIP.',
+    'zip_model_expected' => 'Data object expected but ":type" found',
 
     // Custom validation lines
     'custom' => [
diff --git a/resources/views/exports/import.blade.php b/resources/views/exports/import.blade.php
index 9fe596d88..15f33e6b7 100644
--- a/resources/views/exports/import.blade.php
+++ b/resources/views/exports/import.blade.php
@@ -6,7 +6,7 @@
 
         <main class="card content-wrap auto-height mt-xxl">
             <h1 class="list-heading">{{ trans('entities.import') }}</h1>
-            <form action="{{ url('/import') }}" method="POST">
+            <form action="{{ url('/import') }}" enctype="multipart/form-data" method="POST">
                 {{ csrf_field() }}
                 <div class="flex-container-row justify-space-between wrap gap-x-xl gap-y-s">
                     <p class="flex min-width-l text-muted mb-s">
@@ -22,6 +22,7 @@
                                    name="file"
                                    id="file"
                                    class="custom-simple-file-input">
+                            @include('form.errors', ['name' => 'file'])
                         </div>
                     </div>
                 </div>