Repository: CuyZ/Valinor (php)

Issue:
`enableFlexibleCasting` but just for scalars
we would want to have type juggling from e.g. `numeric-strings` into `int`; same for `float` from `string` (just like regular php cast rules).

we found the [`enableFlexibleCasting`](https://valinor.cuyz.io/latest/usage/type-strictness-and-flexibility/#enabling-flexible-casting) to achieve it, 

``` 
$flexibleMapper = (new \CuyZ\Valinor\MapperBuilder())
    ->enableFlexibleCasting()
    ->mapper();

$flexibleMapper->map('int', '42');
// => 42
```

but it also enables some kind of list<->array juggling, which we would __not__ want:

```
$flexibleMapper->map('list<int>', ['foo' => 42, 'bar' => 1337]);
// => [0 => 42, 1 => 1338]
```

is there a supported way to only enable type juggling for scalars? or some kind of api we should use?

our use-case: we want to convert from `$_GET` or `$_POST` which is always `string` and need to map it into objects which e.g. expect `int`.

it seems valinor is already able so we can easily define per class how data needs to be mapped to said class, but it seems to miss some kind of "general conversion" building blocks for scalar types only.

Pull request:
feat: split flexible casting setting in three distinct modes
The mapper setting `enableFlexibleCasting` is (softly) deprecated in favor of three distinct modes, which guarantee the same functionalities as before.

Allowing scalar value casting
-----------------------------

With this setting enabled, scalar types will accept castable values:

- Integer types will accept any valid numeric value, for instance the string value "42".

- Float types will accept any valid numeric value, for instance the string value "1337.42".

- String types will accept any integer, float or object implementing the `Stringable` interface.

- Boolean types will accept any truthy or falsy value:
    * "true" (string), "1" (string) and 1 (int) will be cast to `true`
    * "false" (string), "0" (string) and 0 (int) will be cast to `false`

```php
(new \CuyZ\Valinor\MapperBuilder())
    ->allowNonSequentialList()
    ->mapper()
    ->map('array{id: string, price: float, active: bool}', [
        'id' => 549465210, // Will be cast to string
        'price' => '42.39', // Will be cast to float
        'active' => 1, // Will be cast to bool
    ]);
```

Allowing non-sequential lists
-----------------------------

By default, list types will only accept sequential keys starting from 0.

This setting allows the mapper to convert associative arrays to a list with sequential keys.

```php
(new \CuyZ\Valinor\MapperBuilder())
    ->allowNonSequentialList()
    ->mapper()
    ->map('list<int>', [
        'foo' => 42,
        'bar' => 1337,
    ]);

// => [0 => 42, 1 => 1337]
```

Allowing undefined values
-------------------------

Allows the mapper to accept undefined values (missing from the input), by converting them to `null` (if the current type is nullable) or an empty array (if the current type is an object or an iterable).

```php
(new \CuyZ\Valinor\MapperBuilder())
    ->allowUndefinedValues()
    ->mapper()
    ->map('array{name: string, age: int|null}', [
        'name' => 'John Doe',
        // 'age' is not defined
    ]);

// => ['name' => 'John Doe', 'age' => null]
```

Fixes #524

Patch:
diff --git a/docs/pages/other/app-and-framework-integration.md b/docs/pages/other/app-and-framework-integration.md
index 64cb225..627dfee 100644
--- a/docs/pages/other/app-and-framework-integration.md
+++ b/docs/pages/other/app-and-framework-integration.md
@@ -74,14 +74,13 @@ should be given to the `warmup` method, as stated in the [cache warmup chapter].
 
 ### Other configurations
 
-Concerning other configurations, such as [enabling flexible casting],
-[configuring supported date formats] or [registering custom constructors],
-an integration should be provided to configure the mapper builder in a
-convenient way — how it is done will mostly depend on the framework features and
-its main philosophy.
+Concerning other configurations, such as [flexible modes], [configuring
+supported date formats] or [registering custom constructors], an integration 
+should be provided to configure the mapper builder in a convenient way — how it
+is done will mostly depend on the framework features and its main philosophy.
 
 [performance chapter]: performance-and-caching.md
 [cache warmup chapter]: performance-and-caching.md#warming-up-cache
-[enabling flexible casting]: ../usage/type-strictness-and-flexibility.md#enabling-flexible-casting
+[flexible modes]: ../usage/type-strictness-and-flexibility.md
 [configuring supported date formats]: ../how-to/deal-with-dates.md
 [registering custom constructors]: ../how-to/use-custom-object-constructors.md
diff --git a/docs/pages/project/alternatives.md b/docs/pages/project/alternatives.md
index a425d87..6918274 100644
--- a/docs/pages/project/alternatives.md
+++ b/docs/pages/project/alternatives.md
@@ -28,4 +28,4 @@ might be missing:
 
 [reported error messages]: ../usage/validation-and-error-handling.md
 [advanced types]: ../usage/type-reference.md
-[type casting]: ../usage/type-strictness-and-flexibility.md#enabling-flexible-casting
+[type casting]: ../usage/type-strictness-and-flexibility.md#allowing-scalar-value-casting
diff --git a/docs/pages/usage/type-strictness-and-flexibility.md b/docs/pages/usage/type-strictness-and-flexibility.md
index e3a1014..319a461 100644
--- a/docs/pages/usage/type-strictness-and-flexibility.md
+++ b/docs/pages/usage/type-strictness-and-flexibility.md
@@ -1,104 +1,114 @@
 # Type strictness & flexibility
 
-The mapper is sensitive to the types of the data that is recursively populated —
-for instance a string `"42"` given to a node that expects an integer will make
-the mapping fail because the type is not strictly respected.
+The mapper provided by this library is designed to be as strict as possible.
+Any deviation from the expected types or structures will result in a mapping
+error.
 
-Array keys that are not bound to any node are forbidden. Mapping an array
-`['foo' => …, 'bar' => …, 'baz' => …]` to an object that needs only `foo` and
-`bar` will fail, because `baz` is superfluous. The same rule applies for
-shaped arrays.
+1. Scalar types will only accept values that strictly match their type.
+2. Objects will only accept values that can be mapped to their constructor
+   parameters, unneeded array entries will cause a mapping error.
+3. Arrays and lists will only accept values that strictly match their subtypes
+   and structure.
 
-When mapping to a list, the given array must have sequential integer keys
-starting at 0; if any gap or invalid key is found it will fail, like for 
-instance trying to map `['foo' => 'foo', 'bar' => 'bar']` to `list<string>`.
-
-Types that are too permissive are not permitted — if the mapper encounters a 
-type like `mixed`, `object` or `array` it will fail because those types are not
-precise enough.
-
----
+Types like `mixed` or `object` are too permissive and not permitted, because
+they are not precise enough.
 
 If these limitations are too restrictive, the mapper can be made more flexible
 to disable one or several rule(s) declared above.
 
-## Enabling flexible casting
+## Allowing scalar value casting
+
+With this setting enabled, scalar types will accept castable values:
+
+- Integer types will accept any valid numeric value, for instance the string
+  value `"42"`.
+
+- Float types will accept any valid numeric value, for instance the string value
+  `"1337.42"`.
+
+- String types will accept any integer, float or object implementing the
+  `Stringable` interface.
 
-This setting changes the behaviours explained below:
+- Boolean types will accept any truthy or falsy value:
+    * `(string) "true"`, `(string) "1"` and `(int) 1` will be cast to `true`
+    * `(string) "false"`, `(string) "0"` and `(int) 0` will be cast to `false`
 
 ```php
-$flexibleMapper = (new \CuyZ\Valinor\MapperBuilder())
-    ->enableFlexibleCasting()
-    ->mapper();
+(new \CuyZ\Valinor\MapperBuilder())
+    ->allowNonSequentialList()
+    ->mapper()
+    ->map('array{id: string, price: float, active: bool}', [
+        'id' => 549465210, // Will be cast to string
+        'price' => '42.39', // Will be cast to float
+        'active' => 1, // Will be cast to bool
+    ]);
+```
 
-// ---
-// Scalar types will accept non-strict values; for instance an integer
-// type will accept any valid numeric value like the *string* "42".
+## Allowing non-sequential lists
 
-$flexibleMapper->map('int', '42');
-// => 42
+By default, list types will only accept sequential keys starting from 0.
 
-// ---
-// List type will accept non-incremental keys.
+This setting allows the mapper to convert associative arrays to a list with
+sequential keys.
+
+```php
+(new \CuyZ\Valinor\MapperBuilder())
+    ->allowNonSequentialList()
+    ->mapper()
+    ->map('list<int>', [
+        'foo' => 42,
+        'bar' => 1337,
+    ]);
 
-$flexibleMapper->map('list<int>', ['foo' => 42, 'bar' => 1337]);
 // => [0 => 42, 1 => 1337]
+```
+
+## Allowing undefined values
+
+Allows the mapper to accept undefined values (missing from the input), by 
+converting them to `null` (if the current type is nullable) or an empty array
+(if the current type is an object or an iterable).
 
-// ---
-// If a value is missing in a source for a node that accepts `null`, the
-// node will be filled with `null`.
-
-$flexibleMapper->map(
-    'array{foo: string, bar: null|string}',
-    ['foo' => 'foo'] // `bar` is missing
-);
-// => ['foo' => 'foo', 'bar' => null]
-
-// ---
-// Array and list types will convert `null` or missing values to an empty
-// array.
-
-$flexibleMapper->map(
-    'array{foo: string, bar: array<string>}',
-    ['foo' => 'foo'] // `bar` is missing
-);
-// => ['foo' => 'foo', 'bar' => []]
+```php
+(new \CuyZ\Valinor\MapperBuilder())
+    ->allowUndefinedValues()
+    ->mapper()
+    ->map('array{name: string, age: int|null}', [
+        'name' => 'John Doe',
+        // 'age' is not defined
+    ]);
+
+// => ['name' => 'John Doe', 'age' => null]
 ```
 
 ## Allowing superfluous keys
 
-With this setting enabled, superfluous keys in source arrays will be allowed, 
-preventing errors when a value is not bound to any object property/parameter or
-shaped array element.
+By default, an error is raised when a source array contains keys that do not
+match a class property/parameter or a shaped array element.
+
+This setting allows the mapper to ignore these superfluous keys.
 
 ```php
 (new \CuyZ\Valinor\MapperBuilder())
     ->allowSuperfluousKeys()
     ->mapper()
-    ->map(
-        'array{foo: string, bar: int}',
-        [
-            'foo' => 'foo',
-            'bar' => 42,
-            'baz' => 1337.404, // `baz` will be ignored
-        ]
-    );
+    ->map('array{name: string, age: int}', [
+        'name' => 'John Doe',
+        'age' => 42,
+        'city' => 'Paris', // Will be ignored
+    ]);
 ```
 
 ## Allowing permissive types
 
-This setting allows permissive types `mixed` and `object` to be used during 
-mapping.
+Allows permissive types `mixed` and `object` to be used during mapping.
 
 ```php
 (new \CuyZ\Valinor\MapperBuilder())
     ->allowPermissiveTypes()
     ->mapper()
-    ->map(
-        'array{foo: string, bar: mixed}',
-        [
-            'foo' => 'foo',
-            'bar' => 42, // Could be any value
-        ]
-    );
+    ->map('array{name: string, data: mixed}', [
+        'name' => 'some_product',
+        'data' => 42, // Could be any value
+    ]);
 ```
diff --git a/docs/pages/usage/validation-and-error-handling.md b/docs/pages/usage/validation-and-error-handling.md
index 74e78bd..a8fd615 100644
--- a/docs/pages/usage/validation-and-error-handling.md
+++ b/docs/pages/usage/validation-and-error-handling.md
@@ -1,23 +1,14 @@
 # Validation and error handling
 
-The source given to a mapper can never be trusted, this is actually the very
-goal of this library: transforming an unstructured input to a well-defined
-object structure. If a value has an invalid type, or if the mapper cannot cast
-it properly (in flexible mode), it means that it is not able to guarantee the
-validity of the desired object thus it will fail.
-
-Any issue encountered during the mapping will add an error to an upstream
-exception of type `\CuyZ\Valinor\Mapper\MappingError`. It is therefore always
-recommended wrapping the mapping function call with a try/catch statement and
-handle the error properly.
-
-When the mapping fails, the exception gives access to the root node. This
-recursive object allows retrieving all needed information through the whole
-mapping tree: path, values, types and messages, including the issues that caused
-the exception.
-
-Node messages can be customized and iterated through with the usage of the class 
-`\CuyZ\Valinor\Mapper\Tree\Message\Messages`.
+Validation and error handling are important aspects of the mapping process:
+transforming an unstructured input to a strictly typed data structure must be
+trusted. If an invalid value is found, the validity of the desired structure
+cannot be guaranteed, thus the mapping will fail.
+
+When the mapping fails, an exception of type `\CuyZ\Valinor\Mapper\MappingError`
+is thrown, containing information about all errors encountered during the 
+mapping. It is therefore always recommended wrapping the mapping function call
+with a try/catch statement and handle the error properly.
 
 ```php
 try {
diff --git a/rector.php b/rector.php
index e774529..5e8595a 100644
--- a/rector.php
+++ b/rector.php
@@ -34,7 +34,7 @@ return static function (RectorConfig $config): void {
         NullToStrictStringFuncCallArgRector::class,
         ReadOnlyPropertyRector::class,
         RestoreDefaultNullToNullableTypePropertyRector::class => [
-            __DIR__ . '/tests/Integration/Mapping/Other/FlexibleCastingMappingTest.php',
+            __DIR__ . '/tests/Integration/Mapping/Other/UndefinedValuesMappingTest.php',
             __DIR__ . '/tests/Integration/Mapping/SingleNodeMappingTest',
         ],
     ]);
diff --git a/src/Library/Settings.php b/src/Library/Settings.php
index 2677d77..4cdb1cb 100644
--- a/src/Library/Settings.php
+++ b/src/Library/Settings.php
@@ -47,7 +47,11 @@ final class Settings
     /** @var non-empty-list<non-empty-string> */
     public array $supportedDateFormats = self::DEFAULT_SUPPORTED_DATETIME_FORMATS;
 
-    public bool $enableFlexibleCasting = false;
+    public bool $allowScalarValueCasting = false;
+
+    public bool $allowNonSequentialList = false;
+
+    public bool $allowUndefinedValues = false;
 
     public bool $allowSuperfluousKeys = false;
 
@@ -111,7 +115,9 @@ final class Settings
             implode('', array_map($this->callableSignature(...), $this->customConstructors)),
             implode('', array_map($this->callableSignature(...), $this->valueModifier)),
             $this->supportedDateFormats,
-            $this->enableFlexibleCasting,
+            $this->allowScalarValueCasting,
+            $this->allowNonSequentialList,
+            $this->allowUndefinedValues,
             $this->allowSuperfluousKeys,
             $this->allowPermissiveTypes,
             $this->callableSignature($this->exceptionFilter),
diff --git a/src/Mapper/Tree/Builder/ArrayNodeBuilder.php b/src/Mapper/Tree/Builder/ArrayNodeBuilder.php
index 9f381b3..93e1025 100644
--- a/src/Mapper/Tree/Builder/ArrayNodeBuilder.php
+++ b/src/Mapper/Tree/Builder/ArrayNodeBuilder.php
@@ -28,7 +28,7 @@ final class ArrayNodeBuilder implements NodeBuilder
 
         assert($type instanceof ArrayType || $type instanceof NonEmptyArrayType || $type instanceof IterableType);
 
-        if ($shell->enableFlexibleCasting() && $value === null) {
+        if ($shell->allowUndefinedValues() && $value === null) {
             return TreeNode::branch($shell, [], []);
         }
 
diff --git a/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php b/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php
index cb49e5c..d36f53d 100644
--- a/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php
+++ b/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php
@@ -52,7 +52,7 @@ final class InterfaceNodeBuilder implements NodeBuilder
             return $this->delegate->build($shell, $rootBuilder);
         }
 
-        if ($shell->enableFlexibleCasting() && $shell->value() === null) {
+        if ($shell->allowUndefinedValues() && $shell->value() === null) {
             $shell = $shell->withValue([]);
         } else {
             $shell = $shell->transformIteratorToArray();
diff --git a/src/Mapper/Tree/Builder/ListNodeBuilder.php b/src/Mapper/Tree/Builder/ListNodeBuilder.php
index 0c3a052..014c275 100644
--- a/src/Mapper/Tree/Builder/ListNodeBuilder.php
+++ b/src/Mapper/Tree/Builder/ListNodeBuilder.php
@@ -27,7 +27,7 @@ final class ListNodeBuilder implements NodeBuilder
 
         assert($type instanceof ListType || $type instanceof NonEmptyListType);
 
-        if ($shell->enableFlexibleCasting() && $value === null) {
+        if ($shell->allowUndefinedValues() && $value === null) {
             return TreeNode::branch($shell, [], []);
         }
 
@@ -58,7 +58,7 @@ final class ListNodeBuilder implements NodeBuilder
                 throw new InvalidIterableKeyType($key, $shell->path());
             }
 
-            if ($shell->enableFlexibleCasting() || $key === $expected) {
+            if ($shell->allowNonSequentialList() || $key === $expected) {
                 $child = $shell->child((string)$expected, $subType);
                 $children[$expected] = $rootBuilder->build($child->withValue($value));
             } else {
diff --git a/src/Mapper/Tree/Builder/ObjectNodeBuilder.php b/src/Mapper/Tree/Builder/ObjectNodeBuilder.php
index 3bbbcf6..7478439 100644
--- a/src/Mapper/Tree/Builder/ObjectNodeBuilder.php
+++ b/src/Mapper/Tree/Builder/ObjectNodeBuilder.php
@@ -43,7 +43,7 @@ final class ObjectNodeBuilder implements NodeBuilder
             return TreeNode::leaf($shell, $shell->value());
         }
 
-        if ($shell->enableFlexibleCasting() && $shell->value() === null) {
+        if ($shell->allowUndefinedValues() && $shell->value() === null) {
             $shell = $shell->withValue([]);
         } else {
             $shell = $shell->transformIteratorToArray();
diff --git a/src/Mapper/Tree/Builder/RootNodeBuilder.php b/src/Mapper/Tree/Builder/RootNodeBuilder.php
index 724cd51..211cef6 100644
--- a/src/Mapper/Tree/Builder/RootNodeBuilder.php
+++ b/src/Mapper/Tree/Builder/RootNodeBuilder.php
@@ -21,7 +21,7 @@ final class RootNodeBuilder
     public function build(Shell $shell): TreeNode
     {
         if (! $shell->hasValue()) {
-            if (! $shell->enableFlexibleCasting()) {
+            if (! $shell->allowUndefinedValues()) {
                 return TreeNode::error($shell, new MissingNodeValue($shell->type()));
             }
 
diff --git a/src/Mapper/Tree/Builder/ScalarNodeBuilder.php b/src/Mapper/Tree/Builder/ScalarNodeBuilder.php
index 7d32fc6..80c7964 100644
--- a/src/Mapper/Tree/Builder/ScalarNodeBuilder.php
+++ b/src/Mapper/Tree/Builder/ScalarNodeBuilder.php
@@ -23,7 +23,7 @@ final class ScalarNodeBuilder implements NodeBuilder
             return TreeNode::leaf($shell, $value);
         }
 
-        if (! $shell->enableFlexibleCasting() || ! $type->canCast($value)) {
+        if (! $shell->allowScalarValueCasting() || ! $type->canCast($value)) {
             return TreeNode::error($shell, $type->errorMessage());
         }
 
diff --git a/src/Mapper/Tree/Shell.php b/src/Mapper/Tree/Shell.php
index 0438522..2724713 100644
--- a/src/Mapper/Tree/Shell.php
+++ b/src/Mapper/Tree/Shell.php
@@ -118,9 +118,17 @@ final class Shell
         return $this->value;
     }
 
-    public function enableFlexibleCasting(): bool
+    public function allowScalarValueCasting(): bool
     {
-        return $this->settings->enableFlexibleCasting;
+        return $this->settings->allowScalarValueCasting;
+    }
+    public function allowNonSequentialList(): bool
+    {
+        return $this->settings->allowNonSequentialList;
+    }
+    public function allowUndefinedValues(): bool
+    {
+        return $this->settings->allowUndefinedValues;
     }
 
     public function allowSuperfluousKeys(): bool
diff --git a/src/MapperBuilder.php b/src/MapperBuilder.php
index 9cc47e2..86c1376 100644
--- a/src/MapperBuilder.php
+++ b/src/MapperBuilder.php
@@ -302,72 +302,122 @@ final class MapperBuilder
     }
 
     /**
-     * Changes the behaviours explained below:
+     * This setting will be removed in a future major version, as a replacement
+     * the following methods should be used:
+     *
+     * - @see allowScalarValueCasting()
+     * - @see allowNonSequentialList()
+     * - @see allowUndefinedValues()
+     */
+    public function enableFlexibleCasting(): self
+    {
+        return $this
+            ->allowScalarValueCasting()
+            ->allowNonSequentialList()
+            ->allowUndefinedValues();
+    }
+
+    /**
+     * With this setting enabled, scalar types will accept castable values:
+     *
+     * - Integer types will accept any valid numeric value, for instance the
+     *   string value "42".
+     *
+     * - Float types will accept any valid numeric value, for instance the
+     *   string value "1337.42".
+     *
+     * - String types will accept any integer, float or object implementing the
+     *   `Stringable` interface.
+     *
+     * - Boolean types will accept any truthy or falsy value:
+     *     - "true" (string), "1" (string) and 1 (int) will be cast to `true`
+     *     - "false" (string), "0" (string) and 0 (int) will be cast to `false`
      *
      * ```php
-     * $flexibleMapper = (new \CuyZ\Valinor\MapperBuilder())
-     *     ->enableFlexibleCasting()
-     *     ->mapper();
-     *
-     * // ---
-     * // Scalar types will accept non-strict values; for instance an integer
-     * // type will accept any valid numeric value like the *string* "42".
-     *
-     * $flexibleMapper->map('int', '42');
-     * // => 42
-     *
-     * // ---
-     * // List type will accept non-incremental keys.
-     *
-     * $flexibleMapper->map('list<int>', ['foo' => 42, 'bar' => 1337]);
-     * // => [0 => 42, 1 => 1338]
-     *
-     * // ---
-     * // If a value is missing in a source for a node that accepts `null`, the
-     * // node will be filled with `null`.
-     *
-     * $flexibleMapper->map(
-     *     'array{foo: string, bar: null|string}',
-     *     ['foo' => 'foo'] // `bar` is missing
-     * );
-     * // => ['foo' => 'foo', 'bar' => null]
-     *
-     * // ---
-     * // Array and list types will convert `null` or missing values to an empty
-     * // array.
-     *
-     * $flexibleMapper->map(
-     *     'array{foo: string, bar: array<string>}',
-     *     ['foo' => 'foo'] // `bar` is missing
-     * );
-     * // => ['foo' => 'foo', 'bar' => []]
+     * (new \CuyZ\Valinor\MapperBuilder())
+     *     ->allowNonSequentialList()
+     *     ->mapper()
+     *     ->map('array{id: string, price: float, active: bool}', [
+     *         'id' => 549465210, // Will be cast to string
+     *         'price' => '42.39', // Will be cast to float
+     *         'active' => 1, // Will be cast to bool
+     *     ]);
      * ```
      */
-    public function enableFlexibleCasting(): self
+    public function allowScalarValueCasting(): self
     {
         $clone = clone $this;
-        $clone->settings->enableFlexibleCasting = true;
+        $clone->settings->allowScalarValueCasting = true;
 
         return $clone;
     }
 
     /**
-     * Superfluous keys in source arrays will be ignored, preventing errors when
-     * a value is not bound to any object property/parameter or shaped array
-     * element.
+     * By default, list types will only accept sequential keys starting from 0.
+     *
+     * This setting allows the mapper to convert associative arrays to a list
+     * with sequential keys.
+     *
+     * ```php
+     * (new \CuyZ\Valinor\MapperBuilder())
+     *     ->allowNonSequentialList()
+     *     ->mapper()
+     *     ->map('list<int>', [
+     *         'foo' => 42,
+     *         'bar' => 1337,
+     *     ]);
+     *
+     * // => [0 => 42, 1 => 1337]
+     * ```
+     */
+    public function allowNonSequentialList(): self
+    {
+        $clone = clone $this;
+        $clone->settings->allowNonSequentialList = true;
+
+        return $clone;
+    }
+
+    /**
+     * Allows the mapper to accept undefined values (missing from the input), by
+     * converting them to `null` (if the current type is nullable) or an empty
+     * array (if the current type is an object or an iterable).
+     *
+     * ```php
+     * (new \CuyZ\Valinor\MapperBuilder())
+     *     ->allowUndefinedValues()
+     *     ->mapper()
+     *     ->map('array{name: string, age: int|null}', [
+     *         'name' => 'John Doe',
+     *         // 'age' is not defined
+     *     ]);
+     *
+     * // => ['name' => 'John Doe', 'age' => null]
+     * ```
+     */
+    public function allowUndefinedValues(): self
+    {
+        $clone = clone $this;
+        $clone->settings->allowUndefinedValues = true;
+
+        return $clone;
+    }
+
+    /**
+     * By default, an error is raised when a source array contains keys that
+     * do not match a class property/parameter or a shaped array element.
+     *
+     * This setting allows the mapper to ignore these superfluous keys.
      *
      * ```php
      * (new \CuyZ\Valinor\MapperBuilder())
      *     ->allowSuperfluousKeys()
      *     ->mapper()
-     *     ->map(
-     *         'array{foo: string, bar: int}',
-     *         [
-     *             'foo' => 'foo',
-     *             'bar' => 42,
-     *             'baz' => 1337.404, // `baz` will be ignored
-     *         ]
-     *     );
+     *     ->map('array{name: string, age: int}', [
+     *         'name' => 'John Doe',
+     *         'age' => 42,
+     *         'city' => 'Paris', // Will be ignored
+     *     ]);
      * ```
      */
     public function allowSuperfluousKeys(): self
@@ -385,13 +435,10 @@ final class MapperBuilder
      * (new \CuyZ\Valinor\MapperBuilder())
      *     ->allowPermissiveTypes()
      *     ->mapper()
-     *     ->map(
-     *         'array{foo: string, bar: mixed}',
-     *         [
-     *             'foo' => 'foo',
-     *             'bar' => 42, // Could be any value
-     *         ]
-     *     );
+     *     ->map('array{name: string, data: mixed}', [
+     *         'name' => 'some_product',
+     *         'data' => 42, // Could be any value
+     *     ]);
      * ```
      */
     public function allowPermissiveTypes(): self


Test patch:
diff --git a/tests/Integration/Mapping/Other/FlexibleCastingMappingTest.php b/tests/Integration/Mapping/Other/FlexibleCastingMappingTest.php
deleted file mode 100644
index e8f36eb..0000000
--- a/tests/Integration/Mapping/Other/FlexibleCastingMappingTest.php
+++ /dev/null
@@ -1,353 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace CuyZ\Valinor\Tests\Integration\Mapping\Other;
-
-use CuyZ\Valinor\Mapper\MappingError;
-use CuyZ\Valinor\Mapper\TreeMapper;
-use CuyZ\Valinor\Tests\Fixture\Enum\BackedIntegerEnum;
-use CuyZ\Valinor\Tests\Fixture\Enum\BackedStringEnum;
-use CuyZ\Valinor\Tests\Fixture\Object\StringableObject;
-use CuyZ\Valinor\Tests\Integration\IntegrationTestCase;
-use stdClass;
-
-final class FlexibleCastingMappingTest extends IntegrationTestCase
-{
-    private TreeMapper $mapper;
-
-    protected function setUp(): void
-    {
-        parent::setUp();
-
-        $this->mapper = $this->mapperBuilder()->enableFlexibleCasting()->mapper();
-    }
-
-    public function test_leading_zero_in_numeric_is_mapped_properly(): void
-    {
-        $source = ['000', '040', '00040', '0001337.404'];
-
-        try {
-            $result = $this->mapper->map('array<int|float>', $source);
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame([0, 40, 40, 1337.404], $result);
-    }
-
-    public function test_leading_zero_in_integer_range_is_mapped_properly(): void
-    {
-        $source = ['060', '042', '000404'];
-
-        try {
-            $result = $this->mapper->map('array<int<1, 500>>', $source);
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame([60, 42, 404], $result);
-    }
-
-    public function test_leading_zero_in_integer_value_is_mapped_properly(): void
-    {
-        $source = ['000', '040', '000404'];
-
-        try {
-            $result = $this->mapper->map('array<0|40|404>', $source);
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame([0, 40, 404], $result);
-    }
-
-    public function test_leading_zero_in_positive_integer_is_mapped_properly(): void
-    {
-        $source = ['040', '000404'];
-
-        try {
-            $result = $this->mapper->map('array<positive-int>', $source);
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame([40, 404], $result);
-    }
-
-    public function test_leading_zero_in_non_negative_integer_is_mapped_properly(): void
-    {
-        $source = ['000', '040', '000404'];
-
-        try {
-            $result = $this->mapper->map('array<non-negative-int>', $source);
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame([0, 40, 404], $result);
-    }
-
-    public function test_array_of_scalars_is_mapped_properly(): void
-    {
-        $source = ['foo', 42, 1337.404];
-
-        try {
-            $result = $this->mapper->map('string[]', $source);
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame(['foo', '42', '1337.404'], $result);
-    }
-
-    public function test_shaped_array_is_mapped_correctly(): void
-    {
-        try {
-            $result = $this->mapper->map(
-                'array{string, foo: int, bar?: float}',
-                [
-                    'foo',
-                    'foo' => '42',
-                ]
-            );
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame(['foo', 'foo' => 42], $result);
-    }
-
-    public function test_string_enum_is_cast_correctly(): void
-    {
-        try {
-            $result = $this->mapper->map(BackedStringEnum::class, new StringableObject('foo'));
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame(BackedStringEnum::FOO, $result);
-    }
-
-    public function test_integer_enum_is_cast_correctly(): void
-    {
-        try {
-            $result = $this->mapper->map(BackedIntegerEnum::class, '42');
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame(BackedIntegerEnum::FOO, $result);
-    }
-
-    public function test_list_filled_with_associative_array_is_converted_to_list(): void
-    {
-        try {
-            $result = $this->mapper->map(
-                'list<string>',
-                [
-                    'foo' => 'foo',
-                    'bar' => 'bar',
-                ]
-            );
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame(['foo', 'bar'], $result);
-    }
-
-    public function test_null_value_for_class_fills_it_with_empty_array(): void
-    {
-        try {
-            $this->mapper->map(stdClass::class, null);
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::expectNotToPerformAssertions();
-    }
-
-    public function test_null_value_for_interface_with_no_properties_needed_fills_it_with_empty_array(): void
-    {
-        try {
-            $result = $this->mapperBuilder()
-                ->infer(SomeInterfaceForClassWithNoProperties::class, fn () => SomeClassWithNoProperties::class)
-                ->enableFlexibleCasting()
-                ->mapper()
-                ->map(SomeInterfaceForClassWithNoProperties::class, null);
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertInstanceOf(SomeClassWithNoProperties::class, $result);
-    }
-
-    public function test_interface_is_inferred_and_mapped_properly_in_flexible_casting_mode(): void
-    {
-        try {
-            $result = $this->mapperBuilder()
-                ->infer(SomeInterfaceForClassWithProperties::class, fn () => SomeClassWithProperties::class)
-                ->enableFlexibleCasting()
-                ->mapper()
-                ->map(SomeInterfaceForClassWithProperties::class, [
-                    'foo' => 'foo',
-                    'bar' => 'bar',
-                ]);
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertInstanceOf(SomeClassWithProperties::class, $result);
-        self::assertSame('foo', $result->foo);
-        self::assertSame('bar', $result->bar);
-    }
-
-    public function test_missing_value_for_array_fills_it_with_empty_array(): void
-    {
-        try {
-            $result = $this->mapper->map(
-                'array{foo: string, bar: array<string>}',
-                ['foo' => 'foo']
-            );
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame('foo', $result['foo']);
-        self::assertSame([], $result['bar']);
-    }
-
-    public function test_null_value_for_array_fills_it_with_empty_array(): void
-    {
-        try {
-            $result = $this->mapper->map(
-                'array{foo: string, bar: array<string>}',
-                [
-                    'foo' => 'foo',
-                    'bar' => null,
-                ]
-            );
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame('foo', $result['foo']);
-        self::assertSame([], $result['bar']);
-    }
-
-    public function test_missing_value_for_list_fills_it_with_empty_array(): void
-    {
-        try {
-            $result = $this->mapper->map(
-                'array{foo: string, bar: list<string>}',
-                ['foo' => 'foo']
-            );
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame('foo', $result['foo']);
-        self::assertSame([], $result['bar']);
-    }
-
-    public function test_null_value_for_list_fills_it_with_empty_array(): void
-    {
-        try {
-            $result = $this->mapper->map(
-                'array{foo: string, bar: list<string>}',
-                [
-                    'foo' => 'foo',
-                    'bar' => null,
-                ]
-            );
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame('foo', $result['foo']);
-        self::assertSame([], $result['bar']);
-    }
-
-    public function test_missing_value_for_nullable_property_fills_it_with_null(): void
-    {
-        $class = new class () {
-            public string $foo;
-
-            public ?string $bar;
-        };
-
-        try {
-            $result = $this->mapper->map(
-                $class::class,
-                ['foo' => 'foo']
-            );
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame('foo', $result->foo);
-        self::assertSame(null, $result->bar);
-    }
-
-    public function test_missing_value_for_nullable_shaped_array_element_fills_it_with_null(): void
-    {
-        try {
-            $result = $this->mapper->map(
-                'array{foo: string, bar: ?string}',
-                ['foo' => 'foo']
-            );
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame('foo', $result['foo']);
-        self::assertSame(null, $result['bar']);
-    }
-
-    public function test_filled_source_value_is_casted_when_union_contains_three_types_including_null(): void
-    {
-        try {
-            $result = $this->mapper->map('null|int|string', new StringableObject('foo'));
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame('foo', $result);
-    }
-
-    public function test_source_value_is_casted_when_other_type_cannot_be_casted(): void
-    {
-        try {
-            $result = $this->mapper->map('string[]|string', new StringableObject('foo'));
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame('foo', $result);
-    }
-
-    public function test_source_can_be_casted_to_array_key(): void
-    {
-        try {
-            $result = $this->mapper->map('array-key', new StringableObject('foo'));
-        } catch (MappingError $error) {
-            $this->mappingFail($error);
-        }
-
-        self::assertSame('foo', $result);
-    }
-}
-
-interface SomeInterfaceForClassWithNoProperties {}
-
-final class SomeClassWithNoProperties implements SomeInterfaceForClassWithNoProperties {}
-
-interface SomeInterfaceForClassWithProperties {}
-
-final class SomeClassWithProperties implements SomeInterfaceForClassWithProperties
-{
-    public string $foo;
-
-    public string $bar;
-}
diff --git a/tests/Integration/Mapping/Other/NonSequentialListAllowedMappingTest.php b/tests/Integration/Mapping/Other/NonSequentialListAllowedMappingTest.php
new file mode 100644
index 0000000..9c7842e
--- /dev/null
+++ b/tests/Integration/Mapping/Other/NonSequentialListAllowedMappingTest.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace CuyZ\Valinor\Tests\Integration\Mapping\Other;
+
+use CuyZ\Valinor\Mapper\MappingError;
+use CuyZ\Valinor\Tests\Integration\IntegrationTestCase;
+
+final class NonSequentialListAllowedMappingTest extends IntegrationTestCase
+{
+    public function test_associative_array_is_converted_to_list(): void
+    {
+        try {
+            $result = $this
+                ->mapperBuilder()
+                ->allowNonSequentialList()
+                ->mapper()
+                ->map('list<string>', [
+                    'foo' => 'foo',
+                    'bar' => 'bar',
+                ]);
+
+            self::assertSame(['foo', 'bar'], $result);
+        } catch (MappingError $error) {
+            $this->mappingFail($error);
+        }
+    }
+}
diff --git a/tests/Integration/Mapping/Other/ScalarValueCastingMappingTest.php b/tests/Integration/Mapping/Other/ScalarValueCastingMappingTest.php
new file mode 100644
index 0000000..1f11b5b
--- /dev/null
+++ b/tests/Integration/Mapping/Other/ScalarValueCastingMappingTest.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+namespace CuyZ\Valinor\Tests\Integration\Mapping\Other;
+
+use CuyZ\Valinor\Mapper\MappingError;
+use CuyZ\Valinor\Tests\Fixture\Enum\BackedIntegerEnum;
+use CuyZ\Valinor\Tests\Fixture\Enum\BackedStringEnum;
+use CuyZ\Valinor\Tests\Fixture\Object\StringableObject;
+use CuyZ\Valinor\Tests\Integration\IntegrationTestCase;
+use PHPUnit\Framework\Attributes\TestWith;
+
+final class ScalarValueCastingMappingTest extends IntegrationTestCase
+{
+    #[TestWith(['type' => 'int', 'value' => '000', 'expected' => 0])]
+    #[TestWith(['type' => 'int', 'value' => '040', 'expected' => 40])]
+    #[TestWith(['type' => 'int', 'value' => '00040', 'expected' => 40])]
+    #[TestWith(['type' => 'float', 'value' => '0001337.404', 'expected' => 1337.404])]
+    #[TestWith(['type' => 'int<1, 500>', 'value' => '060', 'expected' => 60])]
+    #[TestWith(['type' => 'int<1, 500>', 'value' => '042', 'expected' => 42])]
+    #[TestWith(['type' => 'int<1, 500>', 'value' => '000404', 'expected' => 404])]
+    #[TestWith(['type' => '0|40|404', 'value' => '000', 'expected' => 0])]
+    #[TestWith(['type' => '0|40|404', 'value' => '040', 'expected' => 40])]
+    #[TestWith(['type' => '0|40|404', 'value' => '000404', 'expected' => 404])]
+    #[TestWith(['type' => 'positive-int', 'value' => '040', 'expected' => 40])]
+    #[TestWith(['type' => 'positive-int', 'value' => '000404', 'expected' => 404])]
+    #[TestWith(['type' => 'non-negative-int', 'value' => '000', 'expected' => 0])]
+    #[TestWith(['type' => 'non-negative-int', 'value' => '040', 'expected' => 40])]
+    #[TestWith(['type' => 'non-negative-int', 'value' => '000404', 'expected' => 404])]
+    #[TestWith(['type' => 'string', 'value' => '42', 'expected' => '42'])]
+    #[TestWith(['type' => 'string', 'value' => '1337.404', 'expected' => '1337.404'])]
+    #[TestWith(['type' => 'array{string, foo: int, bar?: float}', 'value' => ['hello', 'foo' => '42'], 'expected' => ['hello', 'foo' => 42]])]
+    #[TestWith(['type' => BackedStringEnum::class, 'value' => new StringableObject('foo'), 'expected' => BackedStringEnum::FOO])]
+    #[TestWith(['type' => BackedIntegerEnum::class, 'value' => '42', 'expected' => BackedIntegerEnum::FOO])]
+    #[TestWith(['type' => 'null|int|string', 'value' => new StringableObject('foo'), 'expected' => 'foo'])]
+    #[TestWith(['type' => 'string[]|string', 'value' => new StringableObject('foo'), 'expected' => 'foo'])]
+    #[TestWith(['type' => 'array-key', 'value' => new StringableObject('foo'), 'expected' => 'foo'])]
+    public function test_scalar_values_are_casted_properly(string $type, mixed $value, mixed $expected): void
+    {
+        try {
+            $result = $this
+                ->mapperBuilder()
+                ->allowScalarValueCasting()
+                ->mapper()
+                ->map($type, $value);
+
+            self::assertSame($expected, $result);
+        } catch (MappingError $error) {
+            $this->mappingFail($error);
+        }
+    }
+}
diff --git a/tests/Integration/Mapping/Other/UndefinedValuesMappingTest.php b/tests/Integration/Mapping/Other/UndefinedValuesMappingTest.php
new file mode 100644
index 0000000..a8f3310
--- /dev/null
+++ b/tests/Integration/Mapping/Other/UndefinedValuesMappingTest.php
@@ -0,0 +1,135 @@
+<?php
+
+declare(strict_types=1);
+
+namespace CuyZ\Valinor\Tests\Integration\Mapping\Other;
+
+use CuyZ\Valinor\Mapper\MappingError;
+use CuyZ\Valinor\Tests\Integration\IntegrationTestCase;
+use PHPUnit\Framework\Attributes\TestWith;
+use stdClass;
+
+final class UndefinedValuesMappingTest extends IntegrationTestCase
+{
+    #[TestWith(['type' => 'array{foo: string, bar: array<string>}', 'value' => ['foo' => 'foo'], 'expected' => ['foo' => 'foo', 'bar' => []]])]
+    #[TestWith(['type' => 'array{foo: string, bar: array<string>}', 'value' => ['foo' => 'foo', 'bar' => null], 'expected' => ['foo' => 'foo', 'bar' => []]])]
+    #[TestWith(['type' => 'array{foo: string, bar: list<string>}', 'value' => ['foo' => 'foo'], 'expected' => ['foo' => 'foo', 'bar' => []]])]
+    #[TestWith(['type' => 'array{foo: string, bar: list<string>}', 'value' => ['foo' => 'foo', 'bar' => null], 'expected' => ['foo' => 'foo', 'bar' => []]])]
+    public function test_array_types_can_accept_undefined_values(string $type, mixed $value, mixed $expected): void
+    {
+        try {
+            $result = $this
+                ->mapperBuilder()
+                ->allowUndefinedValues()
+                ->mapper()
+                ->map($type, $value);
+
+            self::assertSame($expected, $result);
+        } catch (MappingError $error) {
+            $this->mappingFail($error);
+        }
+    }
+
+    public function test_null_value_for_class_fills_it_with_empty_array(): void
+    {
+        try {
+            $this
+                ->mapperBuilder()
+                ->allowUndefinedValues()
+                ->mapper()
+                ->map(stdClass::class, null);
+
+            self::expectNotToPerformAssertions();
+        } catch (MappingError $error) {
+            $this->mappingFail($error);
+        }
+    }
+
+    public function test_null_value_for_interface_with_no_properties_needed_fills_it_with_empty_array(): void
+    {
+        try {
+            $result = $this->mapperBuilder()
+                ->infer(SomeInterfaceForClassWithNoProperties::class, fn () => SomeClassWithNoProperties::class)
+                ->allowUndefinedValues()
+                ->mapper()
+                ->map(SomeInterfaceForClassWithNoProperties::class, null);
+
+            self::assertInstanceOf(SomeClassWithNoProperties::class, $result);
+        } catch (MappingError $error) {
+            $this->mappingFail($error);
+        }
+    }
+
+    public function test_missing_value_for_nullable_property_fills_it_with_null(): void
+    {
+        $class = new class () {
+            public string $foo;
+
+            public ?string $bar;
+        };
+
+        try {
+            $result = $this->mapperBuilder()
+                ->infer(SomeInterfaceForClassWithNoProperties::class, fn () => SomeClassWithNoProperties::class)
+                ->allowUndefinedValues()
+                ->mapper()
+                ->map($class::class, ['foo' => 'foo']);
+
+            self::assertSame('foo', $result->foo);
+            self::assertSame(null, $result->bar);
+        } catch (MappingError $error) {
+            $this->mappingFail($error);
+        }
+    }
+
+    public function test_missing_value_for_nullable_shaped_array_element_fills_it_with_null(): void
+    {
+        try {
+            $result = $this->mapperBuilder()
+                ->infer(SomeInterfaceForClassWithNoProperties::class, fn () => SomeClassWithNoProperties::class)
+                ->allowUndefinedValues()
+                ->mapper()
+                ->map(
+                    'array{foo: string, bar: ?string}',
+                    ['foo' => 'foo'],
+                );
+
+            self::assertSame('foo', $result['foo']);
+            self::assertSame(null, $result['bar']);
+        } catch (MappingError $error) {
+            $this->mappingFail($error);
+        }
+    }
+
+    public function test_array_type_does_not_accept_null_when_undefined_values_are_not_allowed(): void
+    {
+        try {
+            $this
+                ->mapperBuilder()
+                ->mapper()
+                ->map('array<string>', null);
+        } catch (MappingError $exception) {
+            self::assertMappingErrors($exception, [
+                '*root*' => '[1618739163] Cannot be empty and must be filled with a value matching type `array<string>`.',
+            ]);
+        }
+    }
+
+    public function test_list_type_does_not_accept_null_when_undefined_values_are_not_allowed(): void
+    {
+        try {
+            $this
+                ->mapperBuilder()
+                ->mapper()
+                ->map('list<string>', null);
+        } catch (MappingError $exception) {
+            self::assertMappingErrors($exception, [
+                '*root*' => '[1618739163] Cannot be empty and must be filled with a value matching type `list<string>`.',
+            ]);
+        }
+    }
+}
+
+interface SomeInterfaceForClassWithNoProperties {}
+
+final class SomeClassWithNoProperties implements SomeInterfaceForClassWithNoProperties {}
diff --git a/tests/Integration/Mapping/UnionMappingTest.php b/tests/Integration/Mapping/UnionMappingTest.php
index ceeac6e..74b2455 100644
--- a/tests/Integration/Mapping/UnionMappingTest.php
+++ b/tests/Integration/Mapping/UnionMappingTest.php
@@ -41,12 +41,12 @@ final class UnionMappingTest extends IntegrationTestCase
         }
     }
 
-    #[DataProvider('union_mapping_works_properly_with_flexible_casting_enabled_data_provider')]
-    public function test_union_mapping_works_properly_with_flexible_casting_enabled(string $type, mixed $source, callable $assertion): void
+    #[DataProvider('union_mapping_works_properly_with_scalar_value_casting_enabled_data_provider')]
+    public function test_union_mapping_works_properly_with_scalar_value_casting_enabled(string $type, mixed $source, callable $assertion): void
     {
         try {
             $result = $this->mapperBuilder()
-                ->enableFlexibleCasting()
+                ->allowScalarValueCasting()
                 ->mapper()
                 ->map($type, $source);
 
@@ -223,7 +223,7 @@ final class UnionMappingTest extends IntegrationTestCase
         ];
     }
 
-    public static function union_mapping_works_properly_with_flexible_casting_enabled_data_provider(): iterable
+    public static function union_mapping_works_properly_with_scalar_value_casting_enabled_data_provider(): iterable
     {
         yield 'float or integer, with string containing float' => [
             'type' => 'float|integer',
diff --git a/tests/Unit/Library/SettingsTest.php b/tests/Unit/Library/SettingsTest.php
index 54623de..58dc99c 100644
--- a/tests/Unit/Library/SettingsTest.php
+++ b/tests/Unit/Library/SettingsTest.php
@@ -21,7 +21,7 @@ final class SettingsTest extends TestCase
     {
         $settings = new Settings();
 
-        self::assertSame('828a4469bd360f998e58d350c99df248', $settings->hash());
+        self::assertSame('2a24fb52f61377b93f562e764ed4dbec', $settings->hash());
     }
 
     public function test_settings_hash(): void
@@ -33,13 +33,15 @@ final class SettingsTest extends TestCase
         $settings->valueModifier[] = fn (string $value): string => $value;
         $settings->cache = $this->createMock(CacheInterface::class);
         $settings->supportedDateFormats = ['Y-m-d\\TH:i:sP'];
-        $settings->enableFlexibleCasting = true;
+        $settings->allowScalarValueCasting = true;
+        $settings->allowNonSequentialList = true;
+        $settings->allowUndefinedValues = true;
         $settings->allowSuperfluousKeys = true;
         $settings->allowPermissiveTypes = true;
         $settings->exceptionFilter = fn (Throwable $e) => throw $e;
         $settings->transformers[] = [fn (mixed $value): mixed => $value];
         $settings->transformerAttributes[stdClass::class] = null;
 
-        self::assertSame('01b19d2cccc8cf73ac7c5e7e3b14a688', $settings->hash());
+        self::assertSame('0beb5be3f63aae0ba6e8c9b0f1040301', $settings->hash());
     }
 }
diff --git a/tests/Unit/Mapper/Tree/Builder/ArrayNodeBuilderTest.php b/tests/Unit/Mapper/Tree/Builder/ArrayNodeBuilderTest.php
index 7172061..2fefea8 100644
--- a/tests/Unit/Mapper/Tree/Builder/ArrayNodeBuilderTest.php
+++ b/tests/Unit/Mapper/Tree/Builder/ArrayNodeBuilderTest.php
@@ -15,10 +15,10 @@ use PHPUnit\Framework\TestCase;
 
 final class ArrayNodeBuilderTest extends TestCase
 {
-    public function test_build_with_null_value_in_flexible_mode_returns_empty_branch_node(): void
+    public function test_build_with_null_value_and_undefined_values_allowed_returns_empty_branch_node(): void
     {
         $setting = new Settings();
-        $setting->enableFlexibleCasting = true;
+        $setting->allowUndefinedValues = true;
 
         $shell = FakeShell::new(ArrayType::native(), settings: $setting);
 
diff --git a/tests/Unit/Mapper/Tree/Builder/ListNodeBuilderTest.php b/tests/Unit/Mapper/Tree/Builder/ListNodeBuilderTest.php
index 312b7ee..8d6f0be 100644
--- a/tests/Unit/Mapper/Tree/Builder/ListNodeBuilderTest.php
+++ b/tests/Unit/Mapper/Tree/Builder/ListNodeBuilderTest.php
@@ -15,10 +15,10 @@ use PHPUnit\Framework\TestCase;
 
 final class ListNodeBuilderTest extends TestCase
 {
-    public function test_build_with_null_value_in_flexible_mode_returns_empty_branch_node(): void
+    public function test_build_with_null_value_and_undefined_values_allowed_returns_empty_branch_node(): void
     {
         $setting = new Settings();
-        $setting->enableFlexibleCasting = true;
+        $setting->allowUndefinedValues = true;
 
         $shell = FakeShell::new(ListType::native(), settings: $setting);
 
diff --git a/tests/Unit/MapperBuilderTest.php b/tests/Unit/MapperBuilderTest.php
index 0f97a69..4b56e3d 100644
--- a/tests/Unit/MapperBuilderTest.php
+++ b/tests/Unit/MapperBuilderTest.php
@@ -30,12 +30,15 @@ final class MapperBuilderTest extends TestCase
         $builderC = $builderA->registerConstructor(static fn (): stdClass => new stdClass());
         $builderD = $builderA->alter(static fn (string $value): string => 'foo');
         $builderE = $builderA->enableFlexibleCasting();
-        $builderF = $builderA->allowSuperfluousKeys();
-        $builderG = $builderA->allowPermissiveTypes();
-        $builderH = $builderA->filterExceptions(fn () => new FakeErrorMessage());
-        $builderI = $builderA->withCache(new FakeCache());
-        $builderJ = $builderA->supportDateFormats('Y-m-d');
-        $builderK = $builderA->registerTransformer(fn (stdClass $object) => 'foo');
+        $builderF = $builderA->allowScalarValueCasting();
+        $builderG = $builderA->allowNonSequentialList();
+        $builderH = $builderA->allowUndefinedValues();
+        $builderI = $builderA->allowSuperfluousKeys();
+        $builderJ = $builderA->allowPermissiveTypes();
+        $builderK = $builderA->filterExceptions(fn () => new FakeErrorMessage());
+        $builderL = $builderA->withCache(new FakeCache());
+        $builderM = $builderA->supportDateFormats('Y-m-d');
+        $builderN = $builderA->registerTransformer(fn (stdClass $object) => 'foo');
 
         self::assertNotSame($builderA, $builderB);
         self::assertNotSame($builderA, $builderC);
@@ -47,6 +50,9 @@ final class MapperBuilderTest extends TestCase
         self::assertNotSame($builderA, $builderI);
         self::assertNotSame($builderA, $builderJ);
         self::assertNotSame($builderA, $builderK);
+        self::assertNotSame($builderA, $builderL);
+        self::assertNotSame($builderA, $builderM);
+        self::assertNotSame($builderA, $builderN);
     }
 
     public function test_mapper_instance_is_the_same(): void