Repository: Lomkit/laravel-rest-api (php)

Issue:
Better validation errors
### Description

Many errors keys doesn't fully provide the path but the message does. This is due to using rules class.

For example:

```php
$response->assertStatus(422);
$response->assertJsonStructure(['message', 'errors' => ['instructions.0']]);
```

Pull request description:
:recycle: better validation errors
closes #17 

Patch:
diff --git a/src/Concerns/PerformsQueries.php b/src/Concerns/PerformsQueries.php
index a55350c..0160486 100644
--- a/src/Concerns/PerformsQueries.php
+++ b/src/Concerns/PerformsQueries.php
@@ -9,13 +9,13 @@ use Lomkit\Rest\Http\Requests\RestRequest;
 trait PerformsQueries
 {
     /**
-     * Build a query for searching resource.
+     * Build a "search" query for fetching resource.
      *
      * @param  \Lomkit\Rest\Http\Requests\RestRequest  $request
      * @param  \Illuminate\Contracts\Database\Eloquent\Builder  $query
      * @return \Illuminate\Contracts\Database\Eloquent\Builder
      */
-    public function searchQuery(RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query) {
+    public function searchQuery(\Lomkit\Rest\Http\Requests\RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query) {
         return $query;
     }
 
@@ -26,7 +26,7 @@ trait PerformsQueries
      * @param  \Illuminate\Contracts\Database\Eloquent\Builder  $query
      * @return \Illuminate\Contracts\Database\Eloquent\Builder
      */
-    public function mutateQuery(RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query) {
+    public function mutateQuery(\Lomkit\Rest\Http\Requests\RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query) {
         return $query;
     }
 
@@ -37,7 +37,7 @@ trait PerformsQueries
      * @param  \Illuminate\Contracts\Database\Eloquent\Builder  $query
      * @return \Illuminate\Contracts\Database\Eloquent\Builder
      */
-    public function destroyQuery(RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query)
+    public function destroyQuery(\Lomkit\Rest\Http\Requests\RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query)
     {
         return $query;
     }
@@ -49,7 +49,7 @@ trait PerformsQueries
      * @param  \Illuminate\Contracts\Database\Eloquent\Builder  $query
      * @return \Illuminate\Contracts\Database\Eloquent\Builder
      */
-    public function restoreQuery(RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query)
+    public function restoreQuery(\Lomkit\Rest\Http\Requests\RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query)
     {
         return $query;
     }
@@ -61,7 +61,7 @@ trait PerformsQueries
      * @param  \Illuminate\Contracts\Database\Eloquent\Builder  $query
      * @return \Illuminate\Contracts\Database\Eloquent\Builder
      */
-    public function forceDeleteQuery(RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query)
+    public function forceDeleteQuery(\Lomkit\Rest\Http\Requests\RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query)
     {
         return $query;
     }
diff --git a/src/Console/stubs/base-resource.stub b/src/Console/stubs/base-resource.stub
index 9a6ad27..6c31d55 100644
--- a/src/Console/stubs/base-resource.stub
+++ b/src/Console/stubs/base-resource.stub
@@ -7,13 +7,24 @@ use Lomkit\Rest\Http\Resource as RestResource;
 abstract class Resource extends RestResource
 {
     /**
-     * Build a query for fetching resource.
+     * Build a "search" query for fetching resource.
      *
      * @param  \Lomkit\Rest\Http\Requests\RestRequest  $request
      * @param  \Illuminate\Contracts\Database\Eloquent\Builder  $query
      * @return \Illuminate\Contracts\Database\Eloquent\Builder
      */
-    public function fetchQuery(\Lomkit\Rest\Http\Requests\RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query) {
+    public function searchQuery(\Lomkit\Rest\Http\Requests\RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query) {
+        return $query;
+    }
+
+    /**
+     * Build a query for mutating resource.
+     *
+     * @param  \Lomkit\Rest\Http\Requests\RestRequest  $request
+     * @param  \Illuminate\Contracts\Database\Eloquent\Builder  $query
+     * @return \Illuminate\Contracts\Database\Eloquent\Builder
+     */
+    public function mutateQuery(\Lomkit\Rest\Http\Requests\RestRequest $request, \Illuminate\Contracts\Database\Eloquent\Builder $query) {
         return $query;
     }
 
diff --git a/src/Http/Requests/ActionsRequest.php b/src/Http/Requests/ActionsRequest.php
index 6831936..99c0303 100644
--- a/src/Http/Requests/ActionsRequest.php
+++ b/src/Http/Requests/ActionsRequest.php
@@ -18,7 +18,7 @@ use Lomkit\Rest\Relations\MorphMany;
 use Lomkit\Rest\Relations\MorphToMany;
 use Lomkit\Rest\Rules\CustomRulable;
 use Lomkit\Rest\Rules\Includable;
-use Lomkit\Rest\Rules\RequiredRelation;
+use Lomkit\Rest\Rules\RequiredRelationOnCreation;
 
 class ActionsRequest extends RestRequest
 {
diff --git a/src/Http/Requests/DetailRequest.php b/src/Http/Requests/DetailRequest.php
index 7ccddfa..3a45471 100644
--- a/src/Http/Requests/DetailRequest.php
+++ b/src/Http/Requests/DetailRequest.php
@@ -22,7 +22,7 @@ use Lomkit\Rest\Relations\MorphToMany;
 use Lomkit\Rest\Rules\ActionField;
 use Lomkit\Rest\Rules\CustomRulable;
 use Lomkit\Rest\Rules\Includable;
-use Lomkit\Rest\Rules\RequiredRelation;
+use Lomkit\Rest\Rules\RequiredRelationOnCreation;
 use Symfony\Component\HttpKernel\Exception\HttpException;
 
 class DetailRequest extends RestRequest
diff --git a/src/Http/Requests/MutateRequest.php b/src/Http/Requests/MutateRequest.php
index 2c1d267..b5821c2 100644
--- a/src/Http/Requests/MutateRequest.php
+++ b/src/Http/Requests/MutateRequest.php
@@ -18,7 +18,7 @@ use Lomkit\Rest\Relations\MorphMany;
 use Lomkit\Rest\Relations\MorphToMany;
 use Lomkit\Rest\Rules\CustomRulable;
 use Lomkit\Rest\Rules\Includable;
-use Lomkit\Rest\Rules\RequiredRelation;
+use Lomkit\Rest\Rules\RequiredRelationOnCreation;
 
 class MutateRequest extends RestRequest
 {
diff --git a/src/Http/Requests/OperateRequest.php b/src/Http/Requests/OperateRequest.php
index 611dd07..980d4f1 100644
--- a/src/Http/Requests/OperateRequest.php
+++ b/src/Http/Requests/OperateRequest.php
@@ -22,7 +22,7 @@ use Lomkit\Rest\Relations\MorphToMany;
 use Lomkit\Rest\Rules\ActionField;
 use Lomkit\Rest\Rules\CustomRulable;
 use Lomkit\Rest\Rules\Includable;
-use Lomkit\Rest\Rules\RequiredRelation;
+use Lomkit\Rest\Rules\RequiredRelationOnCreation;
 use Symfony\Component\HttpKernel\Exception\HttpException;
 
 class OperateRequest extends RestRequest
diff --git a/src/Relations/Relation.php b/src/Relations/Relation.php
index bef5ae7..ce307f7 100644
--- a/src/Relations/Relation.php
+++ b/src/Relations/Relation.php
@@ -12,7 +12,7 @@ use Lomkit\Rest\Concerns\Makeable;
 use Lomkit\Rest\Http\Resource;
 use Lomkit\Rest\Relations\Traits\Constrained;
 use Lomkit\Rest\Relations\Traits\Mutates;
-use Lomkit\Rest\Rules\RequiredRelation;
+use Lomkit\Rest\Rules\RequiredRelationOnCreation;
 
 class Relation implements \JsonSerializable
 {
@@ -89,11 +89,10 @@ class Relation implements \JsonSerializable
         if ($this->isRequiredOnCreation(
             app()->make(RestRequest::class)
         )) {
-            $rules[$prefix] = RequiredRelation::make()->resource($resource);
+            $rules[$prefix] = [RequiredRelationOnCreation::make()->resource($resource)];
         }
 
         if (in_array(HasPivotFields::class, class_uses_recursive($this), true)) {
-
             $pivotPrefix = $prefix;
             if ($this->hasMultipleEntries()) {
                 $pivotPrefix .= '.*';
diff --git a/src/Rules/ActionField.php b/src/Rules/ActionField.php
index 92e0fe3..ce34cc9 100644
--- a/src/Rules/ActionField.php
+++ b/src/Rules/ActionField.php
@@ -5,6 +5,7 @@ namespace Lomkit\Rest\Rules;
 use Closure;
 use Illuminate\Contracts\Validation\DataAwareRule;
 use Illuminate\Contracts\Validation\Rule;
+use Illuminate\Contracts\Validation\ValidationRule;
 use Illuminate\Contracts\Validation\ValidatorAwareRule;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Validator;
@@ -13,7 +14,7 @@ use Lomkit\Rest\Http\Requests\RestRequest;
 use Lomkit\Rest\Http\Requests\SearchRequest;
 use Lomkit\Rest\Http\Resource;
 
-class ActionField implements Rule, DataAwareRule, ValidatorAwareRule
+class ActionField implements ValidationRule, DataAwareRule, ValidatorAwareRule
 {
 
     use Makeable;
@@ -59,20 +60,6 @@ class ActionField implements Rule, DataAwareRule, ValidatorAwareRule
         return $this;
     }
 
-    public function passes($attribute, $value)
-    {
-        $validator = Validator::make(
-            $this->data,
-            $this->buildValidationRules($attribute, $value)
-        );
-
-        if ($validator->fails()) {
-            return $this->fail($validator->messages()->all());
-        }
-
-        return true;
-    }
-
     /**
      * Build the array of underlying validation rules based on the current state.
      *
@@ -143,4 +130,14 @@ class ActionField implements Rule, DataAwareRule, ValidatorAwareRule
 
         return $this;
     }
+
+    public function validate(string $attribute, mixed $value, Closure $fail): void
+    {
+        $validator = Validator::make(
+            $this->data,
+            $this->buildValidationRules($attribute, $value)
+        );
+
+        $validator->validate();
+    }
 }
\ No newline at end of file
diff --git a/src/Rules/AggregateFilterable.php b/src/Rules/AggregateFilterable.php
index 609fe0f..513738d 100644
--- a/src/Rules/AggregateFilterable.php
+++ b/src/Rules/AggregateFilterable.php
@@ -5,6 +5,7 @@ namespace Lomkit\Rest\Rules;
 use Closure;
 use Illuminate\Contracts\Validation\DataAwareRule;
 use Illuminate\Contracts\Validation\Rule;
+use Illuminate\Contracts\Validation\ValidationRule;
 use Illuminate\Contracts\Validation\ValidatorAwareRule;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Validator;
@@ -12,7 +13,7 @@ use Lomkit\Rest\Concerns\Makeable;
 use Lomkit\Rest\Http\Requests\SearchRequest;
 use Lomkit\Rest\Http\Resource;
 
-class AggregateFilterable implements Rule, DataAwareRule, ValidatorAwareRule
+class AggregateFilterable implements ValidationRule, DataAwareRule, ValidatorAwareRule
 {
 
     use Makeable;
@@ -58,20 +59,6 @@ class AggregateFilterable implements Rule, DataAwareRule, ValidatorAwareRule
         return $this;
     }
 
-    public function passes($attribute, $value)
-    {
-        $validator = Validator::make(
-            $this->data,
-            $this->buildValidationRules($attribute, $value)
-        );
-
-        if ($validator->fails()) {
-            return $this->fail($validator->messages()->all());
-        }
-
-        return true;
-    }
-
     /**
      * Build the array of underlying validation rules based on the current state.
      *
@@ -142,4 +129,14 @@ class AggregateFilterable implements Rule, DataAwareRule, ValidatorAwareRule
 
         return $this;
     }
+
+    public function validate(string $attribute, mixed $value, Closure $fail): void
+    {
+        $validator = Validator::make(
+            $this->data,
+            $this->buildValidationRules($attribute, $value)
+        );
+
+        $validator->validate();
+    }
 }
\ No newline at end of file
diff --git a/src/Rules/CustomRulable.php b/src/Rules/CustomRulable.php
index fb9e406..ecb7ef6 100644
--- a/src/Rules/CustomRulable.php
+++ b/src/Rules/CustomRulable.php
@@ -5,6 +5,7 @@ namespace Lomkit\Rest\Rules;
 use Closure;
 use Illuminate\Contracts\Validation\DataAwareRule;
 use Illuminate\Contracts\Validation\Rule;
+use Illuminate\Contracts\Validation\ValidationRule;
 use Illuminate\Contracts\Validation\ValidatorAwareRule;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Validator;
@@ -13,7 +14,7 @@ use Lomkit\Rest\Http\Requests\RestRequest;
 use Lomkit\Rest\Http\Requests\SearchRequest;
 use Lomkit\Rest\Http\Resource;
 
-class CustomRulable implements Rule, DataAwareRule, ValidatorAwareRule
+class CustomRulable implements ValidationRule, DataAwareRule, ValidatorAwareRule
 {
 
     use Makeable;
@@ -59,20 +60,6 @@ class CustomRulable implements Rule, DataAwareRule, ValidatorAwareRule
         return $this;
     }
 
-    public function passes($attribute, $value)
-    {
-        $validator = Validator::make(
-            $this->data,
-            $this->buildValidationRules($attribute, $value)
-        );
-
-        if ($validator->fails()) {
-            return $this->fail($validator->messages()->all());
-        }
-
-        return true;
-    }
-
     /**
      * Build the array of underlying validation rules based on the current state.
      *
@@ -155,4 +142,14 @@ class CustomRulable implements Rule, DataAwareRule, ValidatorAwareRule
 
         return $this;
     }
+
+    public function validate(string $attribute, mixed $value, Closure $fail): void
+    {
+        $validator = Validator::make(
+            $this->data,
+            $this->buildValidationRules($attribute, $value)
+        );
+
+        $validator->validate();
+    }
 }
\ No newline at end of file
diff --git a/src/Rules/Instruction.php b/src/Rules/Instruction.php
index 8bfcd3d..9f5ccd2 100644
--- a/src/Rules/Instruction.php
+++ b/src/Rules/Instruction.php
@@ -5,6 +5,7 @@ namespace Lomkit\Rest\Rules;
 use Closure;
 use Illuminate\Contracts\Validation\DataAwareRule;
 use Illuminate\Contracts\Validation\Rule;
+use Illuminate\Contracts\Validation\ValidationRule;
 use Illuminate\Contracts\Validation\ValidatorAwareRule;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Validator;
@@ -13,7 +14,7 @@ use Lomkit\Rest\Http\Requests\RestRequest;
 use Lomkit\Rest\Http\Requests\SearchRequest;
 use Lomkit\Rest\Http\Resource;
 
-class Instruction implements Rule, DataAwareRule, ValidatorAwareRule
+class Instruction implements ValidationRule, DataAwareRule, ValidatorAwareRule
 {
 
     use Makeable;
@@ -59,20 +60,6 @@ class Instruction implements Rule, DataAwareRule, ValidatorAwareRule
         return $this;
     }
 
-    public function passes($attribute, $value)
-    {
-        $validator = Validator::make(
-            $this->data,
-            $this->buildValidationRules($attribute, $value)
-        );
-
-        if ($validator->fails()) {
-            return $this->fail($validator->messages()->all());
-        }
-
-        return true;
-    }
-
     /**
      * Build the array of underlying validation rules based on the current state.
      *
@@ -158,4 +145,14 @@ class Instruction implements Rule, DataAwareRule, ValidatorAwareRule
 
         return $this;
     }
+
+    public function validate(string $attribute, mixed $value, Closure $fail): void
+    {
+        $validator = Validator::make(
+            $this->data,
+            $this->buildValidationRules($attribute, $value)
+        );
+
+        $validator->validate();
+    }
 }
\ No newline at end of file
diff --git a/src/Rules/InstructionField.php b/src/Rules/InstructionField.php
index 5fdd1a6..366d239 100644
--- a/src/Rules/InstructionField.php
+++ b/src/Rules/InstructionField.php
@@ -5,6 +5,7 @@ namespace Lomkit\Rest\Rules;
 use Closure;
 use Illuminate\Contracts\Validation\DataAwareRule;
 use Illuminate\Contracts\Validation\Rule;
+use Illuminate\Contracts\Validation\ValidationRule;
 use Illuminate\Contracts\Validation\ValidatorAwareRule;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Validator;
@@ -13,7 +14,7 @@ use Lomkit\Rest\Http\Requests\RestRequest;
 use Lomkit\Rest\Http\Requests\SearchRequest;
 use Lomkit\Rest\Http\Resource;
 
-class InstructionField implements Rule, DataAwareRule, ValidatorAwareRule
+class InstructionField implements ValidationRule, DataAwareRule, ValidatorAwareRule
 {
 
     use Makeable;
@@ -59,20 +60,6 @@ class InstructionField implements Rule, DataAwareRule, ValidatorAwareRule
         return $this;
     }
 
-    public function passes($attribute, $value)
-    {
-        $validator = Validator::make(
-            $this->data,
-            $this->buildValidationRules($attribute, $value)
-        );
-
-        if ($validator->fails()) {
-            return $this->fail($validator->messages()->all());
-        }
-
-        return true;
-    }
-
     /**
      * Build the array of underlying validation rules based on the current state.
      *
@@ -143,4 +130,14 @@ class InstructionField implements Rule, DataAwareRule, ValidatorAwareRule
 
         return $this;
     }
+
+    public function validate(string $attribute, mixed $value, Closure $fail): void
+    {
+        $validator = Validator::make(
+            $this->data,
+            $this->buildValidationRules($attribute, $value)
+        );
+
+        $validator->validate();
+    }
 }
\ No newline at end of file
diff --git a/src/Rules/RequiredRelation.php b/src/Rules/RequiredRelation.php
deleted file mode 100644
index dde6a75..0000000
--- a/src/Rules/RequiredRelation.php
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-namespace Lomkit\Rest\Rules;
-
-use Closure;
-use Illuminate\Contracts\Validation\InvokableRule;
-use Lomkit\Rest\Concerns\Makeable;
-use Lomkit\Rest\Http\Requests\RestRequest;
-
-class RequiredRelation implements InvokableRule
-{
-
-    use Makeable;
-
-    /**
-     * The resource related to.
-     *
-     * @var Resource
-     */
-    protected $resource = null;
-
-    /**
-     *
-     *
-     * @param $resource
-     * @return $this
-     */
-    public function resource($resource)
-    {
-        $this->resource = $resource;
-
-        return $this;
-    }
-
-    protected function isOperationRequired(string $operation) {
-        return in_array($operation, ['create']);
-    }
-
-    public function __invoke($attribute, $value, $fail)
-    {
-        $request = app()->make(RestRequest::class);
-
-        $requiredRelations = collect(
-            $this->resource
-                ->getRelations($request)
-        )->filter(
-            function ($relation) use ($request) {
-                return $relation->isRequiredOnCreation($request);
-            }
-        )->mapWIthKeys(
-            function ($relation) {
-                return [$relation->relation => $relation];
-            }
-        );
-
-        if (!empty(array_diff_key($requiredRelations->toArray(), $value))) {
-            $relationsToString = $requiredRelations->implode(
-                function ($item, $key) {
-                    return $key;
-                },
-                ', '
-            );
-
-            $fail(sprintf('The %s relations are required.', $relationsToString));
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Rules/RequiredRelationOnCreation.php b/src/Rules/RequiredRelationOnCreation.php
new file mode 100644
index 0000000..343b876
--- /dev/null
+++ b/src/Rules/RequiredRelationOnCreation.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Lomkit\Rest\Rules;
+
+use Closure;
+use Illuminate\Contracts\Validation\DataAwareRule;
+use Illuminate\Contracts\Validation\InvokableRule;
+use Illuminate\Contracts\Validation\ValidationRule;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Str;
+use Lomkit\Rest\Concerns\Makeable;
+use Lomkit\Rest\Http\Requests\RestRequest;
+
+class RequiredRelationOnCreation implements ValidationRule, DataAwareRule
+{
+    use Makeable;
+
+    /**
+     * Indicates whether the rule should be implicit.
+     *
+     * @var bool
+     */
+    public $implicit = true;
+
+    /**
+     * The resource related to.
+     *
+     * @var Resource
+     */
+    protected $resource = null;
+
+    /**
+     * All of the data under validation.
+     *
+     * @var array<string, mixed>
+     */
+    protected $data = [];
+
+    /**
+     * Set the data under validation.
+     *
+     * @param  array<string, mixed>  $data
+     */
+    public function setData(array $data): static
+    {
+        $this->data = $data;
+
+        return $this;
+    }
+
+    /**
+     *
+     *
+     * @param $resource
+     * @return $this
+     */
+    public function resource($resource)
+    {
+        $this->resource = $resource;
+
+        return $this;
+    }
+
+    protected function isOperationRequired(string $operation) {
+        return in_array($operation, ['create']);
+    }
+
+    public function validate(string $attribute, mixed $value, Closure $fail): void
+    {
+        $arrayDot = Arr::dot($this->data);
+        if (
+            isset($arrayDot[Str::of($attribute)->beforeLast('.')->beforeLast('.')->append('.operation')->toString()]) &&
+            $arrayDot[Str::of($attribute)->beforeLast('.')->beforeLast('.')->append('.operation')->toString()] === 'create' &&
+            (!isset($arrayDot[$attribute.'.0.operation']) && !isset($arrayDot[$attribute.'.operation']) )
+        ) {
+            $fail('This relation is required on creation');
+        }
+    }
+}
\ No newline at end of file


Test patch:
diff --git a/tests/Feature/Controllers/ActionsOperationsTest.php b/tests/Feature/Controllers/ActionsOperationsTest.php
index 3730c29..0d51c4b 100644
--- a/tests/Feature/Controllers/ActionsOperationsTest.php
+++ b/tests/Feature/Controllers/ActionsOperationsTest.php
@@ -125,7 +125,7 @@ class ActionsOperationsTest extends TestCase
         );
 
         $response->assertStatus(422);
-        $response->assertJsonStructure(['message', 'errors' => ['fields.0']]);
+        $response->assertJsonStructure(['message', 'errors' => ['fields.0.value']]);
     }
 
     public function test_operate_action_with_fields(): void
diff --git a/tests/Feature/Controllers/MutateCreateOperationsTest.php b/tests/Feature/Controllers/MutateCreateOperationsTest.php
index 0775e12..7b1e067 100644
--- a/tests/Feature/Controllers/MutateCreateOperationsTest.php
+++ b/tests/Feature/Controllers/MutateCreateOperationsTest.php
@@ -61,7 +61,89 @@ class MutateCreateOperationsTest extends TestCase
         );
 
         $response->assertStatus(422);
-        $response->assertJsonStructure(['message', 'errors' => ['mutate.0']]);
+        $response->assertJsonStructure(['message', 'errors' => ['mutate.0.attributes.string']]);
+    }
+
+    public function test_creating_a_resource_with_no_required_relation(): void
+    {
+        Gate::policy(Model::class, GreenPolicy::class);
+
+        $response = $this->post(
+            '/api/required-creation/mutate',
+            [
+                'mutate' => [
+                    [
+                        'operation' => 'create',
+                        'attributes' => ['string' => 'string', 'name' => 'name', 'number' => 1]
+                    ],
+                ],
+            ],
+            ['Accept' => 'application/json']
+        );
+
+        $response->assertStatus(422);
+        $response->assertJsonStructure(['message', 'errors' => ['mutate.0.relations.belongsToManyRelation']]);
+    }
+
+    public function test_creating_a_resource_with_no_required_relation_but_empty_array(): void
+    {
+        Gate::policy(Model::class, GreenPolicy::class);
+
+        $response = $this->post(
+            '/api/required-creation/mutate',
+            [
+                'mutate' => [
+                    [
+                        'operation' => 'create',
+                        'attributes' => ['string' => 'string', 'name' => 'name', 'number' => 1],
+                        'relations' => [
+                            'belongsToManyRelation' => []
+                        ]
+                    ]
+                ]
+            ],
+            ['Accept' => 'application/json']
+        );
+
+        $response->assertStatus(422);
+        $response->assertJsonStructure(['message', 'errors' => ['mutate.0.relations.belongsToManyRelation']]);
+    }
+
+    public function test_creating_a_resource_with_required_relation(): void
+    {
+        $modelToCreate = ModelFactory::new()->makeOne();
+
+        Gate::policy(Model::class, GreenPolicy::class);
+        Gate::policy(BelongsToManyRelation::class, GreenPolicy::class);
+
+        $response = $this->post(
+            '/api/required-creation/mutate',
+            [
+                'mutate' => [
+                    [
+                        'operation' => 'create',
+                        'attributes' => [
+                            'name' => $modelToCreate->name,
+                            'number' => $modelToCreate->number
+                        ],
+                        'relations' => [
+                            'belongsToManyRelation' => [
+                                [
+                                    'operation' => 'create',
+                                    'attributes' => []
+                                ]
+                            ]
+                        ]
+                    ],
+                ],
+            ],
+            ['Accept' => 'application/json']
+        );
+
+        $this->assertMutatedResponse(
+            $response,
+            [$modelToCreate],
+        );
     }
 
     public function test_creating_a_resource_using_pivot_field_not_following_custom_pivot_rules(): void
diff --git a/tests/Feature/Controllers/MutateUpdateMorphOperationsTest.php b/tests/Feature/Controllers/MutateUpdateMorphOperationsTest.php
index 4bdc731..adfc9e1 100644
--- a/tests/Feature/Controllers/MutateUpdateMorphOperationsTest.php
+++ b/tests/Feature/Controllers/MutateUpdateMorphOperationsTest.php
@@ -51,7 +51,7 @@ class MutateUpdateMorphOperationsTest extends TestCase
         );
 
         $response->assertStatus(422);
-        $response->assertJsonStructure(['message', 'errors' => ['mutate.0']]);
+        $response->assertJsonStructure(['message', 'errors' => ['mutate.0.attributes.string']]);
     }
 
     public function test_updating_a_resource_with_creating_morph_to_relation(): void
diff --git a/tests/Feature/Controllers/MutateUpdateOperationsTest.php b/tests/Feature/Controllers/MutateUpdateOperationsTest.php
index 3876f02..86a55e1 100644
--- a/tests/Feature/Controllers/MutateUpdateOperationsTest.php
+++ b/tests/Feature/Controllers/MutateUpdateOperationsTest.php
@@ -115,6 +115,44 @@ class MutateUpdateOperationsTest extends TestCase
         );
     }
 
+    public function test_updating_a_resource_with_required_on_creation_resource(): void
+    {
+        $modelToUpdate = ModelFactory::new()->createOne();
+
+        Gate::policy(Model::class, GreenPolicy::class);
+
+        $response = $this->post(
+            '/api/required-creation/mutate',
+            [
+                'mutate' => [
+                    [
+                        'operation' => 'update',
+                        'key' => $modelToUpdate->getKey(),
+                        'attributes' => [
+                            'name' => 'new name',
+                            'number' => 5001
+                        ]
+                    ],
+                ],
+            ],
+            ['Accept' => 'application/json']
+        );
+
+        $this->assertMutatedResponse(
+            $response,
+            [],
+            [$modelToUpdate],
+        );
+
+        $this->assertDatabaseHas(
+            $modelToUpdate->getTable(),
+            [
+                'name' => 'new name',
+                'number' => 5001
+            ]
+        );
+    }
+
     public function test_updating_a_resource_with_creating_belongs_to_relation(): void
     {
         $modelToUpdate = ModelFactory::new()->createOne();
diff --git a/tests/Feature/Controllers/SearchInstructionsOperationsTest.php b/tests/Feature/Controllers/SearchInstructionsOperationsTest.php
index 5dce164..cdf7b20 100644
--- a/tests/Feature/Controllers/SearchInstructionsOperationsTest.php
+++ b/tests/Feature/Controllers/SearchInstructionsOperationsTest.php
@@ -94,7 +94,7 @@ class SearchInstructionsOperationsTest extends TestCase
         );
 
         $response->assertStatus(422);
-        $response->assertJsonStructure(['message', 'errors' => ['instructions.0']]);
+        $response->assertJsonStructure(['message', 'errors' => ['instructions.0.fields.0.name']]);
     }
 
     public function test_getting_a_list_of_resources_instructing_numbered_with_unauthorized_validation(): void
@@ -120,7 +120,7 @@ class SearchInstructionsOperationsTest extends TestCase
         );
 
         $response->assertStatus(422);
-        $response->assertJsonStructure(['message', 'errors' => ['instructions.0']]);
+        $response->assertJsonStructure(['message', 'errors' => ['instructions.0.fields.0.value']]);
     }
 
     public function test_getting_a_list_of_resources_instructing_numbered_with_fields(): void
diff --git a/tests/Support/Http/Controllers/RequiredCreationController.php b/tests/Support/Http/Controllers/RequiredCreationController.php
new file mode 100644
index 0000000..2ae4b1b
--- /dev/null
+++ b/tests/Support/Http/Controllers/RequiredCreationController.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Lomkit\Rest\Tests\Support\Http\Controllers;
+
+use Lomkit\Rest\Http\Controllers\Controller;
+use Lomkit\Rest\Tests\Support\Rest\Resources\AutomaticGatingResource;
+use Lomkit\Rest\Tests\Support\Rest\Resources\NoExposedFieldsResource;
+use Lomkit\Rest\Tests\Support\Rest\Resources\RequiredCreationResource;
+
+class RequiredCreationController extends Controller
+{
+    public static $resource = RequiredCreationResource::class;
+}
\ No newline at end of file
diff --git a/tests/Support/Rest/Resources/RequiredCreationResource.php b/tests/Support/Rest/Resources/RequiredCreationResource.php
new file mode 100644
index 0000000..1bc22e2
--- /dev/null
+++ b/tests/Support/Rest/Resources/RequiredCreationResource.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Lomkit\Rest\Tests\Support\Rest\Resources;
+
+use Lomkit\Rest\Concerns\Resource\DisableAutomaticGates;
+use Lomkit\Rest\Http\Requests\RestRequest;
+use Lomkit\Rest\Http\Resource;
+use Lomkit\Rest\Relations\BelongsTo;
+use Lomkit\Rest\Relations\BelongsToMany;
+use Lomkit\Rest\Relations\HasMany;
+use Lomkit\Rest\Relations\HasManyThrough;
+use Lomkit\Rest\Relations\HasOne;
+use Lomkit\Rest\Relations\HasOneOfMany;
+use Lomkit\Rest\Relations\HasOneThrough;
+use Lomkit\Rest\Relations\MorphedByMany;
+use Lomkit\Rest\Relations\MorphMany;
+use Lomkit\Rest\Relations\MorphOne;
+use Lomkit\Rest\Relations\MorphOneOfMany;
+use Lomkit\Rest\Relations\MorphTo;
+use Lomkit\Rest\Relations\MorphToMany;
+use Lomkit\Rest\Tests\Support\Models\Model;
+
+class RequiredCreationResource extends Resource
+{
+
+    public static $model = Model::class;
+
+    public function relations(RestRequest $request): array
+    {
+        return [
+            BelongsToMany::make('belongsToManyRelation', BelongsToManyResource::class)
+                ->requiredOnCreation(),
+        ];
+    }
+
+    public function fields(RestRequest $request): array
+    {
+        return [
+            'id',
+            'name',
+            'number',
+            'string'
+        ];
+    }
+}
\ No newline at end of file
diff --git a/tests/Support/Routes/api.php b/tests/Support/Routes/api.php
index ea411ae..c99fdaa 100644
--- a/tests/Support/Routes/api.php
+++ b/tests/Support/Routes/api.php
@@ -8,6 +8,7 @@ Route::group(['as' => 'api.', 'prefix' => 'api'], function () {
 
     \Lomkit\Rest\Facades\Rest::resource('no-exposed-fields', \Lomkit\Rest\Tests\Support\Http\Controllers\NoExposedFieldsController::class);
     \Lomkit\Rest\Facades\Rest::resource('automatic-gating', \Lomkit\Rest\Tests\Support\Http\Controllers\AutomaticGatingController::class);
+    \Lomkit\Rest\Facades\Rest::resource('required-creation', \Lomkit\Rest\Tests\Support\Http\Controllers\RequiredCreationController::class);
     \Lomkit\Rest\Facades\Rest::resource('no-authorization', \Lomkit\Rest\Tests\Support\Http\Controllers\NoAuthorizationController::class);
     \Lomkit\Rest\Facades\Rest::resource('soft-deleted-models', \Lomkit\Rest\Tests\Support\Http\Controllers\SoftDeletedModelController::class)->withSoftDeletes();
 });