From 4be3a4451823fb47234ba8b42e53a16f54d0e013 Mon Sep 17 00:00:00 2001
From: Mateusz Charytoniuk <mateusz.charytoniuk@protonmail.com>
Date: Tue, 6 Feb 2024 09:07:49 +0100
Subject: [PATCH] chore: move configs to constraints schema

---
 docs/pages/index.md                           |   6 +-
 src/Constraint/AnyConstraint.php              |   2 +-
 src/Constraint/BooleanConstraint.php          |  68 +++++++++
 src/Constraint/BooleanConstraintTest.php      |  51 +++++++
 src/Constraint/ConstConstraint.php            |   2 +-
 src/Constraint/EnumConstraint.php             |   4 +-
 src/Constraint/IntegerConstraint.php          |   6 +-
 src/Constraint/IntegerConstraintTest.php      |  10 +-
 src/Constraint/MapConstraint.php              | 131 ++++++++++++++++++
 src/Constraint/MapConstraintTest.php          |  89 ++++++++++++
 src/Constraint/NumberConstraint.php           |   2 +-
 src/Constraint/StringConstraint.php           |   2 +-
 src/ConstraintSourceInterface.php             |  10 ++
 src/ConstraintValidationException.php         |  30 ++++
 .../ConfigurationProvider.php                 |  28 ++--
 .../ApplicationConfigurationProvider.php      |  48 +++----
 .../DatabaseConfigurationProvider.php         | 103 +++++---------
 .../LlamaCppConfigurationProvider.php         |  57 +++-----
 .../MailerConfigurationProvider.php           |  71 +++-------
 .../OAuth2ConfigurationProvider.php           |  72 ++++------
 .../OpenAPIConfigurationProvider.php          |  37 ++---
 .../RedisConfigurationProvider.php            |  90 ++++--------
 .../SQLiteVSSConfigurationProvider.php        |  32 ++---
 .../SessionConfigurationProvider.php          |  51 +++----
 .../StaticPageConfigurationProvider.php       |  53 +++----
 .../SwooleConfigurationProvider.php           |  77 ++++------
 .../TranslatorConfigurationProvider.php       |  30 ++--
 .../WebSocketConfigurationProvider.php        |  27 ++--
 28 files changed, 672 insertions(+), 517 deletions(-)
 create mode 100644 src/Constraint/BooleanConstraint.php
 create mode 100644 src/Constraint/BooleanConstraintTest.php
 create mode 100644 src/Constraint/MapConstraint.php
 create mode 100644 src/Constraint/MapConstraintTest.php
 create mode 100644 src/ConstraintSourceInterface.php
 create mode 100644 src/ConstraintValidationException.php

