diff --git a/docs/pages/docs/features/validation/index.md b/docs/pages/docs/features/validation/index.md
index f6dc6f989c9c5c0f613bd9058dd75b32d17245ff..ca43f77217890ac7b4fa854272f9997065a5f219 100644
--- a/docs/pages/docs/features/validation/index.md
+++ b/docs/pages/docs/features/validation/index.md
@@ -47,7 +47,7 @@ readonly class BlogPostForm extends InputValidatedData
 ## Validators
 
 Validators take in any data and check if it adheres to the configuration 
-schema. The `makeSchema()` method must return a 
+schema. The `getSchema()` method must return a 
 [JSON Schema](https://json-schema.org/) object.
 
 ```php
@@ -70,7 +70,7 @@ use Distantmagic\Resonance\SingletonCollection;
 #[Singleton(collection: SingletonCollection::InputValidator)]
 readonly class BlogPostFormValidator extends InputValidator
 {
-    protected function castValidatedData(mixed $data): BlogPostForm
+    public function castValidatedData(mixed $data): BlogPostForm
     {
         return new BlogPostForm(
             $data['content'],
@@ -78,7 +78,7 @@ readonly class BlogPostFormValidator extends InputValidator
         );
     }
 
-    protected function makeSchema(): Schema
+    public function getSchema(): Schema
     {
         return new JsonSchema([
             'type' => 'object',
@@ -100,18 +100,22 @@ readonly class BlogPostFormValidator extends InputValidator
 
 Preferably validators should be injected somewhere by the 
 {{docs/features/dependency-injection/index}}, so you don't have to set up their 
-parameters manually. Then you can call their `validateData()` or
-`validateRequest()` methods.
+parameters manually. Then you can call their `validateData()` method.
 
 ```php
 <?php
 
+use Distantmagic\Resonance\InputValidatorController;
+use Distantmagic\Resonance\JsonSchemaValidator;
 use Distantmagic\Resonance\InputValidationResult;
 
+$jsonSchemaValidator = new JsonSchemaValidator();
+$inputValidatorController = new InputValidatorController($jsonSchemaValidator);
+
 /**
  * @var InputValidationResult $validationResult
  */
-$validationResult = $blogPostFormValidator->validateData([
+$validationResult = $inputValidatorController->validateData($blogPostFormValidator, [
     'content' => 'test',
     'title' => 'test',
 ]);
diff --git a/docs/pages/docs/features/websockets/protocols.md b/docs/pages/docs/features/websockets/protocols.md
index 4af04e3689b22c14be67e428fed45b9a4e6061e3..37116bd556d6cf4587399d464aaf858288c828a1 100644
--- a/docs/pages/docs/features/websockets/protocols.md
+++ b/docs/pages/docs/features/websockets/protocols.md
@@ -64,6 +64,7 @@ use App\RPCMethod;
 use Distantmagic\Resonance\Attribute\RespondsToWebSocketRPC;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\Feature;
+use Distantmagic\Resonance\JsonSchema;
 use Distantmagic\Resonance\RPCRequest;
 use Distantmagic\Resonance\RPCResponse;
 use Distantmagic\Resonance\SingletonCollection;
@@ -78,14 +79,21 @@ use Distantmagic\Resonance\WebSocketRPCResponder;
 )]
 final readonly class EchoResponder extends WebSocketRPCResponder
 {
-    protected function onRequest(
+    public function getSchema(): JsonSchema
+    {
+        return new JsonSchema([
+            'type' => 'string',
+        ]);
+    }
+
+    public function onRequest(
         WebSocketAuthResolution $webSocketAuthResolution,
         WebSocketConnection $webSocketConnection,
         RPCRequest $rpcRequest,
     ): void {
         $webSocketConnection->push(new RPCResponse(
             $rpcRequest->requestId,
-            (string) $rpcRequest->payload,
+            $rpcRequest->payload,
         ));
     }
 }
diff --git a/docs/pages/index.md b/docs/pages/index.md
index 837817b6e02286b82dfeef3ae45ef90a4d4b9d44..625b00ff96088b99c946616f11062b229920ca01 100644
--- a/docs/pages/index.md
+++ b/docs/pages/index.md
@@ -106,14 +106,21 @@ description: >
 #[Singleton(collection: SingletonCollection::WebSocketRPCResponder)]
 final readonly class EchoResponder extends WebSocketRPCResponder
 {
-    protected function onRequest(
+    public function getSchema(): JsonSchema
+    {
+        return new JsonSchema([
+            'type' => 'string',
+        ]);
+    }
+
+    public function onRequest(
         WebSocketAuthResolution $webSocketAuthResolution,
         WebSocketConnection $webSocketConnection,
         RPCRequest $rpcRequest,
     ): void {
         $webSocketConnection->push(new RPCResponse(
             $rpcRequest->requestId,
-            (string) $rpcRequest->payload,
+            $rpcRequest->payload,
         ));
     }
 }</code></pre>
diff --git a/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md b/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md
index a263fd977bf0e3ee086bcdc84fdfe2cc950f7c5d..fbb52d35312ef5b8c6b4e6c137a8db4434c35c1e 100644
--- a/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md
+++ b/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md
@@ -203,6 +203,7 @@ use App\RPCMethod;
 use Distantmagic\Resonance\Attribute\RespondsToWebSocketRPC;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\Feature;
+use Distantmagic\Resonance\JsonSchema;
 use Distantmagic\Resonance\LlamaCppClient;
 use Distantmagic\Resonance\LlamaCppCompletionRequest;
 use Distantmagic\Resonance\RPCNotification;
@@ -224,7 +225,24 @@ final readonly class LlmChatPromptResponder extends WebSocketRPCResponder
         private LlamaCppClient $llamaCppClient,
     ) {}
 
-    protected function onNotification(
+    public function getSchema(): JsonSchema
+    {
+        return new JsonSchema([
+            'type' => 'object',
+            'properties' => [
+                'prompt' => [
+                    'minLength' => 1,
+                    'type' => 'string',
+                ],
+            ],
+            'required' => [
+                'prompt',
+            ],
+            'additionalProperties' => false,
+        ]);
+    }
+
+    public function onNotification(
         WebSocketAuthResolution $webSocketAuthResolution,
         WebSocketConnection $webSocketConnection,
         RPCNotification $rpcNotification,
diff --git a/docs/pages/tutorials/session-based-authentication/index.md b/docs/pages/tutorials/session-based-authentication/index.md
index 0559023dedaa3692485c7a42cc448aa796482729..9dec331ff17e63e7625bf148875256b018e758d4 100644
--- a/docs/pages/tutorials/session-based-authentication/index.md
+++ b/docs/pages/tutorials/session-based-authentication/index.md
@@ -370,12 +370,12 @@ use Distantmagic\Resonance\SingletonCollection;
 #[Singleton(collection: SingletonCollection::InputValidator)]
 readonly class UsernamePasswordValidator extends InputValidator
 {
-    protected function castValidatedData(mixed $data): UsernamePassword
+    public function castValidatedData(mixed $data): UsernamePassword
     {
         return new UsernamePassword($data->username, $data->password);
     }
 
-    protected function makeSchema(): Schema
+    public function getSchema(): Schema
     {
         return new JsonSchema([
             'type' => 'object',
diff --git a/src/CastsValidatedDataInterface.php b/src/CastsValidatedDataInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..ff92e50181734dfcb88c1803a7a728a1c9133da2
--- /dev/null
+++ b/src/CastsValidatedDataInterface.php
@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+/**
+ * @template TCastedData of InputValidatedData
+ * @template TValidatedData
+ */
+interface CastsValidatedDataInterface
+{
+    /**
+     * @param TValidatedData $data
+     *
+     * @return TCastedData
+     */
+    public function castValidatedData(mixed $data): InputValidatedData;
+}
diff --git a/src/HttpControllerParameterResolver/ValidatedRequestResolver.php b/src/HttpControllerParameterResolver/ValidatedRequestResolver.php
index 42cb0af577b7df5411919539b1825586b182e89b..ea2e03fc1b7e3ea02b6b2248081935a06d1c73f4 100644
--- a/src/HttpControllerParameterResolver/ValidatedRequestResolver.php
+++ b/src/HttpControllerParameterResolver/ValidatedRequestResolver.php
@@ -13,6 +13,7 @@ use Distantmagic\Resonance\HttpControllerParameterResolution;
 use Distantmagic\Resonance\HttpControllerParameterResolutionStatus;
 use Distantmagic\Resonance\HttpControllerParameterResolver;
 use Distantmagic\Resonance\InputValidatorCollection;
+use Distantmagic\Resonance\InputValidatorController;
 use Distantmagic\Resonance\SingletonCollection;
 use LogicException;
 use Swoole\Http\Request;
@@ -27,6 +28,7 @@ readonly class ValidatedRequestResolver extends HttpControllerParameterResolver
 {
     public function __construct(
         private InputValidatorCollection $inputValidatorCollection,
+        private InputValidatorController $inputValidatorController,
     ) {}
 
     public function resolve(
@@ -42,7 +44,10 @@ readonly class ValidatedRequestResolver extends HttpControllerParameterResolver
         }
 
         $validator = $this->inputValidatorCollection->inputValidators->get($validatorClassName);
-        $validationResult = $validator->validateRequest($request);
+        $validationResult = $this
+            ->inputValidatorController
+            ->validateData($validator, $request->post)
+        ;
 
         if ($validationResult->inputValidatedData) {
             if ($validationResult->inputValidatedData instanceof $parameter->className) {
diff --git a/src/InputValidator.php b/src/InputValidator.php
index 30127d8d85133f144dc0f1e7779e243d4b060852..9ea2e3e62a7ccb103e9478e7fb59b98ccca4f24f 100644
--- a/src/InputValidator.php
+++ b/src/InputValidator.php
@@ -4,80 +4,10 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
-use LogicException;
-use Opis\JsonSchema\Exceptions\ParseException;
-use Swoole\Http\Request;
-
 /**
- * @template TValidatedModel of InputValidatedData
+ * @template TCastedData of InputValidatedData
  * @template TValidatedData
+ *
+ * @template-implements CastsValidatedDataInterface<TCastedData,TValidatedData>
  */
-abstract readonly class InputValidator
-{
-    public JsonSchema $jsonSchema;
-
-    /**
-     * @param TValidatedData $data
-     *
-     * @return TValidatedModel
-     */
-    abstract protected function castValidatedData(mixed $data): InputValidatedData;
-
-    abstract protected function makeSchema(): JsonSchema;
-
-    public function __construct(private JsonSchemaValidator $jsonSchemaValidator)
-    {
-        $this->jsonSchema = $this->makeSchema();
-    }
-
-    public function getSchema(): JsonSchema
-    {
-        return $this->jsonSchema;
-    }
-
-    /**
-     * @return InputValidationResult<TValidatedModel>
-     */
-    public function validateData(mixed $data): InputValidationResult
-    {
-        try {
-            /**
-             * @var JsonSchemaValidationResult<TValidatedData>
-             */
-            $jsonSchemaValidationResult = $this->jsonSchemaValidator->validate($this->jsonSchema, $data);
-        } catch (ParseException $parseException) {
-            throw new LogicException(sprintf(
-                'JSON schema is invalid in: "%s"',
-                $this::class,
-            ), 0, $parseException);
-        }
-
-        $errors = $jsonSchemaValidationResult->errors;
-
-        if (empty($errors)) {
-            return new InputValidationResult($this->castValidatedData($jsonSchemaValidationResult->data));
-        }
-
-        /**
-         * @var InputValidationResult<TValidatedModel>
-         */
-        $validationResult = new InputValidationResult();
-
-        foreach ($errors as $propertyName => $propertyErrors) {
-            $validationResult->errors->put(
-                $propertyName,
-                implode("\n", $propertyErrors),
-            );
-        }
-
-        return $validationResult;
-    }
-
-    /**
-     * @return InputValidationResult<TValidatedModel>
-     */
-    public function validateRequest(Request $request): InputValidationResult
-    {
-        return $this->validateData($request->post);
-    }
-}
+abstract readonly class InputValidator implements CastsValidatedDataInterface, JsonSchemaSourceInterface {}
diff --git a/src/InputValidator/FrontMatterValidator.php b/src/InputValidator/FrontMatterValidator.php
index bd250518afb5174548059c9d37115613364b8bc3..627f9977fab41267575e9cfba78ed0f0ad084620 100644
--- a/src/InputValidator/FrontMatterValidator.php
+++ b/src/InputValidator/FrontMatterValidator.php
@@ -28,7 +28,7 @@ use Generator;
 #[Singleton]
 readonly class FrontMatterValidator extends InputValidator
 {
-    protected function castValidatedData(mixed $data): FrontMatter
+    public function castValidatedData(mixed $data): FrontMatter
     {
         $collections = iterator_to_array($this->normalizeDataCollections($data->collections));
 
@@ -45,7 +45,7 @@ readonly class FrontMatterValidator extends InputValidator
         );
     }
 
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         $contentTypes = StaticPageContentType::values();
 
diff --git a/src/InputValidator/RPCMessageValidator.php b/src/InputValidator/RPCMessageValidator.php
index 7716c6870de02f7324c9a4aa49b081e3d521280e..b7cb9d680cb0bac124603c99571a474587c4e564 100644
--- a/src/InputValidator/RPCMessageValidator.php
+++ b/src/InputValidator/RPCMessageValidator.php
@@ -9,7 +9,6 @@ use Distantmagic\Resonance\Feature;
 use Distantmagic\Resonance\InputValidatedData\RPCMessage;
 use Distantmagic\Resonance\InputValidator;
 use Distantmagic\Resonance\JsonSchema;
-use Distantmagic\Resonance\JsonSchemaValidator;
 use Distantmagic\Resonance\RPCMethodValidatorInterface;
 use stdClass;
 
@@ -23,14 +22,9 @@ use stdClass;
 #[Singleton(grantsFeature: Feature::WebSocket)]
 readonly class RPCMessageValidator extends InputValidator
 {
-    public function __construct(
-        JsonSchemaValidator $jsonSchemaValidator,
-        private RPCMethodValidatorInterface $rpcMethodValidator,
-    ) {
-        parent::__construct($jsonSchemaValidator);
-    }
+    public function __construct(private RPCMethodValidatorInterface $rpcMethodValidator) {}
 
-    protected function castValidatedData(mixed $data): RPCMessage
+    public function castValidatedData(mixed $data): RPCMessage
     {
         return new RPCMessage(
             $this->rpcMethodValidator->castToRPCMethod($data[0]),
@@ -39,7 +33,7 @@ readonly class RPCMessageValidator extends InputValidator
         );
     }
 
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         return new JsonSchema([
             'type' => 'array',
diff --git a/src/InputValidatorController.php b/src/InputValidatorController.php
new file mode 100644
index 0000000000000000000000000000000000000000..8793da2b8190917e442e7755fc7b05811af44679
--- /dev/null
+++ b/src/InputValidatorController.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Distantmagic\Resonance\Attribute\Singleton;
+
+#[Singleton]
+readonly class InputValidatorController
+{
+    public function __construct(private JsonSchemaValidator $jsonSchemaValidator) {}
+
+    /**
+     * @template TCastedData of InputValidatedData
+     * @template TValidatedData
+     *
+     * @param InputValidator<TCastedData,TValidatedData> $inputValidator
+     *
+     * @return InputValidationResult<TCastedData>
+     */
+    public function validateData(InputValidator $inputValidator, mixed $data): InputValidationResult
+    {
+        $jsonSchemaValidationResult = $this->jsonSchemaValidator->validate($inputValidator, $data);
+
+        return $this->castJsonSchemaValidationResult($inputValidator, $jsonSchemaValidationResult);
+    }
+
+    /**
+     * @template TCastedData of InputValidatedData
+     * @template TValidatedData
+     *
+     * @param InputValidator<TCastedData,TValidatedData> $inputValidator
+     * @param JsonSchemaValidationResult<TValidatedData> $jsonSchemaValidationResult
+     *
+     * @return InputValidationResult<TCastedData>
+     */
+    protected function castJsonSchemaValidationResult(
+        InputValidator $inputValidator,
+        JsonSchemaValidationResult $jsonSchemaValidationResult,
+    ): InputValidationResult {
+        $errors = $jsonSchemaValidationResult->errors;
+
+        if (empty($errors)) {
+            return new InputValidationResult($inputValidator->castValidatedData($jsonSchemaValidationResult->data));
+        }
+
+        /**
+         * @var InputValidationResult<TCastedData>
+         */
+        $validationResult = new InputValidationResult();
+
+        foreach ($errors as $propertyName => $propertyErrors) {
+            $validationResult->errors->put(
+                $propertyName,
+                implode("\n", $propertyErrors),
+            );
+        }
+
+        return $validationResult;
+    }
+}
diff --git a/src/JsonSchemaSourceInterface.php b/src/JsonSchemaSourceInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ec9a2a3d8bc559e428e0e5597ceba9848aff80e
--- /dev/null
+++ b/src/JsonSchemaSourceInterface.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+interface JsonSchemaSourceInterface
+{
+    public function getSchema(): JsonSchema;
+}
diff --git a/src/JsonSchemaValidationErrorMessage.php b/src/JsonSchemaValidationErrorMessage.php
new file mode 100644
index 0000000000000000000000000000000000000000..994ba6f4b975ba169be6c382d7a869cd94156eda
--- /dev/null
+++ b/src/JsonSchemaValidationErrorMessage.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Stringable;
+
+readonly class JsonSchemaValidationErrorMessage implements Stringable
+{
+    public string $message;
+
+    /**
+     * @param array<string,array<string>> $errors
+     */
+    public function __construct(array $errors)
+    {
+        $messages = [];
+
+        foreach ($errors as $propertyName => $propertyErrors) {
+            foreach ($propertyErrors as $propertyError) {
+                $messages[] = sprintf('"%s": "%s"', $propertyName, $propertyError);
+            }
+        }
+
+        $this->message = sprintf(
+            "Encountered validation errors:\n-> %s",
+            implode("\n-> ", $messages)
+        );
+    }
+
+    public function __toString(): string
+    {
+        return $this->message;
+    }
+}
diff --git a/src/JsonSchemaValidationException.php b/src/JsonSchemaValidationException.php
new file mode 100644
index 0000000000000000000000000000000000000000..3c2be17594ddcf501028f3d6e3c0b8b863bdbbde
--- /dev/null
+++ b/src/JsonSchemaValidationException.php
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use RuntimeException;
+
+class JsonSchemaValidationException extends RuntimeException
+{
+    /**
+     * @param array<string,array<string>> $errors
+     */
+    public function __construct(array $errors)
+    {
+        parent::__construct((string) new JsonSchemaValidationErrorMessage($errors));
+    }
+}
diff --git a/src/JsonSchemaValidator.php b/src/JsonSchemaValidator.php
index ed0a080d49768e2fb2fa2f3f28f3961665db950b..8abcef8c0d264ad166ca65ca5d9d06d4814251ed 100644
--- a/src/JsonSchemaValidator.php
+++ b/src/JsonSchemaValidator.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance;
 
 use Distantmagic\Resonance\Attribute\Singleton;
+use Ds\Map;
 use Opis\JsonSchema\Errors\ErrorFormatter;
 use Opis\JsonSchema\Helper;
 use Opis\JsonSchema\ValidationResult;
@@ -13,21 +14,24 @@ use Opis\JsonSchema\Validator;
 #[Singleton]
 readonly class JsonSchemaValidator
 {
+    /**
+     * @var Map<JsonSchemaSourceInterface,bool|object|string>
+     */
+    private Map $convertedSchemas;
+
     private ErrorFormatter $errorFormatter;
     private Validator $validator;
 
     public function __construct()
     {
+        $this->convertedSchemas = new Map();
         $this->errorFormatter = new ErrorFormatter();
         $this->validator = new Validator();
     }
 
-    public function validate(JsonSchema $jsonSchema, mixed $data): JsonSchemaValidationResult
+    public function validate(JsonSchemaSourceInterface $jsonSchemaSource, mixed $data): JsonSchemaValidationResult
     {
-        /**
-         * @var bool|object|string $convertedSchema
-         */
-        $convertedSchema = Helper::toJSON($jsonSchema->schema);
+        $convertedSchema = $this->convertSchema($jsonSchemaSource);
 
         /**
          * @var bool|object|string $convertedData
@@ -42,6 +46,22 @@ readonly class JsonSchemaValidator
         );
     }
 
+    private function convertSchema(JsonSchemaSourceInterface $jsonSchemaSource): bool|object|string
+    {
+        if ($this->convertedSchemas->hasKey($jsonSchemaSource)) {
+            return $this->convertedSchemas->get($jsonSchemaSource);
+        }
+
+        /**
+         * @var bool|object|string $convertedSchema
+         */
+        $convertedSchema = Helper::toJSON($jsonSchemaSource->getSchema()->schema);
+
+        $this->convertedSchemas->put($jsonSchemaSource, $convertedSchema);
+
+        return $convertedSchema;
+    }
+
     /**
      * @return array<string,array<string>>
      */
diff --git a/src/OpenAPIRouteRequestBodyContentExtractor/ValidatedRequestExtractor.php b/src/OpenAPIRouteRequestBodyContentExtractor/ValidatedRequestExtractor.php
index 2e6c2ba235ee6f2d2721429437b5385116891f36..4bbdb52a7f5d5d8511dc531d0752a517705b17dd 100644
--- a/src/OpenAPIRouteRequestBodyContentExtractor/ValidatedRequestExtractor.php
+++ b/src/OpenAPIRouteRequestBodyContentExtractor/ValidatedRequestExtractor.php
@@ -36,7 +36,7 @@ readonly class ValidatedRequestExtractor extends OpenAPIRouteRequestBodyContentE
                     ->inputValidatorCollection
                     ->inputValidators
                     ->get($attribute->validator)
-                    ->jsonSchema,
+                    ->getSchema(),
             ),
         ];
     }
diff --git a/src/RPCNotification.php b/src/RPCNotification.php
index 7ca2c4aa0d4f4c587a7816cf2056d502e0ca0ee8..e99f1eb611dc233637d49428fbb8fe25f210f766 100644
--- a/src/RPCNotification.php
+++ b/src/RPCNotification.php
@@ -13,6 +13,9 @@ use Stringable;
  */
 readonly class RPCNotification implements Stringable
 {
+    /**
+     * @param TPayload $payload
+     */
     public function __construct(
         public RPCMethodInterface $method,
         public mixed $payload,
diff --git a/src/SingletonProvider/ConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider.php
index 6a2fc300da1d4f718c3bef4f9dd9139babcf5b89..180e202d6f965ca4daf617df1a6d7e1f7fe17882 100644
--- a/src/SingletonProvider/ConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider.php
@@ -5,13 +5,13 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\SingletonProvider;
 
 use Distantmagic\Resonance\ConfigurationFile;
-use Distantmagic\Resonance\JsonSchema;
+use Distantmagic\Resonance\JsonSchemaSourceInterface;
+use Distantmagic\Resonance\JsonSchemaValidationException;
 use Distantmagic\Resonance\JsonSchemaValidationResult;
 use Distantmagic\Resonance\JsonSchemaValidator;
 use Distantmagic\Resonance\PHPProjectFiles;
 use Distantmagic\Resonance\SingletonContainer;
 use Distantmagic\Resonance\SingletonProvider;
-use LogicException;
 
 /**
  * @template TObject of object
@@ -19,14 +19,10 @@ use LogicException;
  *
  * @template-extends SingletonProvider<TObject>
  */
-abstract readonly class ConfigurationProvider extends SingletonProvider
+abstract readonly class ConfigurationProvider extends SingletonProvider implements JsonSchemaSourceInterface
 {
-    private JsonSchema $jsonSchema;
-
     abstract protected function getConfigurationKey(): string;
 
-    abstract protected function makeSchema(): JsonSchema;
-
     /**
      * @param TSchema $validatedData
      *
@@ -37,9 +33,7 @@ abstract readonly class ConfigurationProvider extends SingletonProvider
     public function __construct(
         private ConfigurationFile $configurationFile,
         private JsonSchemaValidator $jsonSchemaValidator,
-    ) {
-        $this->jsonSchema = $this->makeSchema();
-    }
+    ) {}
 
     /**
      * @return TObject
@@ -54,7 +48,7 @@ abstract readonly class ConfigurationProvider extends SingletonProvider
         /**
          * @var JsonSchemaValidationResult<TSchema>
          */
-        $jsonSchemaValidationResult = $this->jsonSchemaValidator->validate($this->jsonSchema, $data);
+        $jsonSchemaValidationResult = $this->jsonSchemaValidator->validate($this, $data);
 
         $errors = $jsonSchemaValidationResult->errors;
 
@@ -62,18 +56,7 @@ abstract readonly class ConfigurationProvider extends SingletonProvider
             return $this->provideConfiguration($jsonSchemaValidationResult->data);
         }
 
-        $messages = [];
-
-        foreach ($errors as $propertyName => $propertyErrors) {
-            foreach ($propertyErrors as $propertyError) {
-                $messages[] = sprintf('"%s": "%s"', $propertyName, $propertyError);
-            }
-        }
-
-        throw new LogicException(sprintf(
-            "Encountered validation errors:\n-> %s",
-            implode("\n-> ", $messages)
-        ));
+        throw new JsonSchemaValidationException($errors);
     }
 
     public function shouldRegister(): bool
diff --git a/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php
index 2ce54d5a49ea2883d15ab3e584e65fdfa2286372..0495bcc1e747195e264a875acea866c88f4bf7c0 100644
--- a/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php
@@ -21,12 +21,7 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 #[Singleton(provides: ApplicationConfiguration::class)]
 final readonly class ApplicationConfigurationProvider extends ConfigurationProvider
 {
-    protected function getConfigurationKey(): string
-    {
-        return 'app';
-    }
-
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         return new JsonSchema([
             'type' => 'object',
@@ -55,6 +50,11 @@ final readonly class ApplicationConfigurationProvider extends ConfigurationProvi
         ]);
     }
 
+    protected function getConfigurationKey(): string
+    {
+        return 'app';
+    }
+
     protected function provideConfiguration($validatedData): ApplicationConfiguration
     {
         return new ApplicationConfiguration(
diff --git a/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php
index b04319d15fdce8e47fc631426e097564127fbff8..cceea18dffc86945d10f9d1d0b95bd19f273d468 100644
--- a/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php
@@ -31,12 +31,7 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 #[Singleton(provides: DatabaseConfiguration::class)]
 final readonly class DatabaseConfigurationProvider extends ConfigurationProvider
 {
-    protected function getConfigurationKey(): string
-    {
-        return 'database';
-    }
-
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         $valueSchema = [
             'type' => 'object',
@@ -99,6 +94,11 @@ final readonly class DatabaseConfigurationProvider extends ConfigurationProvider
         ]);
     }
 
+    protected function getConfigurationKey(): string
+    {
+        return 'database';
+    }
+
     protected function provideConfiguration($validatedData): DatabaseConfiguration
     {
         $databaseconfiguration = new DatabaseConfiguration();
diff --git a/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php
index 9ba541cf38a7dc161167b1950cea17a9078e310d..8964da359829808f194363d8e95eefc6df3cd014 100644
--- a/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php
@@ -21,12 +21,7 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 #[Singleton(provides: LlamaCppConfiguration::class)]
 final readonly class LlamaCppConfigurationProvider extends ConfigurationProvider
 {
-    protected function getConfigurationKey(): string
-    {
-        return 'llamacpp';
-    }
-
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         return new JsonSchema([
             'type' => 'object',
@@ -59,6 +54,11 @@ final readonly class LlamaCppConfigurationProvider extends ConfigurationProvider
         ]);
     }
 
+    protected function getConfigurationKey(): string
+    {
+        return 'llamacpp';
+    }
+
     protected function provideConfiguration($validatedData): LlamaCppConfiguration
     {
         return new LlamaCppConfiguration(
diff --git a/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php
index 58fab3f8a6128f37c09fec642114600c83af025d..f5b45cd6de5448a376c3ec82b7529aab7cd0bac1 100644
--- a/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php
@@ -31,12 +31,7 @@ use Swoole\Coroutine;
 )]
 final readonly class OAuth2ConfigurationProvider extends ConfigurationProvider
 {
-    protected function getConfigurationKey(): string
-    {
-        return 'oauth2';
-    }
-
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         return new JsonSchema([
             'type' => 'object',
@@ -77,6 +72,11 @@ final readonly class OAuth2ConfigurationProvider extends ConfigurationProvider
         ]);
     }
 
+    protected function getConfigurationKey(): string
+    {
+        return 'oauth2';
+    }
+
     protected function provideConfiguration($validatedData): OAuth2Configuration
     {
         $encryptionKeyContent = Coroutine::readFile($validatedData->encryption_key);
diff --git a/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php
index 3014813cd2292b79205a9f518e34d1657960318f..9cf75ada3103738babc6331f147e8f1b830e6b72 100644
--- a/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php
@@ -19,12 +19,7 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 #[Singleton(provides: OpenAPIConfiguration::class)]
 final readonly class OpenAPIConfigurationProvider extends ConfigurationProvider
 {
-    protected function getConfigurationKey(): string
-    {
-        return 'openapi';
-    }
-
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         return new JsonSchema([
             'type' => 'object',
@@ -46,6 +41,11 @@ final readonly class OpenAPIConfigurationProvider extends ConfigurationProvider
         ]);
     }
 
+    protected function getConfigurationKey(): string
+    {
+        return 'openapi';
+    }
+
     protected function provideConfiguration($validatedData): OpenAPIConfiguration
     {
         return new OpenAPIConfiguration(
diff --git a/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php
index 3f8c1e7aa56bbff7d8620dd62ddb206fa04906c4..9a93a5143c063c4f935b230ee730fb231ae991ad 100644
--- a/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php
@@ -28,12 +28,7 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 #[Singleton(provides: RedisConfiguration::class)]
 final readonly class RedisConfigurationProvider extends ConfigurationProvider
 {
-    protected function getConfigurationKey(): string
-    {
-        return 'redis';
-    }
-
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         $valueSchema = [
             'type' => 'object',
@@ -88,6 +83,11 @@ final readonly class RedisConfigurationProvider extends ConfigurationProvider
         ]);
     }
 
+    protected function getConfigurationKey(): string
+    {
+        return 'redis';
+    }
+
     protected function provideConfiguration($validatedData): RedisConfiguration
     {
         $databaseconfiguration = new RedisConfiguration();
diff --git a/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php
index 665109350e0340b875aa11ab6fc01ea530a802f0..7041902c71d8ca1fef640a53f2bc77ac9b3b99fe 100644
--- a/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php
@@ -31,12 +31,7 @@ final readonly class SessionConfigurationProvider extends ConfigurationProvider
         parent::__construct($configurationFile, $jsonSchemaValidator);
     }
 
-    protected function getConfigurationKey(): string
-    {
-        return 'session';
-    }
-
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         $redisConnectionPools = $this
             ->redisConfiguration
@@ -70,6 +65,11 @@ final readonly class SessionConfigurationProvider extends ConfigurationProvider
         ]);
     }
 
+    protected function getConfigurationKey(): string
+    {
+        return 'session';
+    }
+
     protected function provideConfiguration($validatedData): SessionConfiguration
     {
         return new SessionConfiguration(
diff --git a/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php
index 02c121c54d2daec13d0902807bbbfec58f95c7fc..e1c354fe122f805dd2bd7623390da665027e18e1 100644
--- a/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php
@@ -21,12 +21,7 @@ use Distantmagic\Resonance\StaticPageConfiguration;
 #[Singleton(provides: StaticPageConfiguration::class)]
 final readonly class StaticPageConfigurationProvider extends ConfigurationProvider
 {
-    protected function getConfigurationKey(): string
-    {
-        return 'static';
-    }
-
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         return new JsonSchema([
             'type' => 'object',
@@ -56,6 +51,11 @@ final readonly class StaticPageConfigurationProvider extends ConfigurationProvid
         ]);
     }
 
+    protected function getConfigurationKey(): string
+    {
+        return 'static';
+    }
+
     protected function provideConfiguration($validatedData): StaticPageConfiguration
     {
         return new StaticPageConfiguration(
diff --git a/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php
index ade316341eaf772262b4d7415b7c8ce46a346ebe..83abf24376cdd4fbd56a9093d63cebafc4fa7d9e 100644
--- a/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php
@@ -22,12 +22,7 @@ use Distantmagic\Resonance\SwooleConfiguration;
 #[Singleton(provides: SwooleConfiguration::class)]
 final readonly class SwooleConfigurationProvider extends ConfigurationProvider
 {
-    protected function getConfigurationKey(): string
-    {
-        return 'swoole';
-    }
-
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         return new JsonSchema([
             'type' => 'object',
@@ -62,6 +57,11 @@ final readonly class SwooleConfigurationProvider extends ConfigurationProvider
         ]);
     }
 
+    protected function getConfigurationKey(): string
+    {
+        return 'swoole';
+    }
+
     protected function provideConfiguration($validatedData): SwooleConfiguration
     {
         return new SwooleConfiguration(
diff --git a/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php
index 01519a55d38795ceb974aefcb1fa8f1d87a9b7aa..a3d15f896ef6827595a6c59404d56f1c0f792cbe 100644
--- a/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php
@@ -18,12 +18,7 @@ use Distantmagic\Resonance\TranslatorConfiguration;
 #[Singleton(provides: TranslatorConfiguration::class)]
 final readonly class TranslatorConfigurationProvider extends ConfigurationProvider
 {
-    protected function getConfigurationKey(): string
-    {
-        return 'translator';
-    }
-
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         return new JsonSchema([
             'type' => 'object',
@@ -41,6 +36,11 @@ final readonly class TranslatorConfigurationProvider extends ConfigurationProvid
         ]);
     }
 
+    protected function getConfigurationKey(): string
+    {
+        return 'translator';
+    }
+
     protected function provideConfiguration($validatedData): TranslatorConfiguration
     {
         return new TranslatorConfiguration(
diff --git a/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php
index a49ed0651644b30f02c5e37c2924e7489270d1bb..0dd57855936f52d8f223ca698640cb5aab5f11c9 100644
--- a/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php
@@ -21,12 +21,7 @@ use Distantmagic\Resonance\WebSocketConfiguration;
 )]
 final readonly class WebSocketConfigurationProvider extends ConfigurationProvider
 {
-    protected function getConfigurationKey(): string
-    {
-        return 'swoole';
-    }
-
-    protected function makeSchema(): JsonSchema
+    public function getSchema(): JsonSchema
     {
         return new JsonSchema([
             'type' => 'object',
@@ -41,6 +36,11 @@ final readonly class WebSocketConfigurationProvider extends ConfigurationProvide
         ]);
     }
 
+    protected function getConfigurationKey(): string
+    {
+        return 'swoole';
+    }
+
     protected function provideConfiguration($validatedData): WebSocketConfiguration
     {
         return new WebSocketConfiguration(
diff --git a/src/SingletonProvider/StaticPageAggregateProvider.php b/src/SingletonProvider/StaticPageAggregateProvider.php
index 075a03fcd3942179d6b9572e4eb8a4e0dca791c6..d297c8df49a1dacddcc6dc9073dcbf702383735d 100644
--- a/src/SingletonProvider/StaticPageAggregateProvider.php
+++ b/src/SingletonProvider/StaticPageAggregateProvider.php
@@ -6,6 +6,7 @@ namespace Distantmagic\Resonance\SingletonProvider;
 
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\InputValidator\FrontMatterValidator;
+use Distantmagic\Resonance\InputValidatorController;
 use Distantmagic\Resonance\PHPProjectFiles;
 use Distantmagic\Resonance\SingletonContainer;
 use Distantmagic\Resonance\SingletonProvider;
@@ -22,6 +23,7 @@ final readonly class StaticPageAggregateProvider extends SingletonProvider
 {
     public function __construct(
         private FrontMatterValidator $frontMatterValidator,
+        private InputValidatorController $inputValidatorController,
         private StaticPageConfiguration $staticPageConfiguration,
     ) {}
 
@@ -30,6 +32,7 @@ final readonly class StaticPageAggregateProvider extends SingletonProvider
         $fileIterator = new StaticPageFileIterator($this->staticPageConfiguration->inputDirectory);
         $staticPageIterator = new StaticPageIterator(
             $this->frontMatterValidator,
+            $this->inputValidatorController,
             $fileIterator,
             $this->staticPageConfiguration->outputDirectory,
         );
diff --git a/src/StaticPageIterator.php b/src/StaticPageIterator.php
index 9311cc8e09d4e732cf883d75233021998cc1d572..30cb72ce950c25be947284d95023dd1ec16583d3 100644
--- a/src/StaticPageIterator.php
+++ b/src/StaticPageIterator.php
@@ -24,6 +24,7 @@ readonly class StaticPageIterator implements IteratorAggregate
 
     public function __construct(
         private FrontMatterValidator $frontMatterValidator,
+        private InputValidatorController $inputValidatorController,
         private StaticPageFileIterator $fileIterator,
         private string $staticPagesOutputDirectory,
     ) {
@@ -75,7 +76,10 @@ readonly class StaticPageIterator implements IteratorAggregate
             throw new StaticPageFileException($file, 'File does not have a front matter');
         }
 
-        $inputValidationResult = $this->frontMatterValidator->validateData($frontMatter);
+        $inputValidationResult = $this->inputValidatorController->validateData(
+            $this->frontMatterValidator,
+            $frontMatter,
+        );
 
         if ($inputValidationResult->inputValidatedData) {
             return $inputValidationResult->inputValidatedData;
diff --git a/src/WebSocketProtocolController/RPCProtocolController.php b/src/WebSocketProtocolController/RPCProtocolController.php
index c6000eef9431c8659e2bc405c57b8b7c6035a8cf..6e7336ec7908ebc19ee231a86f1630dc821c84fd 100644
--- a/src/WebSocketProtocolController/RPCProtocolController.php
+++ b/src/WebSocketProtocolController/RPCProtocolController.php
@@ -11,6 +11,9 @@ use Distantmagic\Resonance\CSRFManager;
 use Distantmagic\Resonance\Feature;
 use Distantmagic\Resonance\Gatekeeper;
 use Distantmagic\Resonance\InputValidator\RPCMessageValidator;
+use Distantmagic\Resonance\InputValidatorController;
+use Distantmagic\Resonance\JsonSchemaValidationErrorMessage;
+use Distantmagic\Resonance\JsonSchemaValidator;
 use Distantmagic\Resonance\JsonSerializer;
 use Distantmagic\Resonance\SingletonCollection;
 use Distantmagic\Resonance\SiteAction;
@@ -48,6 +51,8 @@ final readonly class RPCProtocolController extends WebSocketProtocolController
         private CSRFManager $csrfManager,
         private AuthenticatedUserStoreAggregate $authenticatedUserSourceAggregate,
         private Gatekeeper $gatekeeper,
+        private InputValidatorController $inputValidatorController,
+        private JsonSchemaValidator $jsonSchemaValidator,
         private JsonSerializer $jsonSerializer,
         private LoggerInterface $logger,
         private RPCMessageValidator $rpcMessageValidator,
@@ -120,6 +125,7 @@ final readonly class RPCProtocolController extends WebSocketProtocolController
     {
         $webSocketConnection = new WebSocketConnection($server, $fd);
         $connectionHandle = new WebSocketRPCConnectionHandle(
+            $this->jsonSchemaValidator,
             $this->webSocketRPCResponderAggregate,
             $webSocketAuthResolution,
             $webSocketConnection,
@@ -152,15 +158,28 @@ final readonly class RPCProtocolController extends WebSocketProtocolController
 
     private function onJsonMessage(Server $server, Frame $frame, mixed $jsonMessage): void
     {
-        $inputValidationResult = $this->rpcMessageValidator->validateData($jsonMessage);
-
-        if ($inputValidationResult->inputValidatedData) {
-            $this
-                ->getFrameController($frame)
-                ->onRPCMessage($inputValidationResult->inputValidatedData)
-            ;
-        } else {
+        $inputValidationResult = $this->inputValidatorController->validateData(
+            $this->rpcMessageValidator,
+            $jsonMessage
+        );
+
+        if (!$inputValidationResult->inputValidatedData) {
             $this->onProtocolError($server, $frame, $inputValidationResult->getErrorMessage());
+
+            return;
+        }
+
+        $payloadValidationResult = $this
+            ->getFrameController($frame)
+            ->onRPCMessage($inputValidationResult->inputValidatedData)
+        ;
+
+        if (!empty($payloadValidationResult->errors)) {
+            $this->onProtocolError(
+                $server,
+                $frame,
+                (string) new JsonSchemaValidationErrorMessage($payloadValidationResult->errors),
+            );
         }
     }
 
diff --git a/src/WebSocketRPCConnectionHandle.php b/src/WebSocketRPCConnectionHandle.php
index c001426eb3bcb3e69bb858098a9f38016daca457..27a2b084380f092ffddb682bbb80e4af1aebf205 100644
--- a/src/WebSocketRPCConnectionHandle.php
+++ b/src/WebSocketRPCConnectionHandle.php
@@ -15,6 +15,7 @@ readonly class WebSocketRPCConnectionHandle
     private Set $activeResponders;
 
     public function __construct(
+        public JsonSchemaValidator $jsonSchemaValidator,
         public WebSocketRPCResponderAggregate $webSocketRPCResponderAggregate,
         public WebSocketAuthResolution $webSocketAuthResolution,
         public WebSocketConnection $webSocketConnection,
@@ -25,6 +26,10 @@ readonly class WebSocketRPCConnectionHandle
     public function onClose(): void
     {
         foreach ($this->activeResponders as $responder) {
+            $responder->onBeforeMessage(
+                $this->webSocketAuthResolution,
+                $this->webSocketConnection,
+            );
             $responder->onClose(
                 $this->webSocketAuthResolution,
                 $this->webSocketConnection,
@@ -32,19 +37,50 @@ readonly class WebSocketRPCConnectionHandle
         }
     }
 
-    public function onRPCMessage(RPCMessage $rpcMessage): void
+    public function onRPCMessage(RPCMessage $rpcMessage): JsonSchemaValidationResult
     {
         $responder = $this
             ->webSocketRPCResponderAggregate
             ->selectResponder($rpcMessage)
         ;
 
+        $jsonSchemaValidationResult = $this
+            ->jsonSchemaValidator
+            ->validate($responder, $rpcMessage->payload)
+        ;
+
+        if (!empty($jsonSchemaValidationResult->errors)) {
+            return $jsonSchemaValidationResult;
+        }
+
         $this->activeResponders->add($responder);
 
-        $responder->respond(
+        $responder->onBeforeMessage(
             $this->webSocketAuthResolution,
             $this->webSocketConnection,
-            $rpcMessage
         );
+
+        if (is_string($rpcMessage->requestId)) {
+            $responder->onRequest(
+                $this->webSocketAuthResolution,
+                $this->webSocketConnection,
+                new RPCRequest(
+                    $rpcMessage->method,
+                    $jsonSchemaValidationResult->data,
+                    $rpcMessage->requestId,
+                ),
+            );
+        } else {
+            $responder->onNotification(
+                $this->webSocketAuthResolution,
+                $this->webSocketConnection,
+                new RPCNotification(
+                    $rpcMessage->method,
+                    $jsonSchemaValidationResult->data,
+                )
+            );
+        }
+
+        return $jsonSchemaValidationResult;
     }
 }
diff --git a/src/WebSocketRPCResponder.php b/src/WebSocketRPCResponder.php
index 25c74bfc1a11d5d849a83927cb02268d7badc7a3..995d6295dd646d07b38ab5b10059efa48e19b6b5 100644
--- a/src/WebSocketRPCResponder.php
+++ b/src/WebSocketRPCResponder.php
@@ -4,10 +4,14 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
-use Distantmagic\Resonance\InputValidatedData\RPCMessage;
 use Distantmagic\Resonance\WebSocketProtocolException\UnexpectedNotification;
 use Distantmagic\Resonance\WebSocketProtocolException\UnexpectedRequest;
 
+/**
+ * @template TPayload
+ *
+ * @template-implements WebSocketRPCResponderInterface<TPayload>
+ */
 abstract readonly class WebSocketRPCResponder implements WebSocketRPCResponderInterface
 {
     public function onClose(
@@ -15,29 +19,7 @@ abstract readonly class WebSocketRPCResponder implements WebSocketRPCResponderIn
         WebSocketConnection $webSocketConnection,
     ): void {}
 
-    public function respond(
-        WebSocketAuthResolution $webSocketAuthResolution,
-        WebSocketConnection $webSocketConnection,
-        RPCMessage $rpcMessage,
-    ): void {
-        if (is_string($rpcMessage->requestId)) {
-            $this->onRequest(
-                $webSocketAuthResolution,
-                $webSocketConnection,
-                new RPCRequest($rpcMessage->method, $rpcMessage->payload, $rpcMessage->requestId),
-            );
-
-            return;
-        }
-
-        $this->onNotification(
-            $webSocketAuthResolution,
-            $webSocketConnection,
-            new RPCNotification($rpcMessage->method, $rpcMessage->payload)
-        );
-    }
-
-    protected function onNotification(
+    public function onNotification(
         WebSocketAuthResolution $webSocketAuthResolution,
         WebSocketConnection $webSocketConnection,
         RPCNotification $rpcNotification,
@@ -45,7 +27,7 @@ abstract readonly class WebSocketRPCResponder implements WebSocketRPCResponderIn
         throw new UnexpectedNotification($rpcNotification->method);
     }
 
-    protected function onRequest(
+    public function onRequest(
         WebSocketAuthResolution $webSocketAuthResolution,
         WebSocketConnection $webSocketConnection,
         RPCRequest $rpcRequest,
diff --git a/src/WebSocketRPCResponderInterface.php b/src/WebSocketRPCResponderInterface.php
index afca1afdce1e6acb4f54fa299f1b7c7d956d938e..e88f71b2b58b1870f642965cdebf18ddfd63576d 100644
--- a/src/WebSocketRPCResponderInterface.php
+++ b/src/WebSocketRPCResponderInterface.php
@@ -4,18 +4,36 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
-use Distantmagic\Resonance\InputValidatedData\RPCMessage;
-
-interface WebSocketRPCResponderInterface
+/**
+ * @template TPayload
+ */
+interface WebSocketRPCResponderInterface extends JsonSchemaSourceInterface
 {
+    public function onBeforeMessage(
+        WebSocketAuthResolution $webSocketAuthResolution,
+        WebSocketConnection $webSocketConnection,
+    ): void;
+
     public function onClose(
         WebSocketAuthResolution $webSocketAuthResolution,
         WebSocketConnection $webSocketConnection,
     ): void;
 
-    public function respond(
+    /**
+     * @param RPCNotification<TPayload> $rpcNotification
+     */
+    public function onNotification(
+        WebSocketAuthResolution $webSocketAuthResolution,
+        WebSocketConnection $webSocketConnection,
+        RPCNotification $rpcNotification,
+    ): void;
+
+    /**
+     * @param RPCRequest<TPayload> $rpcRequest
+     */
+    public function onRequest(
         WebSocketAuthResolution $webSocketAuthResolution,
         WebSocketConnection $webSocketConnection,
-        RPCMessage $rpcMessage,
+        RPCRequest $rpcRequest,
     ): void;
 }