diff --git a/docs/pages/index.md b/docs/pages/index.md
index 93f35f48847347bfaad3609460a5c6314f873071..ff1078ac44ed549d5561fa7773e7875730118428 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 0baab197962c60fb89649ac9aca82c17a9f8e332..7fa48a30fd16e019425bde5610ed13e91b6df2e7 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 0000000000000000000000000000000000000000..8b58e9aa10a95fce6e5b7afd61bb38c2a0de7171
--- /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 0000000000000000000000000000000000000000..ff422df175d7b0fdb143207e0cc8314f9c2e3c08
--- /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 2bfc053ba4b55a1250ff4efc7d4e866e3ca894ce..9f1e3904c05a9844bcd5cf2855866ee83459bef4 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 bb14cc29abfcc40b8008db08692b52db9ab60bd0..ee10aab8261ca55412205212fcfd4f142dcdd850 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 764f0b45a3875a097eb481ea38065e4fe9cfaae2..624488f67569db683b981b658811e26a6527888e 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 178148c45aeccd0ec3a89640230e5431fbf1229a..1fb70bf4884ce080ad8b73bfdc7771c1ec3edb77 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 0000000000000000000000000000000000000000..837067b6ba5aa48b3c05e2b66d4e9dad0dc0637f
--- /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 0000000000000000000000000000000000000000..c63e5744eeae7c62707c0c4d0c9b26c03140579a
--- /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 fc39114dffab53b9517fe781431b13cf95931ab4..4f1a9abb03b39ee68a056026fe1eef99f009085f 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 47432d07399406d32bdaec2b23484a9c2d43737b..4572046fee85c29b7ac41d38b7a294e6fb85cd6a 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 0000000000000000000000000000000000000000..10a4ef4c3cb590f5a83c3615ef40d80ecf050693
--- /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 0000000000000000000000000000000000000000..e09148d4b0921c171e14eddd3463c0e358db56ae
--- /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 180e202d6f965ca4daf617df1a6d7e1f7fe17882..dbb7d6bb839ef7e0bd5d0d34fc55cdaddd97e483 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 d3280e51e07651c43ea219960a336ef691b11153..8fb7c2aea401c215b4a78ebe1e174b192fb80205 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 d7cb2e2d25c23fe9930b258388509294959e0b55..a33537151a3f1af02e509c89391d5d073fdf3c41 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 fad8679964ff770f0b1febea4ee9d0d4b6937f6a..0f7f0b4c7a0f41eb3ae540b0971fb792562fa586 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 34504416e6b05f8823ca6cde6eb894396e0a8af5..4f09ae76bcddd8f2f675071fa3622aab311d2ee1 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 bdb88003134e430be8d21b5fb103705540ae5137..8df863e28ef5b1e125fea48e72f36b92f5f17f67 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 de82092dd8b301cd0f9f1115d2d64a22c2be4d86..d779e780bc70dcf2196fc942e7b178901a69b9c5 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 5b562b226f376865ac55b3ad634499f8d599a6b6..e05c94de07928f1e6259bd007a94bf4d6259793e 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 654f676b69bf6bea758737a006e41b1cb1453415..e7ab93b4ef59b03f9a0a66573b5951c05c16b489 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 ec00d6ebfbb9400133251f3a034a64f5979a3743..c96ea49bcfe13ff9538fcee97eb9864672067c68 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 38f90ebf7a51ae512b1e36d27d3e23b36df9a8d9..5f3fca8fce8fc1a1717fd9dcc65379cb19893f77 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 bf91df2168e8fca0604cdb689103cce9d0a1c421..deb95764d6d58f70e050003877e58712c8fde294 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 87c4216ae645ec28d5c7d0d75c8f1497d0da8252..f090d1763aedc4c7514c2e53325142a3f61eb001 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 8757cc3cafdf60303814d30d082a50baa797fa26..40540e109f30313171e4ad2e0ef4250d9434920c 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'],
         );
     }
 }