diff --git a/src/ModelDescriber/JMSModelDescriber.php b/src/ModelDescriber/JMSModelDescriber.php index a5a9ea66c..d46f908ed 100644 --- a/src/ModelDescriber/JMSModelDescriber.php +++ b/src/ModelDescriber/JMSModelDescriber.php @@ -152,6 +152,7 @@ public function describe(Model $model, OA\Schema $schema) } catch (\ReflectionException $ignored) { } } + $this->checkRequiredFields($reflections, $schema, $name); if (null !== $item->setter) { try { $reflections[] = new \ReflectionMethod($item->class, $item->setter); @@ -397,4 +398,34 @@ private function propertyTypeUsesGroups(array $type): ?bool return null; } } + + /** + * Mark property as required if it is not nullable. + * + * @param array<\ReflectionProperty|\ReflectionMethod> $reflections + */ + private function checkRequiredFields(array $reflections, OA\Schema $schema, string $name): void + { + foreach ($reflections as $reflection) { + $nullable = false; + if ($reflection instanceof \ReflectionProperty) { + $type = PHP_VERSION_ID >= 70400 ? $reflection->getType() : null; + if (null !== $type && !$type->allowsNull()) { + $nullable = true; + } + } elseif ($reflection instanceof \ReflectionMethod) { + $returnType = $reflection->getReturnType(); + if (null !== $returnType && !$returnType->allowsNull()) { + $nullable = true; + } + } + if ($nullable) { + $required = Generator::UNDEFINED !== $schema->required ? $schema->required : []; + $required[] = $name; + + $schema->required = $required; + break; + } + } + } } diff --git a/tests/Functional/Controller/JMSController80.php b/tests/Functional/Controller/JMSController80.php index 0f9f57d8b..3f99afa37 100644 --- a/tests/Functional/Controller/JMSController80.php +++ b/tests/Functional/Controller/JMSController80.php @@ -16,6 +16,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex80; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSDualComplex; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSNamingStrategyConstraints; +use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSTyped80; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSUser; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChat; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatRoomUser; @@ -166,4 +167,18 @@ public function minUserNestedAction() public function discriminatorMapAction() { } + + /** + * @Route("/api/jms_typed", methods={"GET"}) + * + * @OA\Response( + * response=200, + * description="Success", + * + * @Model(type=JMSTyped80::class) + * ) + */ + public function typedAction() + { + } } diff --git a/tests/Functional/Controller/JMSController81.php b/tests/Functional/Controller/JMSController81.php index 8848e196a..a367e2209 100644 --- a/tests/Functional/Controller/JMSController81.php +++ b/tests/Functional/Controller/JMSController81.php @@ -17,6 +17,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex81; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSDualComplex; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSNamingStrategyConstraints; +use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSTyped81; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSUser; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChat; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatRoomUser; @@ -141,4 +142,14 @@ public function enum() public function discriminatorMapAction() { } + + #[Route('/api/jms_typed', methods: ['GET'])] + #[OA\Response( + response: 200, + description: 'Success', + content: new Model(type: JMSTyped81::class)) + ] + public function typedAction() + { + } } diff --git a/tests/Functional/Entity/JMSTyped80.php b/tests/Functional/Entity/JMSTyped80.php new file mode 100644 index 000000000..9602dbeac --- /dev/null +++ b/tests/Functional/Entity/JMSTyped80.php @@ -0,0 +1,46 @@ + 'Article81', + 'required' => [ + 'id', + 'type', + 'int_backed_type', + 'not_backed_type', + ], ], json_decode($this->getModel('Article81')->toJson(), true)); self::assertEquals([ @@ -420,4 +426,23 @@ protected static function createKernel(array $options = []): KernelInterface { return new TestKernel(TestKernel::USE_JMS); } + + public function testModelTypedDocumentation(): void + { + self::assertEquals([ + 'type' => 'object', + 'properties' => [ + 'id' => ['type' => 'integer'], + 'user' => ['$ref' => '#/components/schemas/JMSUser'], + 'name' => ['type' => 'string'], + 'virtual_friend' => ['$ref' => '#/components/schemas/JMSUser'], + ], + 'required' => [ + 'virtual_friend', + 'id', + 'user', + ], + 'schema' => 'JMSTyped', + ], json_decode($this->getModel('JMSTyped')->toJson(), true)); + } } diff --git a/tests/Functional/TestKernel.php b/tests/Functional/TestKernel.php index 863792b43..35fb0b158 100644 --- a/tests/Functional/TestKernel.php +++ b/tests/Functional/TestKernel.php @@ -21,6 +21,8 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\BazingaUser; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex80; use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSComplex81; +use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSTyped80; +use Nelmio\ApiDocBundle\Tests\Functional\Entity\JMSTyped81; use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture; use Nelmio\ApiDocBundle\Tests\Functional\Entity\PrivateProtectedExposure; use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups; @@ -220,6 +222,10 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'type' => JMSComplex80::class, 'groups' => null, ], + [ + 'alias' => 'JMSTyped', + 'type' => JMSTyped80::class, + ], ]); } elseif (self::isAttributesAvailable()) { $models = array_merge($models, [ @@ -237,6 +243,10 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'type' => JMSComplex81::class, 'groups' => null, ], + [ + 'alias' => 'JMSTyped', + 'type' => JMSTyped81::class, + ], ]); }