Repository: janephp/janephp (php)

Issue:
minLength > 0 adds NotBlank Constraint without consideration for `nullable: true`
**Jane version(s) affected**: 7.4.0

**Description**  
A string property with a `minLength > 0` adds a `NotBlank` constraints and ignores the `nullable: true` property in the schema.
https://github.com/janephp/janephp/blob/cc3fef6e61d084b0befe6099f8a22940a58c9a1e/src/Component/JsonSchema/Guesser/Validator/String_/MinLengthValidator.php#L33-L34 

**How to reproduce**  
```yaml
    Shop:
      type: object
      properties
        name:
          type: string
          minLength: 1
          nullable: true
```

**Possible Solution**  
`NotBlank` constraint got an option called `allowNull` , which could be used to mitigate this.

**Additional context**  
- 


Pull request description:
[697-minLength-with-nullable] Consider nullable property for minLengthValidator
Suggestion to fix #697.

I do not understand the structure of the tests, so I did not write any. Maybe someone with more experience can add them?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c5652999..3c9b4365a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,11 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Changed
 - [Jane] [GH#748](https://github.com/janephp/janephp/pull/748) Move PHP CS Fixer to a separate composer.json file
 - [JsonSchema] [GH#747](https://github.com/janephp/janephp/pull/747) Allow league/uri v7
+- [JsonSchema] [GH#698](https://github.com/janephp/janephp/pull/698) Consider nullable property for minLengthValidator
 - [OpenApiCommon] [GH#750](https://github.com/janephp/janephp/pull/750) Replace 'findUrlFactory' with 'findUriFactory'
 
-### Fixed
-- [AutoMapper] [GH#753](https://github.com/janephp/janephp/pull/753) Fix: AutoMapper should accept getters with default properties
-
 ## [7.5.3] - 2023-08-04
 ### Changed
 - [JsonSchema] [GH#436](https://github.com/janephp/janephp/pull/436) Better PHPDoc for generated Map type properties
diff --git a/src/Component/AutoMapper/Attribute/MapToContext.php b/src/Component/AutoMapper/Attribute/MapToContext.php
index 3c7973bb0..286e54586 100644
--- a/src/Component/AutoMapper/Attribute/MapToContext.php
+++ b/src/Component/AutoMapper/Attribute/MapToContext.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Jane\Component\AutoMapper\Attribute;
 
+use Attribute;
+
 #[\Attribute(\Attribute::TARGET_PARAMETER)] final class MapToContext
 {
     public function __construct(
diff --git a/src/Component/AutoMapper/Extractor/ReadAccessor.php b/src/Component/AutoMapper/Extractor/ReadAccessor.php
index 62ac605b2..00bca6f39 100644
--- a/src/Component/AutoMapper/Extractor/ReadAccessor.php
+++ b/src/Component/AutoMapper/Extractor/ReadAccessor.php
@@ -87,8 +87,8 @@ final class ReadAccessor
                                 )
                             )
                         );
-                    } elseif (!$parameter->isDefaultValueAvailable()) {
-                        throw new \InvalidArgumentException("Accessors method \"{$this->sourceClass}\"::\"{$this->name}()\" parameters must have either a default value or the #[MapToContext] attribute.");
+                    } else {
+                        throw new \InvalidArgumentException("Accessors method \"{$this->sourceClass}\"::\"{$this->name}()\" should not have parameters or they must have #[MapToContext] attribute.");
                     }
                 }
             }
diff --git a/src/Component/AutoMapper/MapperContext.php b/src/Component/AutoMapper/MapperContext.php
index c9d9868f6..6a421b2a8 100644
--- a/src/Component/AutoMapper/MapperContext.php
+++ b/src/Component/AutoMapper/MapperContext.php
@@ -128,6 +128,8 @@ class MapperContext
      * Handle circular reference for a specific reference.
      *
      * By default will try to keep it and return the previous value
+     *
+     * @return mixed
      */
     public static function &handleCircularReference(array &$context, string $reference, $object, ?int $circularReferenceLimit = null, callable $callback = null)
     {
diff --git a/src/Component/JsonSchema/Guesser/Guess/Property.php b/src/Component/JsonSchema/Guesser/Guess/Property.php
index 343d0dfcc..898ad220c 100644
--- a/src/Component/JsonSchema/Guesser/Guess/Property.php
+++ b/src/Component/JsonSchema/Guesser/Guess/Property.php
@@ -21,6 +21,9 @@ class Property
      */
     private $reference;
 
+    /**
+     * @var mixed
+     */
     private $object;
 
     /**
@@ -38,6 +41,9 @@ class Property
      */
     private $description;
 
+    /**
+     * @var mixed
+     */
     private $default;
 
     /**
diff --git a/src/Component/JsonSchema/Guesser/Validator/String_/MinLengthValidator.php b/src/Component/JsonSchema/Guesser/Validator/String_/MinLengthValidator.php
index c43fba7c9..e37ba8a34 100644
--- a/src/Component/JsonSchema/Guesser/Validator/String_/MinLengthValidator.php
+++ b/src/Component/JsonSchema/Guesser/Validator/String_/MinLengthValidator.php
@@ -31,7 +31,28 @@ class MinLengthValidator implements ValidatorInterface
             'minMessage' => 'This value is too short. It should have {{ limit }} characters or more.',
         ]));
         if ($object->getMinLength() > 0) {
-            $guess->addValidatorGuess(new ValidatorGuess(NotBlank::class));
+            $nullable = false;
+
+            if (\get_class($object) === JsonSchema::class) {
+                $nullable = \is_array($object->getType()) ? \in_array('null', $object->getType()) : 'null' === $object->getType();
+            }
+            if (\get_class($object) === 'Jane\\Component\\OpenApi2\\JsonSchema\\Model\\Schema') {
+                $nullable = $object->offsetExists('x-nullable') && \is_bool($object->offsetGet('x-nullable')) && $object->offsetGet('x-nullable');
+            }
+            if (\get_class($object) === 'Jane\\Component\\OpenApi3\\JsonSchema\\Model\\Schema') {
+                $nullable = method_exists($object, 'getNullable') && $object->getNullable() ?? false;
+            }
+
+            $options = [];
+            if ($nullable === true) {
+                // Using an integer as a replacement boolean value is most likely to break as soon as
+                // \Symfony\Component\Validator\Constraints\NotBlank::$allowNull is strongly typed.
+                // Currently we can not use 'bool' here, because \Jane\Component\JsonSchema\Generator\ValidatorGenerator::generateConstraint()
+                // does not handle them. This seems to be an issue with nikic/php-parser not being able to provide support
+                // for it.
+                $options = ['allowNull' => 1];
+            }
+            $guess->addValidatorGuess(new ValidatorGuess(NotBlank::class, $options));
         }
     }
 }
diff --git a/src/Component/JsonSchema/Registry/Schema.php b/src/Component/JsonSchema/Registry/Schema.php
index c185c084c..eb3ce7bfb 100644
--- a/src/Component/JsonSchema/Registry/Schema.php
+++ b/src/Component/JsonSchema/Registry/Schema.php
@@ -134,11 +134,17 @@ class Schema implements SchemaInterface
         return \in_array($reference, $this->references, true);
     }
 
+    /**
+     * @return mixed
+     */
     public function getParsed()
     {
         return $this->parsed;
     }
 
+    /**
+     * @param mixed $parsed
+     */
     public function setParsed($parsed): void
     {
         $this->parsed = $parsed;
diff --git a/src/Component/JsonSchemaRuntime/Reference.php b/src/Component/JsonSchemaRuntime/Reference.php
index 814e8f9be..a99c13518 100644
--- a/src/Component/JsonSchemaRuntime/Reference.php
+++ b/src/Component/JsonSchemaRuntime/Reference.php
@@ -47,6 +47,8 @@ class Reference
     /**
      * Resolve a JSON Reference.
      *
+     * @param callable $deserializeCallback
+     *
      * @return mixed Return the json value (deserialized) referenced
      */
     public function resolve(callable $deserializeCallback = null)
diff --git a/src/Component/OpenApiCommon/Guesser/Guess/ParentClass.php b/src/Component/OpenApiCommon/Guesser/Guess/ParentClass.php
index e3a877790..5d351c23a 100644
--- a/src/Component/OpenApiCommon/Guesser/Guess/ParentClass.php
+++ b/src/Component/OpenApiCommon/Guesser/Guess/ParentClass.php
@@ -52,6 +52,9 @@ class ParentClass extends ClassGuess
         return array_keys($this->childEntries);
     }
 
+    /**
+     * @return string
+     */
     public function getChildEntryClassNameByKey(string $key): ?string
     {
         if (!\array_key_exists($key, $this->childEntries)) {


Test patch:
diff --git a/src/Bundle/AutoMapperBundle/Tests/Fixtures/Address.php b/src/Bundle/AutoMapperBundle/Tests/Fixtures/Address.php
index 2a61bd143..6d0b48296 100644
--- a/src/Bundle/AutoMapperBundle/Tests/Fixtures/Address.php
+++ b/src/Bundle/AutoMapperBundle/Tests/Fixtures/Address.php
@@ -9,6 +9,9 @@ class Address
      */
     private $city;
 
+    /**
+     * @param string $city
+     */
     public function setCity(?string $city): void
     {
         $this->city = $city;
diff --git a/src/Component/AutoMapper/Tests/AutoMapperTest.php b/src/Component/AutoMapper/Tests/AutoMapperTest.php
index 7ec946ddf..0c16d4d63 100644
--- a/src/Component/AutoMapper/Tests/AutoMapperTest.php
+++ b/src/Component/AutoMapper/Tests/AutoMapperTest.php
@@ -1178,7 +1178,6 @@ class AutoMapperTest extends AutoMapperBaseTest
             [
                 'value' => 'foo_bar_baz',
                 'virtualProperty' => 'foo_bar_baz',
-                'propertyWithDefaultValue' => 'foo',
             ],
             $this->autoMapper->map(
                 new ClassWithMapToContextAttribute('bar'),
diff --git a/src/Component/AutoMapper/Tests/Fixtures/Address.php b/src/Component/AutoMapper/Tests/Fixtures/Address.php
index bbb8391e5..8f11d180d 100644
--- a/src/Component/AutoMapper/Tests/Fixtures/Address.php
+++ b/src/Component/AutoMapper/Tests/Fixtures/Address.php
@@ -9,6 +9,9 @@ class Address
      */
     private $city;
 
+    /**
+     * @param string $city
+     */
     public function setCity(?string $city): void
     {
         $this->city = $city;
diff --git a/src/Component/AutoMapper/Tests/Fixtures/ClassWithMapToContextAttribute.php b/src/Component/AutoMapper/Tests/Fixtures/ClassWithMapToContextAttribute.php
index 1e813906d..c4fdce9ac 100644
--- a/src/Component/AutoMapper/Tests/Fixtures/ClassWithMapToContextAttribute.php
+++ b/src/Component/AutoMapper/Tests/Fixtures/ClassWithMapToContextAttribute.php
@@ -24,10 +24,4 @@ class ClassWithMapToContextAttribute
     ): string {
         return "{$prefix}_{$this->value}_{$suffix}";
     }
-
-    public function getPropertyWithDefaultValue(
-        string $someVar = 'foo',
-    ): string {
-        return $someVar;
-    }
 }
diff --git a/src/Component/AutoMapper/Tests/Fixtures/UserConstructorDTO.php b/src/Component/AutoMapper/Tests/Fixtures/UserConstructorDTO.php
index 3ac56b583..00c62355e 100644
--- a/src/Component/AutoMapper/Tests/Fixtures/UserConstructorDTO.php
+++ b/src/Component/AutoMapper/Tests/Fixtures/UserConstructorDTO.php
@@ -39,6 +39,9 @@ class UserConstructorDTO
         return $this->id;
     }
 
+    /**
+     * @return string
+     */
     public function getName(): ?string
     {
         return $this->name;
diff --git a/src/Component/JsonSchema/Tests/Validation/ValidationTest.php b/src/Component/JsonSchema/Tests/Validation/ValidationTest.php
index c312e08be..d33c7bc96 100644
--- a/src/Component/JsonSchema/Tests/Validation/ValidationTest.php
+++ b/src/Component/JsonSchema/Tests/Validation/ValidationTest.php
@@ -15,6 +15,7 @@ use Jane\Component\JsonSchema\Tests\Validation\Generated\Model\SimpleObject;
 use Jane\Component\JsonSchema\Tests\Validation\Generated\Model\SimpleObjectSubProperty;
 use Jane\Component\JsonSchema\Tests\Validation\Generated\Model\StringObject;
 use Jane\Component\JsonSchema\Tests\Validation\Generated\Model\TypeObject;
+use Jane\Component\JsonSchema\Tests\Validation\Generated\Model\VerifyNullableStringPropertyWithMinLengthValidatesCorrectly;
 use Jane\Component\JsonSchema\Tests\Validation\Generated\Normalizer\ArrayObjectNormalizer;
 use Jane\Component\JsonSchema\Tests\Validation\Generated\Normalizer\FormatObjectNormalizer;
 use Jane\Component\JsonSchema\Tests\Validation\Generated\Normalizer\NumericObjectNormalizer;
@@ -27,6 +28,7 @@ use Jane\Component\JsonSchema\Tests\Validation\Generated\Normalizer\SimpleObject
 use Jane\Component\JsonSchema\Tests\Validation\Generated\Normalizer\SimpleObjectSubPropertyNormalizer;
 use Jane\Component\JsonSchema\Tests\Validation\Generated\Normalizer\StringObjectNormalizer;
 use Jane\Component\JsonSchema\Tests\Validation\Generated\Normalizer\TypeObjectNormalizer;
+use Jane\Component\JsonSchema\Tests\Validation\Generated\Normalizer\VerifyNullableStringPropertyWithMinLengthValidatesCorrectlyNormalizer;
 use Jane\Component\JsonSchema\Tests\Validation\Generated\Runtime\Normalizer\ValidationException;
 use PHPUnit\Framework\TestCase;
 use Symfony\Component\Console\Input\ArrayInput;
@@ -76,6 +78,9 @@ class ValidationTest extends TestCase
 
         // 11. Nested SubModel
         $this->nestedSubModelValidation();
+
+        // 12.
+        $this->verifyNullableStringPropertyWithMinLengthValidatesCorrectly();
     }
 
     private function numericValidation(): void
@@ -914,4 +919,31 @@ class ValidationTest extends TestCase
         $this->assertEquals('[subLevel-1][subLevel-2][end]', $violation->getPropertyPath());
         $this->assertEquals('This value is not valid.', $violation->getMessage());
     }
+
+    private function verifyNullableStringPropertyWithMinLengthValidatesCorrectly(): void
+    {
+        $normalizer = new VerifyNullableStringPropertyWithMinLengthValidatesCorrectlyNormalizer();
+
+        $caughtException = null;
+        try {
+            $normalizer->denormalize([
+                'name' => '',
+            ], VerifyNullableStringPropertyWithMinLengthValidatesCorrectly::class);
+        } catch (ValidationException $exception) {
+            $caughtException = $exception;
+        }
+
+        self::assertInstanceOf(ValidationException::class, $caughtException);
+        self::assertEquals(400, $caughtException->getCode());
+        self::assertEquals(2, $caughtException->getViolationList()->count());
+        self::assertEquals('[name]', $caughtException->getViolationList()->get(0)->getPropertyPath());
+
+        $caughtException = null;
+        $data = $normalizer->denormalize([
+            'name' => null,
+        ], VerifyNullableStringPropertyWithMinLengthValidatesCorrectly::class);
+
+        self::assertInstanceOf(VerifyNullableStringPropertyWithMinLengthValidatesCorrectly::class, $data);
+        self::assertEquals(null, $data->getName());
+    }
 }
diff --git a/src/Component/JsonSchema/Tests/Validation/schema.json b/src/Component/JsonSchema/Tests/Validation/schema.json
index 007d696f4..4896b1756 100644
--- a/src/Component/JsonSchema/Tests/Validation/schema.json
+++ b/src/Component/JsonSchema/Tests/Validation/schema.json
@@ -224,6 +224,18 @@
                     "pattern": "^[a-z]+$"
                 }
             }
+        },
+        "VerifyNullableStringPropertyWithMinLengthValidatesCorrectly":{
+            "type": "object",
+            "properties": {
+                "name": {
+                    "type": [
+                        "string",
+                        "null"
+                    ],
+                    "minLength": 1
+                }
+            }
         }
     }
 }