diff --git a/docs/pages/index.md b/docs/pages/index.md
index 93f35f48..ff1078ac 100644
--- a/docs/pages/index.md
+++ b/docs/pages/index.md
@@ -105,11 +105,9 @@ description: >
 #[Singleton(collection: SingletonCollection::WebSocketRPCResponder)]
 final readonly class EchoResponder extends WebSocketRPCResponder
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        return new JsonSchema([
-            'type' => 'string',
-        ]);
+        return new StringConstraint();
     }
 
     public function onRequest(
diff --git a/src/Constraint/AnyConstraint.php b/src/Constraint/AnyConstraint.php
index 0baab197..7fa48a30 100644
--- a/src/Constraint/AnyConstraint.php
+++ b/src/Constraint/AnyConstraint.php
@@ -12,7 +12,7 @@ use Distantmagic\Resonance\ConstraintResult;
 use Distantmagic\Resonance\ConstraintResultStatus;
 use stdClass;
 
-readonly class AnyConstraint extends Constraint
+final readonly class AnyConstraint extends Constraint
 {
     public function default(mixed $defaultValue): self
     {
diff --git a/src/Constraint/BooleanConstraint.php b/src/Constraint/BooleanConstraint.php
new file mode 100644
index 00000000..8b58e9aa
--- /dev/null
+++ b/src/Constraint/BooleanConstraint.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\Constraint;
+
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\ConstraintDefaultValue;
+use Distantmagic\Resonance\ConstraintPath;
+use Distantmagic\Resonance\ConstraintReason;
+use Distantmagic\Resonance\ConstraintResult;
+use Distantmagic\Resonance\ConstraintResultStatus;
+
+final readonly class BooleanConstraint extends Constraint
+{
+    public function default(mixed $defaultValue): self
+    {
+        return new self(
+            defaultValue: new ConstraintDefaultValue($defaultValue),
+            isNullable: $this->isNullable,
+            isRequired: $this->isRequired,
+        );
+    }
+
+    public function nullable(): self
+    {
+        return new self(
+            defaultValue: $this->defaultValue ?? new ConstraintDefaultValue(null),
+            isNullable: true,
+            isRequired: $this->isRequired,
+        );
+    }
+
+    public function optional(): self
+    {
+        return new self(
+            defaultValue: $this->defaultValue,
+            isNullable: $this->isNullable,
+            isRequired: false,
+        );
+    }
+
+    protected function doConvertToJsonSchema(): array
+    {
+        return [
+            'type' => 'boolean',
+        ];
+    }
+
+    protected function doValidate(mixed $notValidatedData, ConstraintPath $path): ConstraintResult
+    {
+        if (!is_bool($notValidatedData)) {
+            return new ConstraintResult(
+                castedData: $notValidatedData,
+                path: $path,
+                reason: ConstraintReason::InvalidDataType,
+                status: ConstraintResultStatus::Invalid,
+            );
+        }
+
+        return new ConstraintResult(
+            castedData: $notValidatedData,
+            path: $path,
+            reason: ConstraintReason::Ok,
+            status: ConstraintResultStatus::Valid,
+        );
+    }
+}
diff --git a/src/Constraint/BooleanConstraintTest.php b/src/Constraint/BooleanConstraintTest.php
new file mode 100644
index 00000000..ff422df1
--- /dev/null
+++ b/src/Constraint/BooleanConstraintTest.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\Constraint;
+
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversNothing
+ *
+ * @internal
+ */
+final class BooleanConstraintTest extends TestCase
+{
+    public function test_is_converted_optionally_to_json_schema(): void
+    {
+        $constraint = new BooleanConstraint();
+
+        self::assertEquals([
+            'type' => 'boolean',
+        ], $constraint->optional()->toJsonSchema());
+    }
+
+    public function test_is_converted_to_json_schema(): void
+    {
+        $constraint = new BooleanConstraint();
+
+        self::assertEquals([
+            'type' => 'boolean',
+        ], $constraint->toJsonSchema());
+    }
+
+    public function test_nullable_is_converted_to_json_schema(): void
+    {
+        $constraint = new BooleanConstraint();
+
+        self::assertEquals([
+            'type' => ['null', 'boolean'],
+            'default' => null,
+        ], $constraint->nullable()->toJsonSchema());
+    }
+
+    public function test_validates(): void
+    {
+        $constraint = new BooleanConstraint();
+
+        self::assertTrue($constraint->validate(false)->status->isValid());
+        self::assertFalse($constraint->validate(5.5)->status->isValid());
+    }
+}
diff --git a/src/Constraint/ConstConstraint.php b/src/Constraint/ConstConstraint.php
index 2bfc053b..9f1e3904 100644
--- a/src/Constraint/ConstConstraint.php
+++ b/src/Constraint/ConstConstraint.php
@@ -11,7 +11,7 @@ use Distantmagic\Resonance\ConstraintResult;
 use Distantmagic\Resonance\ConstraintResultStatus;
 use LogicException;
 
-readonly class ConstConstraint extends Constraint
+final readonly class ConstConstraint extends Constraint
 {
     /**
      * @param float|int|non-empty-string $constValue
diff --git a/src/Constraint/EnumConstraint.php b/src/Constraint/EnumConstraint.php
index bb14cc29..ee10aab8 100644
--- a/src/Constraint/EnumConstraint.php
+++ b/src/Constraint/EnumConstraint.php
@@ -11,10 +11,10 @@ use Distantmagic\Resonance\ConstraintReason;
 use Distantmagic\Resonance\ConstraintResult;
 use Distantmagic\Resonance\ConstraintResultStatus;
 
-readonly class EnumConstraint extends Constraint
+final readonly class EnumConstraint extends Constraint
 {
     /**
-     * @param array<non-empty-string> $values
+     * @param array<string>|list<string> $values
      */
     public function __construct(
         public array $values,
diff --git a/src/Constraint/IntegerConstraint.php b/src/Constraint/IntegerConstraint.php
index 764f0b45..624488f6 100644
--- a/src/Constraint/IntegerConstraint.php
+++ b/src/Constraint/IntegerConstraint.php
@@ -11,7 +11,7 @@ use Distantmagic\Resonance\ConstraintReason;
 use Distantmagic\Resonance\ConstraintResult;
 use Distantmagic\Resonance\ConstraintResultStatus;
 
-readonly class IntegerConstraint extends Constraint
+final readonly class IntegerConstraint extends Constraint
 {
     public function default(mixed $defaultValue): self
     {
@@ -49,7 +49,7 @@ readonly class IntegerConstraint extends Constraint
 
     protected function doValidate(mixed $notValidatedData, ConstraintPath $path): ConstraintResult
     {
-        if (!is_int($notValidatedData)) {
+        if (!is_numeric($notValidatedData) || (int) $notValidatedData != $notValidatedData) {
             return new ConstraintResult(
                 castedData: $notValidatedData,
                 path: $path,
@@ -59,7 +59,7 @@ readonly class IntegerConstraint extends Constraint
         }
 
         return new ConstraintResult(
-            castedData: $notValidatedData,
+            castedData: (int) $notValidatedData,
             path: $path,
             reason: ConstraintReason::Ok,
             status: ConstraintResultStatus::Valid,
diff --git a/src/Constraint/IntegerConstraintTest.php b/src/Constraint/IntegerConstraintTest.php
index 178148c4..1fb70bf4 100644
--- a/src/Constraint/IntegerConstraintTest.php
+++ b/src/Constraint/IntegerConstraintTest.php
@@ -41,11 +41,17 @@ final class IntegerConstraintTest extends TestCase
         ], $constraint->nullable()->toJsonSchema());
     }
 
-    public function test_validates(): void
+    public function test_validates_failure(): void
     {
         $constraint = new IntegerConstraint();
 
-        self::assertTrue($constraint->validate(5)->status->isValid());
         self::assertFalse($constraint->validate(5.5)->status->isValid());
     }
+
+    public function test_validates_ok(): void
+    {
+        $constraint = new IntegerConstraint();
+
+        self::assertTrue($constraint->validate(5)->status->isValid());
+    }
 }
diff --git a/src/Constraint/MapConstraint.php b/src/Constraint/MapConstraint.php
new file mode 100644
index 00000000..837067b6
--- /dev/null
+++ b/src/Constraint/MapConstraint.php
@@ -0,0 +1,131 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\Constraint;
+
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\ConstraintDefaultValue;
+use Distantmagic\Resonance\ConstraintPath;
+use Distantmagic\Resonance\ConstraintReason;
+use Distantmagic\Resonance\ConstraintResult;
+use Distantmagic\Resonance\ConstraintResultStatus;
+
+final readonly class MapConstraint extends Constraint
+{
+    public function __construct(
+        public Constraint $valueConstraint,
+        ?ConstraintDefaultValue $defaultValue = null,
+        bool $isNullable = false,
+        bool $isRequired = true,
+    ) {
+        parent::__construct(
+            defaultValue: $defaultValue,
+            isNullable: $isNullable,
+            isRequired: $isRequired,
+        );
+    }
+
+    public function default(mixed $defaultValue): self
+    {
+        return new self(
+            valueConstraint: $this->valueConstraint,
+            defaultValue: new ConstraintDefaultValue($defaultValue),
+            isNullable: $this->isNullable,
+            isRequired: $this->isRequired,
+        );
+    }
+
+    public function nullable(): self
+    {
+        return new self(
+            valueConstraint: $this->valueConstraint,
+            defaultValue: $this->defaultValue ?? new ConstraintDefaultValue(null),
+            isNullable: true,
+            isRequired: $this->isRequired,
+        );
+    }
+
+    public function optional(): self
+    {
+        return new self(
+            valueConstraint: $this->valueConstraint,
+            defaultValue: $this->defaultValue,
+            isNullable: $this->isNullable,
+            isRequired: false,
+        );
+    }
+
+    protected function doConvertToJsonSchema(): array
+    {
+        return [
+            'type' => 'object',
+            'additionalProperties' => $this->valueConstraint->toJsonSchema(),
+        ];
+    }
+
+    protected function doValidate(mixed $notValidatedData, ConstraintPath $path): ConstraintResult
+    {
+        if (!is_array($notValidatedData)) {
+            return new ConstraintResult(
+                castedData: $notValidatedData,
+                path: $path,
+                reason: ConstraintReason::InvalidDataType,
+                status: ConstraintResultStatus::Invalid,
+            );
+        }
+
+        $ret = [];
+
+        /**
+         * @var list<ConstraintResult>
+         */
+        $invalidChildStatuses = [];
+
+        /**
+         * @var mixed $notValidatedKey explicitly mixed for typechecks
+         * @var mixed $notValidatedValue explicitly mixed for typechecks
+         */
+        foreach ($notValidatedData as $notValidatedKey => $notValidatedValue) {
+            if (!is_string($notValidatedKey)) {
+                $invalidChildStatuses[] = new ConstraintResult(
+                    castedData: null,
+                    path: $path,
+                    reason: ConstraintReason::InvalidDataType,
+                    status: ConstraintResultStatus::Invalid,
+                );
+            } else {
+                $childResult = $this->valueConstraint->validate(
+                    notValidatedData: $notValidatedValue,
+                    path: $path->fork($notValidatedKey)
+                );
+
+                if ($childResult->status->isValid()) {
+                    /**
+                     * @var mixed explicitly mixed for typechecks
+                     */
+                    $ret[$notValidatedKey] = $childResult->castedData;
+                } else {
+                    $invalidChildStatuses[] = $childResult;
+                }
+            }
+        }
+
+        if (!empty($invalidChildStatuses)) {
+            return new ConstraintResult(
+                castedData: $notValidatedData,
+                nested: $invalidChildStatuses,
+                path: $path,
+                reason: ConstraintReason::InvalidNestedConstraint,
+                status: ConstraintResultStatus::Invalid,
+            );
+        }
+
+        return new ConstraintResult(
+            castedData: $ret,
+            path: $path,
+            reason: ConstraintReason::Ok,
+            status: ConstraintResultStatus::Valid,
+        );
+    }
+}
diff --git a/src/Constraint/MapConstraintTest.php b/src/Constraint/MapConstraintTest.php
new file mode 100644
index 00000000..c63e5744
--- /dev/null
+++ b/src/Constraint/MapConstraintTest.php
@@ -0,0 +1,89 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\Constraint;
+
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversNothing
+ *
+ * @internal
+ */
+final class MapConstraintTest extends TestCase
+{
+    public function test_is_converted_optionally_to_json_schema(): void
+    {
+        $constraint = new MapConstraint(
+            valueConstraint: new StringConstraint()
+        );
+        self::assertEquals([
+            'type' => 'object',
+            'additionalProperties' => [
+                'type' => 'string',
+                'minLength' => 1,
+            ],
+        ], $constraint->optional()->toJsonSchema());
+    }
+
+    public function test_is_converted_to_json_schema(): void
+    {
+        $constraint = new MapConstraint(
+            valueConstraint: new StringConstraint()
+        );
+        self::assertEquals([
+            'type' => 'object',
+            'additionalProperties' => [
+                'type' => 'string',
+                'minLength' => 1,
+            ],
+        ], $constraint->optional()->toJsonSchema());
+    }
+
+    public function test_nullable_is_converted_to_json_schema(): void
+    {
+        $constraint = new MapConstraint(
+            valueConstraint: new StringConstraint()
+        );
+        self::assertEquals([
+            'type' => ['null', 'object'],
+            'additionalProperties' => [
+                'type' => 'string',
+                'minLength' => 1,
+            ],
+            'default' => null,
+        ], $constraint->nullable()->toJsonSchema());
+    }
+
+    public function test_validates_fail(): void
+    {
+        $constraint = new MapConstraint(
+            valueConstraint: new StringConstraint()
+        );
+
+        $validatedResult = $constraint->validate([
+            'aaa' => 'hi',
+            'bbb' => 5,
+        ]);
+        self::assertFalse($validatedResult->status->isValid());
+        self::assertEquals([
+            '' => 'invalid_nested_constraint',
+            'bbb' => 'invalid_data_type',
+        ], $validatedResult->getErrors()->toArray());
+    }
+
+    public function test_validates_ok(): void
+    {
+        $constraint = new MapConstraint(
+            valueConstraint: new StringConstraint()
+        );
+
+        $validatedResult = $constraint->validate([
+            'aaa' => 'hi',
+            'bbb' => 'foo',
+        ]);
+
+        self::assertTrue($validatedResult->status->isValid());
+    }
+}
diff --git a/src/Constraint/NumberConstraint.php b/src/Constraint/NumberConstraint.php
index fc39114d..4f1a9abb 100644
--- a/src/Constraint/NumberConstraint.php
+++ b/src/Constraint/NumberConstraint.php
@@ -11,7 +11,7 @@ use Distantmagic\Resonance\ConstraintReason;
 use Distantmagic\Resonance\ConstraintResult;
 use Distantmagic\Resonance\ConstraintResultStatus;
 
-readonly class NumberConstraint extends Constraint
+final readonly class NumberConstraint extends Constraint
 {
     public function default(mixed $defaultValue): self
     {
diff --git a/src/Constraint/StringConstraint.php b/src/Constraint/StringConstraint.php
index 47432d07..4572046f 100644
--- a/src/Constraint/StringConstraint.php
+++ b/src/Constraint/StringConstraint.php
@@ -13,7 +13,7 @@ use Distantmagic\Resonance\ConstraintResultStatus;
 use Distantmagic\Resonance\ConstraintStringFormat;
 use RuntimeException;
 
-readonly class StringConstraint extends Constraint
+final readonly class StringConstraint extends Constraint
 {
     public function __construct(
         public ?ConstraintStringFormat $format = null,
diff --git a/src/ConstraintSourceInterface.php b/src/ConstraintSourceInterface.php
new file mode 100644
index 00000000..10a4ef4c
--- /dev/null
+++ b/src/ConstraintSourceInterface.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+interface ConstraintSourceInterface
+{
+    public function getConstraint(): Constraint;
+}
diff --git a/src/ConstraintValidationException.php b/src/ConstraintValidationException.php
new file mode 100644
index 00000000..e09148d4
--- /dev/null
+++ b/src/ConstraintValidationException.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use RuntimeException;
+
+class ConstraintValidationException extends RuntimeException
+{
+    public function __construct(
+        string $constraintId,
+        ConstraintResult $constraintResult,
+    ) {
+        $errors = $constraintResult->getErrors();
+        $message = [];
+
+        foreach ($errors as $name => $errorCode) {
+            $message[] = sprintf('"%s" -> %s', $name, $errorCode);
+        }
+
+        var_dump($constraintResult->castedData);
+
+        parent::__construct(sprintf(
+            "%s:\n%s",
+            $constraintId,
+            implode("\n", $message),
+        ));
+    }
+}
diff --git a/src/SingletonProvider/ConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider.php
index 180e202d..dbb7d6bb 100644
--- a/src/SingletonProvider/ConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider.php
@@ -5,10 +5,8 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\SingletonProvider;
 
 use Distantmagic\Resonance\ConfigurationFile;
-use Distantmagic\Resonance\JsonSchemaSourceInterface;
-use Distantmagic\Resonance\JsonSchemaValidationException;
-use Distantmagic\Resonance\JsonSchemaValidationResult;
-use Distantmagic\Resonance\JsonSchemaValidator;
+use Distantmagic\Resonance\ConstraintSourceInterface;
+use Distantmagic\Resonance\ConstraintValidationException;
 use Distantmagic\Resonance\PHPProjectFiles;
 use Distantmagic\Resonance\SingletonContainer;
 use Distantmagic\Resonance\SingletonProvider;
@@ -19,7 +17,7 @@ use Distantmagic\Resonance\SingletonProvider;
  *
  * @template-extends SingletonProvider<TObject>
  */
-abstract readonly class ConfigurationProvider extends SingletonProvider implements JsonSchemaSourceInterface
+abstract readonly class ConfigurationProvider extends SingletonProvider implements ConstraintSourceInterface
 {
     abstract protected function getConfigurationKey(): string;
 
@@ -32,7 +30,6 @@ abstract readonly class ConfigurationProvider extends SingletonProvider implemen
 
     public function __construct(
         private ConfigurationFile $configurationFile,
-        private JsonSchemaValidator $jsonSchemaValidator,
     ) {}
 
     /**
@@ -45,18 +42,19 @@ abstract readonly class ConfigurationProvider extends SingletonProvider implemen
          */
         $data = $this->configurationFile->config->get($this->getConfigurationKey());
 
-        /**
-         * @var JsonSchemaValidationResult<TSchema>
-         */
-        $jsonSchemaValidationResult = $this->jsonSchemaValidator->validate($this, $data);
-
-        $errors = $jsonSchemaValidationResult->errors;
+        $constraintResult = $this->getConstraint()->validate($data);
 
-        if (empty($errors)) {
-            return $this->provideConfiguration($jsonSchemaValidationResult->data);
+        if ($constraintResult->status->isValid()) {
+            /**
+             * @var TSchema $constraintResult->castedData
+             */
+            return $this->provideConfiguration($constraintResult->castedData);
         }
 
-        throw new JsonSchemaValidationException($errors);
+        throw new ConstraintValidationException(
+            $this->getConfigurationKey(),
+            $constraintResult,
+        );
     }
 
     public function shouldRegister(): bool
diff --git a/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php
index d3280e51..8fb7c2ae 100644
--- a/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php
@@ -6,13 +6,16 @@ namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 use Distantmagic\Resonance\ApplicationConfiguration;
 use Distantmagic\Resonance\Attribute\Singleton;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\EnumConstraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
+use Distantmagic\Resonance\Constraint\StringConstraint;
 use Distantmagic\Resonance\Environment;
-use Distantmagic\Resonance\JsonSchema;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 use RuntimeException;
 
 /**
- * @template-extends ConfigurationProvider<ApplicationConfiguration, object{
+ * @template-extends ConfigurationProvider<ApplicationConfiguration, array{
  *     env: string,
  *     esbuild_metafile: non-empty-string,
  *     scheme: non-empty-string,
@@ -22,33 +25,16 @@ use RuntimeException;
 #[Singleton(provides: ApplicationConfiguration::class)]
 final readonly class ApplicationConfigurationProvider extends ConfigurationProvider
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        return new JsonSchema([
-            'type' => 'object',
-            'properties' => [
-                'env' => [
-                    'type' => 'string',
-                    'enum' => Environment::values(),
-                ],
-                'esbuild_metafile' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                    'default' => 'esbuild-meta.json',
-                ],
-                'scheme' => [
-                    'type' => 'string',
-                    'enum' => ['http', 'https'],
-                    'default' => 'https',
-                ],
-                'url' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                    'format' => 'uri',
-                ],
+        return new ObjectConstraint(
+            properties: [
+                'env' => new EnumConstraint(Environment::values()),
+                'esbuild_metafile' => (new StringConstraint())->default('esbuild-meta.json'),
+                'scheme' => (new EnumConstraint(['http', 'https']))->default('https'),
+                'url' => new StringConstraint(),
             ],
-            'required' => ['env', 'url'],
-        ]);
+        );
     }
 
     protected function getConfigurationKey(): string
@@ -58,16 +44,16 @@ final readonly class ApplicationConfigurationProvider extends ConfigurationProvi
 
     protected function provideConfiguration($validatedData): ApplicationConfiguration
     {
-        $url = rtrim($validatedData->url, '/');
+        $url = rtrim($validatedData['url'], '/');
 
         if (empty($url)) {
             throw new RuntimeException('URL cannot be an empty string');
         }
 
         return new ApplicationConfiguration(
-            environment: Environment::from($validatedData->env),
-            esbuildMetafile: DM_ROOT.'/'.$validatedData->esbuild_metafile,
-            scheme: $validatedData->scheme,
+            environment: Environment::from($validatedData['env']),
+            esbuildMetafile: DM_ROOT.'/'.$validatedData['esbuild_metafile'],
+            scheme: $validatedData['scheme'],
             url: $url,
         );
     }
diff --git a/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php
index d7cb2e2d..a3353715 100644
--- a/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php
@@ -5,16 +5,22 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 use Distantmagic\Resonance\Attribute\Singleton;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\BooleanConstraint;
+use Distantmagic\Resonance\Constraint\EnumConstraint;
+use Distantmagic\Resonance\Constraint\IntegerConstraint;
+use Distantmagic\Resonance\Constraint\MapConstraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
+use Distantmagic\Resonance\Constraint\StringConstraint;
 use Distantmagic\Resonance\DatabaseConfiguration;
 use Distantmagic\Resonance\DatabaseConnectionPoolConfiguration;
 use Distantmagic\Resonance\DatabaseConnectionPoolDriverName;
-use Distantmagic\Resonance\JsonSchema;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 /**
  * @template-extends ConfigurationProvider<
  *     DatabaseConfiguration,
- *     array<non-empty-string, object{
+ *     array<non-empty-string, array{
  *         database: string,
  *         driver: string,
  *         host: non-empty-string,
@@ -31,67 +37,24 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 #[Singleton(provides: DatabaseConfiguration::class)]
 final readonly class DatabaseConfigurationProvider extends ConfigurationProvider
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        $valueSchema = [
-            'type' => 'object',
-            'properties' => [
-                'database' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'driver' => [
-                    'type' => 'string',
-                    'enum' => DatabaseConnectionPoolDriverName::values(),
-                ],
-                'host' => [
-                    'type' => ['string', 'null'],
-                    'minLength' => 1,
-                    'default' => null,
-                ],
-                'log_queries' => [
-                    'type' => 'boolean',
-                ],
-                'password' => [
-                    'type' => 'string',
-                ],
-                'pool_prefill' => [
-                    'type' => 'boolean',
-                ],
-                'pool_size' => [
-                    'type' => 'integer',
-                    'minimum' => 1,
-                ],
-                'port' => [
-                    'type' => 'integer',
-                    'minimum' => 1,
-                    'maximum' => 65535,
-                    'default' => 3306,
-                ],
-                'unix_socket' => [
-                    'type' => ['string', 'null'],
-                    'default' => null,
-                ],
-                'username' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
+        $valueConstraint = new ObjectConstraint(
+            properties: [
+                'database' => new StringConstraint(),
+                'driver' => new EnumConstraint(DatabaseConnectionPoolDriverName::values()),
+                'host' => (new StringConstraint())->default(null),
+                'log_queries' => new BooleanConstraint(),
+                'password' => (new StringConstraint())->nullable(),
+                'pool_prefill' => (new BooleanConstraint())->default(true),
+                'pool_size' => new IntegerConstraint(),
+                'port' => (new IntegerConstraint())->nullable()->default(3306),
+                'unix_socket' => (new StringConstraint())->nullable(),
+                'username' => new StringConstraint(),
             ],
-            'required' => [
-                'database',
-                'driver',
-                'log_queries',
-                'password',
-                'pool_prefill',
-                'pool_size',
-                'username',
-            ],
-        ];
+        );
 
-        return new JsonSchema([
-            'type' => 'object',
-            'additionalProperties' => $valueSchema,
-        ]);
+        return new MapConstraint(valueConstraint: $valueConstraint);
     }
 
     protected function getConfigurationKey(): string
@@ -107,16 +70,16 @@ final readonly class DatabaseConfigurationProvider extends ConfigurationProvider
             $databaseconfiguration->connectionPoolConfiguration->put(
                 $name,
                 new DatabaseConnectionPoolConfiguration(
-                    database: $connectionPoolConfiguration->database,
-                    driver: DatabaseConnectionPoolDriverName::from($connectionPoolConfiguration->driver),
-                    host: $connectionPoolConfiguration->host,
-                    logQueries: $connectionPoolConfiguration->log_queries,
-                    password: $connectionPoolConfiguration->password,
-                    poolPrefill: $connectionPoolConfiguration->pool_prefill,
-                    poolSize: $connectionPoolConfiguration->pool_size,
-                    port: $connectionPoolConfiguration->port,
-                    unixSocket: $connectionPoolConfiguration->unix_socket,
-                    username: $connectionPoolConfiguration->username,
+                    database: $connectionPoolConfiguration['database'],
+                    driver: DatabaseConnectionPoolDriverName::from($connectionPoolConfiguration['driver']),
+                    host: $connectionPoolConfiguration['host'],
+                    logQueries: $connectionPoolConfiguration['log_queries'],
+                    password: $connectionPoolConfiguration['password'],
+                    poolPrefill: $connectionPoolConfiguration['pool_prefill'],
+                    poolSize: $connectionPoolConfiguration['pool_size'],
+                    port: $connectionPoolConfiguration['port'],
+                    unixSocket: $connectionPoolConfiguration['unix_socket'],
+                    username: $connectionPoolConfiguration['username'],
                 ),
             );
         }
diff --git a/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php
index fad86799..0f7f0b4c 100644
--- a/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php
@@ -5,12 +5,17 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 use Distantmagic\Resonance\Attribute\Singleton;
-use Distantmagic\Resonance\JsonSchema;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\EnumConstraint;
+use Distantmagic\Resonance\Constraint\IntegerConstraint;
+use Distantmagic\Resonance\Constraint\NumberConstraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
+use Distantmagic\Resonance\Constraint\StringConstraint;
 use Distantmagic\Resonance\LlamaCppConfiguration;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 /**
- * @template-extends ConfigurationProvider<LlamaCppConfiguration, object{
+ * @template-extends ConfigurationProvider<LlamaCppConfiguration, array{
  *     api_key: null|non-empty-string,
  *     completion_token_timeout: float,
  *     host: non-empty-string,
@@ -21,37 +26,17 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 #[Singleton(provides: LlamaCppConfiguration::class)]
 final readonly class LlamaCppConfigurationProvider extends ConfigurationProvider
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        return new JsonSchema([
-            'type' => 'object',
-            'properties' => [
-                'api_key' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                    'default' => null,
-                ],
-                'host' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'completion_token_timeout' => [
-                    'type' => 'number',
-                    'default' => 1.0,
-                ],
-                'port' => [
-                    'type' => 'integer',
-                    'minimum' => 1,
-                    'maximum' => 65535,
-                ],
-                'scheme' => [
-                    'type' => 'string',
-                    'enum' => ['http', 'https'],
-                    'default' => 'http',
-                ],
+        return new ObjectConstraint(
+            properties: [
+                'api_key' => (new StringConstraint())->default(null),
+                'completion_token_timeout' => (new NumberConstraint())->default(1.0),
+                'host' => new StringConstraint(),
+                'port' => new IntegerConstraint(),
+                'scheme' => (new EnumConstraint(['http', 'https']))->default('http'),
             ],
-            'required' => ['host', 'port'],
-        ]);
+        );
     }
 
     protected function getConfigurationKey(): string
@@ -62,11 +47,11 @@ final readonly class LlamaCppConfigurationProvider extends ConfigurationProvider
     protected function provideConfiguration($validatedData): LlamaCppConfiguration
     {
         return new LlamaCppConfiguration(
-            apiKey: $validatedData->api_key,
-            completionTokenTimeout: $validatedData->completion_token_timeout,
-            host: $validatedData->host,
-            port: $validatedData->port,
-            scheme: $validatedData->scheme,
+            apiKey: $validatedData['api_key'],
+            completionTokenTimeout: $validatedData['completion_token_timeout'],
+            host: $validatedData['host'],
+            port: $validatedData['port'],
+            scheme: $validatedData['scheme'],
         );
     }
 }
diff --git a/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php
index 34504416..4f09ae76 100644
--- a/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php
@@ -5,7 +5,10 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 use Distantmagic\Resonance\Attribute\Singleton;
-use Distantmagic\Resonance\JsonSchema;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\MapConstraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
+use Distantmagic\Resonance\Constraint\StringConstraint;
 use Distantmagic\Resonance\MailerConfiguration;
 use Distantmagic\Resonance\MailerTransportConfiguration;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
@@ -13,7 +16,7 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 /**
  * @template-extends ConfigurationProvider<
  *     MailerConfiguration,
- *     array<non-empty-string, object{
+ *     array<non-empty-string, array{
  *         dkim_domain_name: null|non-empty-string,
  *         dkim_selector: null|non-empty-string,
  *         dkim_signing_key_passphrase: null|non-empty-string,
@@ -26,50 +29,20 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 #[Singleton(provides: MailerConfiguration::class)]
 final readonly class MailerConfigurationProvider extends ConfigurationProvider
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        $valueSchema = [
-            'type' => 'object',
-            'properties' => [
-                'dkim_domain_name' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                    'default' => null,
-                ],
-                'dkim_selector' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                    'default' => null,
-                ],
-                'dkim_signing_key_passphrase' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                    'default' => null,
-                ],
-                'dkim_signing_key_private' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                    'default' => null,
-                ],
-                'dkim_signing_key_public' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                    'default' => null,
-                ],
-                'transport_dsn' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
+        $valueConstraint = new ObjectConstraint(
+            properties: [
+                'dkim_domain_name' => (new StringConstraint())->nullable(),
+                'dkim_selector' => (new StringConstraint())->nullable(),
+                'dkim_signing_key_passphrase' => (new StringConstraint())->nullable(),
+                'dkim_signing_key_private' => (new StringConstraint())->nullable(),
+                'dkim_signing_key_public' => (new StringConstraint())->nullable(),
+                'transport_dsn' => new StringConstraint(),
             ],
-            'required' => [
-                'transport_dsn',
-            ],
-        ];
+        );
 
-        return new JsonSchema([
-            'type' => 'object',
-            'additionalProperties' => $valueSchema,
-        ]);
+        return new MapConstraint(valueConstraint: $valueConstraint);
     }
 
     protected function getConfigurationKey(): string
@@ -85,12 +58,12 @@ final readonly class MailerConfigurationProvider extends ConfigurationProvider
             $mailerConfiguration->transportConfiguration->put(
                 $name,
                 new MailerTransportConfiguration(
-                    dkimDomainName: $transportConfiguration->dkim_domain_name,
-                    dkimSelector: $transportConfiguration->dkim_selector,
-                    dkimSigningKeyPassphrase: $transportConfiguration->dkim_signing_key_passphrase,
-                    dkimSigningKeyPrivate: $transportConfiguration->dkim_signing_key_private,
-                    dkimSigningKeyPublic: $transportConfiguration->dkim_signing_key_public,
-                    transportDsn: $transportConfiguration->transport_dsn,
+                    dkimDomainName: $transportConfiguration['dkim_domain_name'],
+                    dkimSelector: $transportConfiguration['dkim_selector'],
+                    dkimSigningKeyPassphrase: $transportConfiguration['dkim_signing_key_passphrase'],
+                    dkimSigningKeyPrivate: $transportConfiguration['dkim_signing_key_private'],
+                    dkimSigningKeyPublic: $transportConfiguration['dkim_signing_key_public'],
+                    transportDsn: $transportConfiguration['transport_dsn'],
                 )
             );
         }
diff --git a/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php
index bdb88003..8df863e2 100644
--- a/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php
@@ -7,8 +7,10 @@ namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 use Defuse\Crypto\Key;
 use Distantmagic\Resonance\Attribute\GrantsFeature;
 use Distantmagic\Resonance\Attribute\Singleton;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
+use Distantmagic\Resonance\Constraint\StringConstraint;
 use Distantmagic\Resonance\Feature;
-use Distantmagic\Resonance\JsonSchema;
 use Distantmagic\Resonance\OAuth2Configuration;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 use League\OAuth2\Server\CryptKey;
@@ -16,7 +18,7 @@ use RuntimeException;
 use Swoole\Coroutine;
 
 /**
- * @template-extends ConfigurationProvider<OAuth2Configuration, object{
+ * @template-extends ConfigurationProvider<OAuth2Configuration, array{
  *     encryption_key: non-empty-string,
  *     jwt_signing_key_passphrase: null|string,
  *     jwt_signing_key_private: non-empty-string,
@@ -30,45 +32,19 @@ use Swoole\Coroutine;
 #[Singleton(provides: OAuth2Configuration::class)]
 final readonly class OAuth2ConfigurationProvider extends ConfigurationProvider
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        return new JsonSchema([
-            'type' => 'object',
-            'properties' => [
-                'encryption_key' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'jwt_signing_key_passphrase' => [
-                    'type' => 'string',
-                    'default' => null,
-                ],
-                'jwt_signing_key_private' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'jwt_signing_key_public' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'session_key_authorization_request' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                    'default' => 'oauth2.authorization_request',
-                ],
-                'session_key_pkce' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                    'default' => 'oauth2.pkce',
-                ],
-                'session_key_state' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                    'default' => 'oauth2.state',
-                ],
+        return new ObjectConstraint(
+            properties: [
+                'encryption_key' => new StringConstraint(),
+                'jwt_signing_key_passphrase' => (new StringConstraint())->nullable(),
+                'jwt_signing_key_private' => new StringConstraint(),
+                'jwt_signing_key_public' => new StringConstraint(),
+                'session_key_authorization_request' => (new StringConstraint())->default('oauth2.authorization_request'),
+                'session_key_pkce' => (new StringConstraint())->default('oauth2.pkce'),
+                'session_key_state' => (new StringConstraint())->default('oauth2.state'),
             ],
-            'required' => ['encryption_key', 'jwt_signing_key_private', 'jwt_signing_key_public'],
-        ]);
+        );
     }
 
     protected function getConfigurationKey(): string
@@ -78,25 +54,25 @@ final readonly class OAuth2ConfigurationProvider extends ConfigurationProvider
 
     protected function provideConfiguration($validatedData): OAuth2Configuration
     {
-        $encryptionKeyContent = Coroutine::readFile($validatedData->encryption_key);
+        $encryptionKeyContent = Coroutine::readFile($validatedData['encryption_key']);
 
         if (!is_string($encryptionKeyContent)) {
-            throw new RuntimeException('Unable to read encrpytion key file: '.$validatedData->encryption_key);
+            throw new RuntimeException('Unable to read encrpytion key file: '.$validatedData['encryption_key']);
         }
 
         return new OAuth2Configuration(
             encryptionKey: Key::loadFromAsciiSafeString($encryptionKeyContent),
             jwtSigningKeyPrivate: new CryptKey(
-                DM_ROOT.'/'.$validatedData->jwt_signing_key_private,
-                $validatedData->jwt_signing_key_passphrase,
+                DM_ROOT.'/'.$validatedData['jwt_signing_key_private'],
+                $validatedData['jwt_signing_key_passphrase'],
             ),
             jwtSigningKeyPublic: new CryptKey(
-                DM_ROOT.'/'.$validatedData->jwt_signing_key_public,
-                $validatedData->jwt_signing_key_passphrase,
+                DM_ROOT.'/'.$validatedData['jwt_signing_key_public'],
+                $validatedData['jwt_signing_key_passphrase'],
             ),
-            sessionKeyAuthorizationRequest: $validatedData->session_key_authorization_request,
-            sessionKeyPkce: $validatedData->session_key_pkce,
-            sessionKeyState: $validatedData->session_key_state,
+            sessionKeyAuthorizationRequest: $validatedData['session_key_authorization_request'],
+            sessionKeyPkce: $validatedData['session_key_pkce'],
+            sessionKeyState: $validatedData['session_key_state'],
         );
     }
 }
diff --git a/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php
index de82092d..d779e780 100644
--- a/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php
@@ -5,12 +5,14 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 use Distantmagic\Resonance\Attribute\Singleton;
-use Distantmagic\Resonance\JsonSchema;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
+use Distantmagic\Resonance\Constraint\StringConstraint;
 use Distantmagic\Resonance\OpenAPIConfiguration;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 /**
- * @template-extends ConfigurationProvider<OpenAPIConfiguration, object{
+ * @template-extends ConfigurationProvider<OpenAPIConfiguration, array{
  *     description: non-empty-string,
  *     title: non-empty-string,
  *     version: non-empty-string,
@@ -19,26 +21,15 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 #[Singleton(provides: OpenAPIConfiguration::class)]
 final readonly class OpenAPIConfigurationProvider extends ConfigurationProvider
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        return new JsonSchema([
-            'type' => 'object',
-            'properties' => [
-                'description' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'title' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'version' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
+        return new ObjectConstraint(
+            properties: [
+                'description' => new StringConstraint(),
+                'title' => new StringConstraint(),
+                'version' => new StringConstraint(),
             ],
-            'required' => ['description', 'title', 'version'],
-        ]);
+        );
     }
 
     protected function getConfigurationKey(): string
@@ -49,9 +40,9 @@ final readonly class OpenAPIConfigurationProvider extends ConfigurationProvider
     protected function provideConfiguration($validatedData): OpenAPIConfiguration
     {
         return new OpenAPIConfiguration(
-            description: $validatedData->description,
-            title: $validatedData->title,
-            version: $validatedData->version,
+            description: $validatedData['description'],
+            title: $validatedData['title'],
+            version: $validatedData['version'],
         );
     }
 }
diff --git a/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php
index 5b562b22..e05c94de 100644
--- a/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php
@@ -5,7 +5,12 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 use Distantmagic\Resonance\Attribute\Singleton;
-use Distantmagic\Resonance\JsonSchema;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\BooleanConstraint;
+use Distantmagic\Resonance\Constraint\IntegerConstraint;
+use Distantmagic\Resonance\Constraint\MapConstraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
+use Distantmagic\Resonance\Constraint\StringConstraint;
 use Distantmagic\Resonance\RedisConfiguration;
 use Distantmagic\Resonance\RedisConnectionPoolConfiguration;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
@@ -13,10 +18,10 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 /**
  * @template-extends ConfigurationProvider<
  *     RedisConfiguration,
- *     array<string, object{
+ *     array<string, array{
  *         db_index: int,
  *         host: non-empty-string,
- *         password: string,
+ *         password: null|string,
  *         pool_prefill: bool,
  *         pool_size: int,
  *         port: int,
@@ -28,59 +33,22 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 #[Singleton(provides: RedisConfiguration::class)]
 final readonly class RedisConfigurationProvider extends ConfigurationProvider
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        $valueSchema = [
-            'type' => 'object',
-            'properties' => [
-                'db_index' => [
-                    'type' => 'integer',
-                    'minimum' => 0,
-                ],
-                'host' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'password' => [
-                    'type' => 'string',
-                ],
-                'pool_prefill' => [
-                    'type' => 'boolean',
-                ],
-                'pool_size' => [
-                    'type' => 'integer',
-                    'minimum' => 1,
-                ],
-                'port' => [
-                    'type' => 'integer',
-                    'minimum' => 1,
-                    'maximum' => 65535,
-                ],
-                'prefix' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'timeout' => [
-                    'type' => 'integer',
-                    'minimum' => 0,
-                ],
+        $valueConstraint = new ObjectConstraint(
+            properties: [
+                'db_index' => new IntegerConstraint(),
+                'host' => new StringConstraint(),
+                'password' => (new StringConstraint())->nullable(),
+                'pool_prefill' => (new BooleanConstraint())->default(true),
+                'pool_size' => new IntegerConstraint(),
+                'port' => new IntegerConstraint(),
+                'prefix' => new StringConstraint(),
+                'timeout' => new IntegerConstraint(),
             ],
-            'required' => [
-                'db_index',
-                'host',
-                'password',
-                'pool_prefill',
-                'pool_size',
-                'port',
-                'prefix',
-                'timeout',
-            ],
-        ];
+        );
 
-        return new JsonSchema([
-            'type' => 'object',
-            'additionalProperties' => $valueSchema,
-        ]);
+        return new MapConstraint(valueConstraint: $valueConstraint);
     }
 
     protected function getConfigurationKey(): string
@@ -96,14 +64,14 @@ final readonly class RedisConfigurationProvider extends ConfigurationProvider
             $databaseconfiguration->connectionPoolConfiguration->put(
                 $name,
                 new RedisConnectionPoolConfiguration(
-                    dbIndex: $connectionPoolConfiguration->db_index,
-                    host: $connectionPoolConfiguration->host,
-                    password: $connectionPoolConfiguration->password,
-                    poolPrefill: $connectionPoolConfiguration->pool_prefill,
-                    poolSize: $connectionPoolConfiguration->pool_size,
-                    port: $connectionPoolConfiguration->port,
-                    prefix: $connectionPoolConfiguration->prefix,
-                    timeout: $connectionPoolConfiguration->timeout,
+                    dbIndex: $connectionPoolConfiguration['db_index'],
+                    host: $connectionPoolConfiguration['host'],
+                    password: (string) $connectionPoolConfiguration['password'],
+                    poolPrefill: $connectionPoolConfiguration['pool_prefill'],
+                    poolSize: $connectionPoolConfiguration['pool_size'],
+                    port: $connectionPoolConfiguration['port'],
+                    prefix: $connectionPoolConfiguration['prefix'],
+                    timeout: $connectionPoolConfiguration['timeout'],
                 ),
             );
         }
diff --git a/src/SingletonProvider/ConfigurationProvider/SQLiteVSSConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/SQLiteVSSConfigurationProvider.php
index 654f676b..e7ab93b4 100644
--- a/src/SingletonProvider/ConfigurationProvider/SQLiteVSSConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/SQLiteVSSConfigurationProvider.php
@@ -5,12 +5,14 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 use Distantmagic\Resonance\Attribute\Singleton;
-use Distantmagic\Resonance\JsonSchema;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
+use Distantmagic\Resonance\Constraint\StringConstraint;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 use Distantmagic\Resonance\SQLiteVSSConfiguration;
 
 /**
- * @template-extends ConfigurationProvider<SQLiteVSSConfiguration, object{
+ * @template-extends ConfigurationProvider<SQLiteVSSConfiguration, array{
  *     extension_vector0: non-empty-string,
  *     extension_vss0: non-empty-string,
  * }>
@@ -18,22 +20,14 @@ use Distantmagic\Resonance\SQLiteVSSConfiguration;
 #[Singleton(provides: SQLiteVSSConfiguration::class)]
 final readonly class SQLiteVSSConfigurationProvider extends ConfigurationProvider
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        return new JsonSchema([
-            'type' => 'object',
-            'properties' => [
-                'extension_vector0' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'extension_vss0' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-            ],
-            'required' => ['extension_vector0', 'extension_vss0'],
-        ]);
+        return new ObjectConstraint(
+            properties: [
+                'extension_vector0' => new StringConstraint(),
+                'extension_vss0' => new StringConstraint(),
+            ]
+        );
     }
 
     protected function getConfigurationKey(): string
@@ -44,8 +38,8 @@ final readonly class SQLiteVSSConfigurationProvider extends ConfigurationProvide
     protected function provideConfiguration($validatedData): SQLiteVSSConfiguration
     {
         return new SQLiteVSSConfiguration(
-            extensionVector0: $validatedData->extension_vector0,
-            extensionVss0: $validatedData->extension_vss0,
+            extensionVector0: $validatedData['extension_vector0'],
+            extensionVss0: $validatedData['extension_vss0'],
         );
     }
 }
diff --git a/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php
index ec00d6eb..c96ea49b 100644
--- a/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php
@@ -6,14 +6,17 @@ namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 use Distantmagic\Resonance\Attribute\Singleton;
 use Distantmagic\Resonance\ConfigurationFile;
-use Distantmagic\Resonance\JsonSchema;
-use Distantmagic\Resonance\JsonSchemaValidator;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\EnumConstraint;
+use Distantmagic\Resonance\Constraint\IntegerConstraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
+use Distantmagic\Resonance\Constraint\StringConstraint;
 use Distantmagic\Resonance\RedisConfiguration;
 use Distantmagic\Resonance\SessionConfiguration;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 /**
- * @template-extends ConfigurationProvider<SessionConfiguration, object{
+ * @template-extends ConfigurationProvider<SessionConfiguration, array{
  *     cookie_lifespan: int,
  *     cookie_name: non-empty-string,
  *     cookie_samesite: string,
@@ -25,13 +28,12 @@ final readonly class SessionConfigurationProvider extends ConfigurationProvider
 {
     public function __construct(
         private ConfigurationFile $configurationFile,
-        private JsonSchemaValidator $jsonSchemaValidator,
         private RedisConfiguration $redisConfiguration,
     ) {
-        parent::__construct($configurationFile, $jsonSchemaValidator);
+        parent::__construct($configurationFile);
     }
 
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
         $redisConnectionPools = $this
             ->redisConfiguration
@@ -40,29 +42,14 @@ final readonly class SessionConfigurationProvider extends ConfigurationProvider
             ->toArray()
         ;
 
-        return new JsonSchema([
-            'type' => 'object',
-            'properties' => [
-                'cookie_lifespan' => [
-                    'type' => 'integer',
-                    'minimum' => 1,
-                ],
-                'cookie_name' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'cookie_samesite' => [
-                    'type' => 'string',
-                    'enum' => ['lax', 'none', 'strict'],
-                    'default' => 'lax',
-                ],
-                'redis_connection_pool' => [
-                    'type' => 'string',
-                    'enum' => $redisConnectionPools,
-                ],
+        return new ObjectConstraint(
+            properties: [
+                'cookie_lifespan' => new IntegerConstraint(),
+                'cookie_name' => new StringConstraint(),
+                'cookie_samesite' => (new EnumConstraint(['lax', 'none', 'strict']))->default('lax'),
+                'redis_connection_pool' => new EnumConstraint($redisConnectionPools),
             ],
-            'required' => ['cookie_lifespan', 'cookie_name'],
-        ]);
+        );
     }
 
     protected function getConfigurationKey(): string
@@ -73,10 +60,10 @@ final readonly class SessionConfigurationProvider extends ConfigurationProvider
     protected function provideConfiguration($validatedData): SessionConfiguration
     {
         return new SessionConfiguration(
-            cookieLifespan: $validatedData->cookie_lifespan,
-            cookieName: $validatedData->cookie_name,
-            cookieSameSite: $validatedData->cookie_samesite,
-            redisConnectionPool: $validatedData->redis_connection_pool,
+            cookieLifespan: $validatedData['cookie_lifespan'],
+            cookieName: $validatedData['cookie_name'],
+            cookieSameSite: $validatedData['cookie_samesite'],
+            redisConnectionPool: $validatedData['redis_connection_pool'],
         );
     }
 }
diff --git a/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php
index 38f90ebf..5f3fca8f 100644
--- a/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php
@@ -5,12 +5,14 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 use Distantmagic\Resonance\Attribute\Singleton;
-use Distantmagic\Resonance\JsonSchema;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
+use Distantmagic\Resonance\Constraint\StringConstraint;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 use Distantmagic\Resonance\StaticPageConfiguration;
 
 /**
- * @template-extends ConfigurationProvider<StaticPageConfiguration, object{
+ * @template-extends ConfigurationProvider<StaticPageConfiguration, array{
  *     base_url: non-empty-string,
  *     esbuild_metafile: non-empty-string,
  *     input_directory: non-empty-string,
@@ -21,34 +23,17 @@ use Distantmagic\Resonance\StaticPageConfiguration;
 #[Singleton(provides: StaticPageConfiguration::class)]
 final readonly class StaticPageConfigurationProvider extends ConfigurationProvider
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        return new JsonSchema([
-            'type' => 'object',
-            'properties' => [
-                'base_url' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'esbuild_metafile' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'input_directory' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'output_directory' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'sitemap' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
+        return new ObjectConstraint(
+            properties: [
+                'base_url' => new StringConstraint(),
+                'esbuild_metafile' => new StringConstraint(),
+                'input_directory' => new StringConstraint(),
+                'output_directory' => new StringConstraint(),
+                'sitemap' => new StringConstraint(),
             ],
-            'required' => ['base_url', 'esbuild_metafile', 'input_directory', 'output_directory', 'sitemap'],
-        ]);
+        );
     }
 
     protected function getConfigurationKey(): string
@@ -59,12 +44,12 @@ final readonly class StaticPageConfigurationProvider extends ConfigurationProvid
     protected function provideConfiguration($validatedData): StaticPageConfiguration
     {
         return new StaticPageConfiguration(
-            baseUrl: $validatedData->base_url,
-            esbuildMetafile: DM_ROOT.'/'.$validatedData->esbuild_metafile,
-            inputDirectory: DM_ROOT.'/'.$validatedData->input_directory,
-            outputDirectory: DM_ROOT.'/'.$validatedData->output_directory,
-            sitemap: DM_ROOT.'/'.$validatedData->sitemap,
-            stripOutputPrefix: $validatedData->output_directory.'/',
+            baseUrl: $validatedData['base_url'],
+            esbuildMetafile: DM_ROOT.'/'.$validatedData['esbuild_metafile'],
+            inputDirectory: DM_ROOT.'/'.$validatedData['input_directory'],
+            outputDirectory: DM_ROOT.'/'.$validatedData['output_directory'],
+            sitemap: DM_ROOT.'/'.$validatedData['sitemap'],
+            stripOutputPrefix: $validatedData['output_directory'].'/',
         );
     }
 }
diff --git a/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php
index bf91df21..deb95764 100644
--- a/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php
@@ -5,14 +5,18 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 use Distantmagic\Resonance\Attribute\Singleton;
-use Distantmagic\Resonance\JsonSchema;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\BooleanConstraint;
+use Distantmagic\Resonance\Constraint\IntegerConstraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
+use Distantmagic\Resonance\Constraint\StringConstraint;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 use Distantmagic\Resonance\SwooleConfiguration;
 
 /**
- * @template-extends ConfigurationProvider<SwooleConfiguration, object{
+ * @template-extends ConfigurationProvider<SwooleConfiguration, array{
  *     host: non-empty-string,
- *     log_level: non-empty-string,
+ *     log_level: int,
  *     log_requests: boolean,
  *     port: int,
  *     ssl_cert_file: non-empty-string,
@@ -23,50 +27,19 @@ use Distantmagic\Resonance\SwooleConfiguration;
 #[Singleton(provides: SwooleConfiguration::class)]
 final readonly class SwooleConfigurationProvider extends ConfigurationProvider
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        return new JsonSchema([
-            'type' => 'object',
-            'properties' => [
-                'host' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'log_level' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'log_requests' => [
-                    'type' => 'boolean',
-                    'default' => false,
-                ],
-                'port' => [
-                    'type' => 'integer',
-                    'minimum' => 1,
-                    'maximum' => 65535,
-                ],
-                'ssl_cert_file' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'ssl_key_file' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'task_worker_num' => [
-                    'type' => 'integer',
-                    'min' => 1,
-                    'default' => 4,
-                ],
+        return new ObjectConstraint(
+            properties: [
+                'host' => new StringConstraint(),
+                'log_level' => new IntegerConstraint(),
+                'log_requests' => (new BooleanConstraint())->default(false),
+                'port' => new IntegerConstraint(),
+                'ssl_cert_file' => new StringConstraint(),
+                'ssl_key_file' => new StringConstraint(),
+                'task_worker_num' => (new IntegerConstraint())->default(4),
             ],
-            'required' => [
-                'host',
-                'log_level',
-                'port',
-                'ssl_cert_file',
-                'ssl_key_file',
-            ],
-        ]);
+        );
     }
 
     protected function getConfigurationKey(): string
@@ -77,13 +50,13 @@ final readonly class SwooleConfigurationProvider extends ConfigurationProvider
     protected function provideConfiguration($validatedData): SwooleConfiguration
     {
         return new SwooleConfiguration(
-            host: $validatedData->host,
-            logLevel: (int) ($validatedData->log_level),
-            logRequests: $validatedData->log_requests,
-            port: $validatedData->port,
-            sslCertFile: $validatedData->ssl_cert_file,
-            sslKeyFile: $validatedData->ssl_key_file,
-            taskWorkerNum: $validatedData->task_worker_num,
+            host: $validatedData['host'],
+            logLevel: $validatedData['log_level'],
+            logRequests: $validatedData['log_requests'],
+            port: $validatedData['port'],
+            sslCertFile: $validatedData['ssl_cert_file'],
+            sslKeyFile: $validatedData['ssl_key_file'],
+            taskWorkerNum: $validatedData['task_worker_num'],
         );
     }
 }
diff --git a/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php
index 87c4216a..f090d176 100644
--- a/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php
@@ -5,12 +5,14 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 use Distantmagic\Resonance\Attribute\Singleton;
-use Distantmagic\Resonance\JsonSchema;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
+use Distantmagic\Resonance\Constraint\StringConstraint;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 use Distantmagic\Resonance\TranslatorConfiguration;
 
 /**
- * @template-extends ConfigurationProvider<TranslatorConfiguration, object{
+ * @template-extends ConfigurationProvider<TranslatorConfiguration, array{
  *     base_directory: non-empty-string,
  *     default_primary_language: non-empty-string,
  * }>
@@ -18,22 +20,14 @@ use Distantmagic\Resonance\TranslatorConfiguration;
 #[Singleton(provides: TranslatorConfiguration::class)]
 final readonly class TranslatorConfigurationProvider extends ConfigurationProvider
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        return new JsonSchema([
-            'type' => 'object',
-            'properties' => [
-                'base_directory' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
-                'default_primary_language' => [
-                    'type' => 'string',
-                    'minLength' => 1,
-                ],
+        return new ObjectConstraint(
+            properties: [
+                'base_directory' => new StringConstraint(),
+                'default_primary_language' => new StringConstraint(),
             ],
-            'required' => ['base_directory', 'default_primary_language'],
-        ]);
+        );
     }
 
     protected function getConfigurationKey(): string
@@ -44,8 +38,8 @@ final readonly class TranslatorConfigurationProvider extends ConfigurationProvid
     protected function provideConfiguration($validatedData): TranslatorConfiguration
     {
         return new TranslatorConfiguration(
-            baseDirectory: $validatedData->base_directory,
-            defaultPrimaryLanguage: $validatedData->default_primary_language,
+            baseDirectory: $validatedData['base_directory'],
+            defaultPrimaryLanguage: $validatedData['default_primary_language'],
         );
     }
 }
diff --git a/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php
index 8757cc3c..40540e10 100644
--- a/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php
+++ b/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php
@@ -6,13 +6,15 @@ namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 
 use Distantmagic\Resonance\Attribute\GrantsFeature;
 use Distantmagic\Resonance\Attribute\Singleton;
+use Distantmagic\Resonance\Constraint;
+use Distantmagic\Resonance\Constraint\IntegerConstraint;
+use Distantmagic\Resonance\Constraint\ObjectConstraint;
 use Distantmagic\Resonance\Feature;
-use Distantmagic\Resonance\JsonSchema;
 use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider;
 use Distantmagic\Resonance\WebSocketConfiguration;
 
 /**
- * @template-extends ConfigurationProvider<WebSocketConfiguration, object{
+ * @template-extends ConfigurationProvider<WebSocketConfiguration, array{
  *     max_connections: int,
  * }>
  */
@@ -20,19 +22,16 @@ use Distantmagic\Resonance\WebSocketConfiguration;
 #[Singleton(provides: WebSocketConfiguration::class)]
 final readonly class WebSocketConfigurationProvider extends ConfigurationProvider
 {
-    public function getSchema(): JsonSchema
+    public function getConstraint(): Constraint
     {
-        return new JsonSchema([
-            'type' => 'object',
-            'properties' => [
-                'max_connections' => [
-                    'type' => 'integer',
-                    'minimum' => 1,
-                    'maximum' => 65535,
-                    'default' => 10000,
-                ],
+        return new ObjectConstraint(
+            // 'minimum' => 1,
+            // 'maximum' => 65535,
+            // 'default' => 10000,
+            properties: [
+                'max_connections' => (new IntegerConstraint())->default(10000),
             ],
-        ]);
+        );
     }
 
     protected function getConfigurationKey(): string
@@ -43,7 +42,7 @@ final readonly class WebSocketConfigurationProvider extends ConfigurationProvide
     protected function provideConfiguration($validatedData): WebSocketConfiguration
     {
         return new WebSocketConfiguration(
-            maxConnections: $validatedData->max_connections,
+            maxConnections: $validatedData['max_connections'],
         );
     }
 }
-- 
GitLab