diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..ae3c2e1
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+/.gitignore export-ignore
+/Tests export-ignore
diff --git a/Command/OpenApiCommand.php b/Command/OpenApiCommand.php
index 88d3c0c..a7d1c43 100644
--- a/Command/OpenApiCommand.php
+++ b/Command/OpenApiCommand.php
@@ -53,7 +53,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
     {
         $filesystem = new Filesystem();
         $io = new SymfonyStyle($input, $output);
-        $data = $this->normalizer->normalize($this->openApiFactory->__invoke(), 'json');
+        $data = $this->normalizer->normalize($this->openApiFactory->__invoke(), 'json', [
+            'spec_version' => $input->getOption('spec-version'),
+        ]);
         $content = $input->getOption('yaml')
             ? Yaml::dump($data, 10, 2, Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)
             : (json_encode($data, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES) ?: '');
diff --git a/Factory/OpenApiFactory.php b/Factory/OpenApiFactory.php
index 0b39647..6f18de7 100644
--- a/Factory/OpenApiFactory.php
+++ b/Factory/OpenApiFactory.php
@@ -13,6 +13,7 @@
 
 namespace ApiPlatform\OpenApi\Factory;
 
+use ApiPlatform\Doctrine\Odm\State\Options as DoctrineODMOptions;
 use ApiPlatform\Doctrine\Orm\State\Options as DoctrineOptions;
 use ApiPlatform\JsonSchema\Schema;
 use ApiPlatform\JsonSchema\SchemaFactoryInterface;
@@ -155,7 +156,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
             }
 
             $path = $this->getPath($path);
-            $method = $operation->getMethod() ?? HttpOperation::METHOD_GET;
+            $method = $operation->getMethod() ?? 'GET';
 
             if (!\in_array($method, PathItem::$methods, true)) {
                 continue;
@@ -272,7 +273,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
                 $openapiOperation = $openapiOperation->withParameter($parameter);
             }
 
-            if ($operation instanceof CollectionOperationInterface && HttpOperation::METHOD_POST !== $method) {
+            if ($operation instanceof CollectionOperationInterface && 'POST' !== $method) {
                 foreach (array_merge($this->getPaginationParameters($operation), $this->getFiltersParameters($operation)) as $parameter) {
                     if ($this->hasParameter($openapiOperation, $parameter)) {
                         continue;
@@ -285,11 +286,11 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
             $existingResponses = $openapiOperation?->getResponses() ?: [];
             // Create responses
             switch ($method) {
-                case HttpOperation::METHOD_GET:
+                case 'GET':
                     $successStatus = (string) $operation->getStatus() ?: 200;
                     $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, sprintf('%s %s', $resourceShortName, $operation instanceof CollectionOperationInterface ? 'collection' : 'resource'), $openapiOperation, $operation, $responseMimeTypes, $operationOutputSchemas);
                     break;
-                case HttpOperation::METHOD_POST:
+                case 'POST':
                     $successStatus = (string) $operation->getStatus() ?: 201;
 
                     $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, sprintf('%s resource created', $resourceShortName), $openapiOperation, $operation, $responseMimeTypes, $operationOutputSchemas, $resourceMetadataCollection);
@@ -298,8 +299,8 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
 
                     $openapiOperation = $this->buildOpenApiResponse($existingResponses, '422', 'Unprocessable entity', $openapiOperation);
                     break;
-                case HttpOperation::METHOD_PATCH:
-                case HttpOperation::METHOD_PUT:
+                case 'PATCH':
+                case 'PUT':
                     $successStatus = (string) $operation->getStatus() ?: 200;
                     $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, sprintf('%s resource updated', $resourceShortName), $openapiOperation, $operation, $responseMimeTypes, $operationOutputSchemas, $resourceMetadataCollection);
                     $openapiOperation = $this->buildOpenApiResponse($existingResponses, '400', 'Invalid input', $openapiOperation);
@@ -308,7 +309,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
                     }
                     $openapiOperation = $this->buildOpenApiResponse($existingResponses, '422', 'Unprocessable entity', $openapiOperation);
                     break;
-                case HttpOperation::METHOD_DELETE:
+                case 'DELETE':
                     $successStatus = (string) $operation->getStatus() ?: 204;
 
                     $openapiOperation = $this->buildOpenApiResponse($existingResponses, $successStatus, sprintf('%s resource deleted', $resourceShortName), $openapiOperation);
@@ -316,7 +317,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
                     break;
             }
 
-            if (!$operation instanceof CollectionOperationInterface && HttpOperation::METHOD_POST !== $operation->getMethod()) {
+            if (!$operation instanceof CollectionOperationInterface && 'POST' !== $operation->getMethod()) {
                 if (!isset($existingResponses[404])) {
                     $openapiOperation = $openapiOperation->withResponse(404, new Response('Resource not found'));
                 }
@@ -346,7 +347,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
                     'The "openapiContext" option is deprecated, use "openapi" instead.'
                 );
                 $openapiOperation = $openapiOperation->withRequestBody(new RequestBody($contextRequestBody['description'] ?? '', new \ArrayObject($contextRequestBody['content']), $contextRequestBody['required'] ?? false));
-            } elseif (null === $openapiOperation->getRequestBody() && \in_array($method, [HttpOperation::METHOD_PATCH, HttpOperation::METHOD_PUT, HttpOperation::METHOD_POST], true)) {
+            } elseif (null === $openapiOperation->getRequestBody() && \in_array($method, ['PATCH', 'PUT', 'POST'], true)) {
                 $operationInputSchemas = [];
                 foreach ($requestMimeTypes as $operationFormat) {
                     $operationInputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_INPUT, $operation, $schema, null, $forceSchemaCollection);
@@ -354,7 +355,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
                     $this->appendSchemaDefinitions($schemas, $operationInputSchema->getDefinitions());
                 }
 
-                $openapiOperation = $openapiOperation->withRequestBody(new RequestBody(sprintf('The %s %s resource', HttpOperation::METHOD_POST === $method ? 'new' : 'updated', $resourceShortName), $this->buildContent($requestMimeTypes, $operationInputSchemas), true));
+                $openapiOperation = $openapiOperation->withRequestBody(new RequestBody(sprintf('The %s %s resource', 'POST' === $method ? 'new' : 'updated', $resourceShortName), $this->buildContent($requestMimeTypes, $operationInputSchemas), true));
             }
 
             // TODO Remove in 4.0
@@ -498,12 +499,12 @@ private function getLinks(ResourceMetadataCollection $resourceMetadataCollection
         foreach ($resourceMetadataCollection as $resource) {
             foreach ($resource->getOperations() as $operationName => $operation) {
                 $parameters = [];
-                $method = $operation instanceof HttpOperation ? $operation->getMethod() : HttpOperation::METHOD_GET;
+                $method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET';
                 if (
                     $operationName === $operation->getName()
                     || isset($links[$operationName])
                     || $operation instanceof CollectionOperationInterface
-                    || HttpOperation::METHOD_GET !== $method
+                    || 'GET' !== $method
                 ) {
                     continue;
                 }
@@ -561,12 +562,18 @@ private function getFiltersParameters(CollectionOperationInterface|HttpOperation
 
             $filter = $this->filterLocator->get($filterId);
             $entityClass = $operation->getClass();
-            if (($options = $operation->getStateOptions()) && $options instanceof DoctrineOptions && $options->getEntityClass()) {
-                $entityClass = $options->getEntityClass();
+            if ($options = $operation->getStateOptions()) {
+                if ($options instanceof DoctrineOptions && $options->getEntityClass()) {
+                    $entityClass = $options->getEntityClass();
+                }
+
+                if ($options instanceof DoctrineODMOptions && $options->getDocumentClass()) {
+                    $entityClass = $options->getDocumentClass();
+                }
             }
 
             foreach ($filter->getDescription($entityClass) as $name => $data) {
-                $schema = $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false)) : ['type' => 'string']);
+                $schema = $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false), 'openapi') : ['type' => 'string']);
 
                 $parameters[] = new Parameter(
                     $name,
diff --git a/OpenApi.php b/OpenApi.php
index 8a95e18..abd2c01 100644
--- a/OpenApi.php
+++ b/OpenApi.php
@@ -22,9 +22,7 @@ final class OpenApi
 {
     use ExtensionTrait;
 
-    // We're actually supporting 3.1 but swagger ui has a version constraint
-    // public const VERSION = '3.1.0';
-    public const VERSION = '3.0.0';
+    public const VERSION = '3.1.0';
 
     private string $openapi = self::VERSION;
 
diff --git a/Serializer/LegacyOpenApiNormalizer.php b/Serializer/LegacyOpenApiNormalizer.php
new file mode 100644
index 0000000..a7d7381
--- /dev/null
+++ b/Serializer/LegacyOpenApiNormalizer.php
@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * This file is part of the API Platform project.
+ *
+ * (c) Kévin Dunglas <dunglas@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\OpenApi\Serializer;
+
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+final class LegacyOpenApiNormalizer implements NormalizerInterface
+{
+    public const SPEC_VERSION = 'spec_version';
+    private array $defaultContext = [
+        self::SPEC_VERSION => '3.1.0',
+    ];
+
+    public function __construct(private readonly NormalizerInterface $decorated, $defaultContext = [])
+    {
+        $this->defaultContext = array_merge($this->defaultContext, $defaultContext);
+    }
+
+    public function normalize(mixed $object, string $format = null, array $context = []): array
+    {
+        $openapi = $this->decorated->normalize($object, $format, $context);
+
+        if ('3.0.0' !== ($context['spec_version'] ?? null)) {
+            return $openapi;
+        }
+
+        $schemas = &$openapi['components']['schemas'];
+        $openapi['openapi'] = '3.0.0';
+        foreach ($openapi['components']['schemas'] as $name => $component) {
+            foreach ($component['properties'] ?? [] as $property => $value) {
+                if (\is_array($value['type'] ?? false)) {
+                    foreach ($value['type'] as $type) {
+                        $schemas[$name]['properties'][$property]['anyOf'][] = ['type' => $type];
+                    }
+                    unset($schemas[$name]['properties'][$property]['type']);
+                }
+                unset($schemas[$name]['properties'][$property]['owl:maxCardinality']);
+            }
+        }
+
+        return $openapi;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
+    {
+        return $this->decorated->supportsNormalization($data, $format, $context);
+    }
+
+    public function getSupportedTypes($format): array
+    {
+        return $this->decorated->getSupportedTypes($format);
+    }
+}
diff --git a/Serializer/OpenApiNormalizer.php b/Serializer/OpenApiNormalizer.php
index 83ebf63..a11fcb6 100644
--- a/Serializer/OpenApiNormalizer.php
+++ b/Serializer/OpenApiNormalizer.php
@@ -26,6 +26,8 @@
 final class OpenApiNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
 {
     public const FORMAT = 'json';
+    public const JSON_FORMAT = 'jsonopenapi';
+    public const YAML_FORMAT = 'yamlopenapi';
     private const EXTENSION_PROPERTIES_KEY = 'extensionProperties';
 
     public function __construct(private readonly NormalizerInterface $decorated)
@@ -37,7 +39,7 @@ public function __construct(private readonly NormalizerInterface $decorated)
      */
     public function normalize(mixed $object, string $format = null, array $context = []): array
     {
-        $pathsCallback = static fn ($innerObject): array => $innerObject instanceof Paths ? $innerObject->getPaths() : [];
+        $pathsCallback = static fn ($decoratedObject): array => $decoratedObject instanceof Paths ? $decoratedObject->getPaths() : [];
         $context[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] = true;
         $context[AbstractObjectNormalizer::SKIP_NULL_VALUES] = true;
         $context[AbstractNormalizer::CALLBACKS] = [
@@ -72,12 +74,12 @@ private function recursiveClean(array $data): array
      */
     public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
     {
-        return self::FORMAT === $format && $data instanceof OpenApi;
+        return (self::FORMAT === $format || self::JSON_FORMAT === $format || self::YAML_FORMAT === $format) && $data instanceof OpenApi;
     }
 
     public function getSupportedTypes($format): array
     {
-        return self::FORMAT === $format ? [OpenApi::class => true] : [];
+        return (self::FORMAT === $format || self::JSON_FORMAT === $format || self::YAML_FORMAT === $format) ? [OpenApi::class => true] : [];
     }
 
     public function hasCacheableSupportsMethod(): bool
diff --git a/Tests/Factory/OpenApiFactoryTest.php b/Tests/Factory/OpenApiFactoryTest.php
index b4c0b96..8d4c5df 100644
--- a/Tests/Factory/OpenApiFactoryTest.php
+++ b/Tests/Factory/OpenApiFactoryTest.php
@@ -85,7 +85,7 @@ public function testInvoke(): void
             'getDummyItem' => (new Get())->withUriTemplate('/dummies/{id}')->withOperation($baseOperation)->withUriVariables(['id' => (new Link())->withFromClass(Dummy::class)->withIdentifiers(['id'])]),
             'putDummyItem' => (new Put())->withUriTemplate('/dummies/{id}')->withOperation($baseOperation)->withUriVariables(['id' => (new Link())->withFromClass(Dummy::class)->withIdentifiers(['id'])]),
             'deleteDummyItem' => (new Delete())->withUriTemplate('/dummies/{id}')->withOperation($baseOperation)->withUriVariables(['id' => (new Link())->withFromClass(Dummy::class)->withIdentifiers(['id'])]),
-            'customDummyItem' => (new HttpOperation())->withMethod(HttpOperation::METHOD_HEAD)->withUriTemplate('/foo/{id}')->withOperation($baseOperation)->withUriVariables(['id' => (new Link())->withFromClass(Dummy::class)->withIdentifiers(['id'])])->withOpenapi(new OpenApiOperation(
+            'customDummyItem' => (new HttpOperation())->withMethod('HEAD')->withUriTemplate('/foo/{id}')->withOperation($baseOperation)->withUriVariables(['id' => (new Link())->withFromClass(Dummy::class)->withIdentifiers(['id'])])->withOpenapi(new OpenApiOperation(
                 tags: ['Dummy', 'Profile'],
                 responses: [
                     '202' => new OpenApiResponse(
@@ -254,34 +254,116 @@ public function testInvoke(): void
 
         $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
         $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::any())->shouldBeCalled()->willReturn(
-            (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withDescription('This is an id.')->withReadable(true)->withWritable(false)->withIdentifier(true)
+            (new ApiProperty())
+                ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])
+                ->withDescription('This is an id.')
+                ->withReadable(true)
+                ->withWritable(false)
+                ->withIdentifier(true)
+                ->withSchema(['type' => 'integer', 'readOnly' => true, 'description' => 'This is an id.'])
         );
         $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->shouldBeCalled()->willReturn(
-            (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withRequired(false)->withIdentifier(false)->withSchema(['minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$'])
+            (new ApiProperty())
+                ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
+                ->withDescription('This is a name.')
+                ->withReadable(true)
+                ->withWritable(true)
+                ->withReadableLink(true)
+                ->withWritableLink(true)
+                ->withRequired(false)
+                ->withIdentifier(false)
+                ->withSchema(['minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$', 'description' => 'This is a name.', 'type' => 'string'])
         );
         $propertyMetadataFactoryProphecy->create(Dummy::class, 'description', Argument::any())->shouldBeCalled()->willReturn(
-            (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('This is an initializable but not writable property.')->withReadable(true)->withWritable(false)->withReadableLink(true)->withWritableLink(true)->withRequired(false)->withIdentifier(false)->withInitializable(true)
+            (new ApiProperty())
+                ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
+                ->withDescription('This is an initializable but not writable property.')
+                ->withReadable(true)
+                ->withWritable(false)
+                ->withReadableLink(true)
+                ->withWritableLink(true)
+                ->withRequired(false)
+                ->withIdentifier(false)
+                ->withInitializable(true)
+                ->withSchema(['type' => 'string', 'description' => 'This is an initializable but not writable property.'])
         );
         $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyDate', Argument::any())->shouldBeCalled()->willReturn(
-            (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class)])->withDescription('This is a \DateTimeInterface object.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withRequired(false)->withIdentifier(false)
+            (new ApiProperty())
+                ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class)])
+                ->withDescription('This is a \DateTimeInterface object.')
+                ->withReadable(true)
+                ->withWritable(true)
+                ->withReadableLink(true)
+                ->withWritableLink(true)
+                ->withRequired(false)
+                ->withIdentifier(false)
+                ->withSchema(['type' => ['string', 'null'], 'description' => 'This is a \DateTimeInterface object.', 'format' => 'date-time'])
         );
         $propertyMetadataFactoryProphecy->create(Dummy::class, 'enum', Argument::any())->shouldBeCalled()->willReturn(
-            (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('This is an enum.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withRequired(false)->withIdentifier(false)->withOpenapiContext(['type' => 'string', 'enum' => ['one', 'two'], 'example' => 'one'])
+            (new ApiProperty())
+                ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
+                ->withDescription('This is an enum.')
+                ->withReadable(true)
+                ->withWritable(true)
+                ->withReadableLink(true)
+                ->withWritableLink(true)
+                ->withRequired(false)
+                ->withIdentifier(false)
+                ->withSchema(['type' => 'string', 'description' => 'This is an enum.'])
+                ->withOpenapiContext(['type' => 'string', 'enum' => ['one', 'two'], 'example' => 'one'])
         );
         $propertyMetadataFactoryProphecy->create(OutputDto::class, 'id', Argument::any())->shouldBeCalled()->willReturn(
-            (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withDescription('This is an id.')->withReadable(true)->withWritable(false)->withIdentifier(true)
+            (new ApiProperty())
+                ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])
+                ->withDescription('This is an id.')
+                ->withReadable(true)
+                ->withWritable(false)
+                ->withIdentifier(true)
+                ->withSchema(['type' => 'integer', 'description' => 'This is an id.', 'readOnly' => true])
         );
         $propertyMetadataFactoryProphecy->create(OutputDto::class, 'name', Argument::any())->shouldBeCalled()->willReturn(
-            (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withRequired(false)->withIdentifier(false)->withSchema(['minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$'])
+            (new ApiProperty())
+                ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
+                ->withDescription('This is a name.')
+                ->withReadable(true)
+                ->withWritable(true)
+                ->withReadableLink(true)
+                ->withWritableLink(true)
+                ->withRequired(false)
+                ->withIdentifier(false)
+                ->withSchema(['type' => 'string', 'description' => 'This is a name.', 'minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$'])
         );
         $propertyMetadataFactoryProphecy->create(OutputDto::class, 'description', Argument::any())->shouldBeCalled()->willReturn(
-            (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('This is an initializable but not writable property.')->withReadable(true)->withWritable(false)->withReadableLink(true)->withWritableLink(true)->withInitializable(true)
+            (new ApiProperty())
+                ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
+                ->withDescription('This is an initializable but not writable property.')
+                ->withReadable(true)
+                ->withWritable(false)
+                ->withReadableLink(true)
+                ->withWritableLink(true)
+                ->withInitializable(true)
+                ->withSchema(['type' => 'string', 'description' => 'This is an initializable but not writable property.'])
         );
         $propertyMetadataFactoryProphecy->create(OutputDto::class, 'dummyDate', Argument::any())->shouldBeCalled()->willReturn(
-            (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class)])->withDescription('This is a \DateTimeInterface object.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
+            (new ApiProperty())
+                ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class)])
+                ->withDescription('This is a \DateTimeInterface object.')
+                ->withReadable(true)
+                ->withWritable(true)
+                ->withReadableLink(true)
+                ->withWritableLink(true)
+                ->withSchema(['type' => ['string', 'null'], 'format' => 'date-time', 'description' => 'This is a \DateTimeInterface object.'])
         );
         $propertyMetadataFactoryProphecy->create(OutputDto::class, 'enum', Argument::any())->shouldBeCalled()->willReturn(
-            (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('This is an enum.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withOpenapiContext(['type' => 'string', 'enum' => ['one', 'two'], 'example' => 'one'])
+            (new ApiProperty())
+                ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])
+                ->withDescription('This is an enum.')
+                ->withReadable(true)
+                ->withWritable(true)
+                ->withReadableLink(true)
+                ->withWritableLink(true)
+                ->withSchema(['type' => 'string', 'description' => 'This is an enum.'])
+                ->withOpenapiContext(['type' => 'string', 'enum' => ['one', 'two'], 'example' => 'one'])
         );
 
         $filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
@@ -329,8 +411,9 @@ public function testInvoke(): void
 
         $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal();
 
+        $schemaFactory = new SchemaFactory(null, $resourceCollectionMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
+
         $typeFactory = new TypeFactory();
-        $schemaFactory = new SchemaFactory($typeFactory, $resourceCollectionMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
         $typeFactory->setSchemaFactory($schemaFactory);
 
         $factory = new OpenApiFactory(
@@ -379,10 +462,9 @@ public function testInvoke(): void
                     'description' => 'This is an initializable but not writable property.',
                 ]),
                 'dummy_date' => new \ArrayObject([
-                    'type' => 'string',
+                    'type' => ['string', 'null'],
                     'description' => 'This is a \DateTimeInterface object.',
                     'format' => 'date-time',
-                    'nullable' => true,
                 ]),
                 'enum' => new \ArrayObject([
                     'type' => 'string',
diff --git a/Tests/Fixtures/OutputDto.php b/Tests/Fixtures/OutputDto.php
index 0ca3227..068aaa7 100644
--- a/Tests/Fixtures/OutputDto.php
+++ b/Tests/Fixtures/OutputDto.php
@@ -13,8 +13,6 @@
 
 namespace ApiPlatform\OpenApi\Tests\Fixtures;
 
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
-
 /**
  * @author Kévin Dunglas <dunglas@gmail.com>
  */
diff --git a/Tests/Serializer/ApiGatewayNormalizerTest.php b/Tests/Serializer/ApiGatewayNormalizerTest.php
index 37dc305..d52dad9 100644
--- a/Tests/Serializer/ApiGatewayNormalizerTest.php
+++ b/Tests/Serializer/ApiGatewayNormalizerTest.php
@@ -21,35 +21,12 @@
 use PHPUnit\Framework\TestCase;
 use Prophecy\Argument;
 use Prophecy\PhpUnit\ProphecyTrait;
-use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
 use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
-use Symfony\Component\Serializer\Serializer;
 
 final class ApiGatewayNormalizerTest extends TestCase
 {
     use ProphecyTrait;
 
-    /**
-     * @group legacy
-     */
-    public function testSupportsNormalization(): void
-    {
-        $normalizerProphecy = $this->prophesize(NormalizerInterface::class);
-        $normalizerProphecy->supportsNormalization(OpenApiNormalizer::FORMAT, OpenApi::class)->willReturn(true);
-        if (!method_exists(Serializer::class, 'getSupportedTypes')) {
-            $normalizerProphecy->willImplement(CacheableSupportsMethodInterface::class);
-            $normalizerProphecy->hasCacheableSupportsMethod()->willReturn(true);
-        }
-
-        $normalizer = new ApiGatewayNormalizer($normalizerProphecy->reveal());
-
-        $this->assertTrue($normalizer->supportsNormalization(OpenApiNormalizer::FORMAT, OpenApi::class));
-
-        if (!method_exists(Serializer::class, 'getSupportedTypes')) {
-            $this->assertTrue($normalizer->hasCacheableSupportsMethod());
-        }
-    }
-
     public function testNormalize(): void
     {
         $swaggerDocument = [
diff --git a/Tests/Serializer/OpenApiNormalizerTest.php b/Tests/Serializer/OpenApiNormalizerTest.php
index 9a5c924..632ce4d 100644
--- a/Tests/Serializer/OpenApiNormalizerTest.php
+++ b/Tests/Serializer/OpenApiNormalizerTest.php
@@ -42,8 +42,8 @@
 use ApiPlatform\OpenApi\OpenApi;
 use ApiPlatform\OpenApi\Options;
 use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer;
+use ApiPlatform\OpenApi\Tests\Fixtures\Dummy;
 use ApiPlatform\State\Pagination\PaginationOptions;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
 use PHPUnit\Framework\TestCase;
 use Prophecy\Argument;
 use Prophecy\PhpUnit\ProphecyTrait;
@@ -151,6 +151,7 @@ public function testNormalize(): void
                 ->withReadable(true)
                 ->withWritable(false)
                 ->withIdentifier(true)
+                ->withSchema(['type' => 'integer', 'description' => 'This is an id.', 'readOnly' => true])
         );
         $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->shouldBeCalled()->willReturn(
             (new ApiProperty())
@@ -162,7 +163,7 @@ public function testNormalize(): void
                 ->withWritableLink(true)
                 ->withRequired(false)
                 ->withIdentifier(false)
-                ->withSchema(['minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$'])
+                ->withSchema(['type' => 'string', 'description' => 'This is a name.', 'minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$'])
         );
         $propertyMetadataFactoryProphecy->create(Dummy::class, 'description', Argument::any())->shouldBeCalled()->willReturn(
             (new ApiProperty())
@@ -174,6 +175,7 @@ public function testNormalize(): void
                 ->withWritableLink(true)
                 ->withRequired(false)
                 ->withIdentifier(false)
+                ->withSchema(['type' => 'string', 'readOnly' => true, 'description' => 'This is an initializable but not writable property.'])
         );
         $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyDate', Argument::any())->shouldBeCalled()->willReturn(
             (new ApiProperty())
@@ -185,6 +187,7 @@ public function testNormalize(): void
                 ->withWritableLink(true)
                 ->withRequired(false)
                 ->withIdentifier(false)
+                ->withSchema(['type' => 'string', 'format' => 'date-time', 'description' => 'This is a \DateTimeInterface object.'])
         );
 
         $propertyMetadataFactoryProphecy->create('Zorro', 'id', Argument::any())->shouldBeCalled()->willReturn(
@@ -194,6 +197,7 @@ public function testNormalize(): void
                 ->withReadable(true)
                 ->withWritable(false)
                 ->withIdentifier(true)
+                ->withSchema(['type' => 'integer', 'description' => 'This is an id.', 'readOnly' => true])
         );
 
         $filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
@@ -201,8 +205,9 @@ public function testNormalize(): void
         $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal();
         $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal();
 
+        $schemaFactory = new SchemaFactory(null, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
+
         $typeFactory = new TypeFactory();
-        $schemaFactory = new SchemaFactory($typeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
         $typeFactory->setSchemaFactory($schemaFactory);
 
         $factory = new OpenApiFactory(
diff --git a/composer.json b/composer.json
index 0c1d8e7..076ae99 100644
--- a/composer.json
+++ b/composer.json
@@ -31,14 +31,17 @@
         "api-platform/json-schema": "*@dev || ^3.1",
         "api-platform/metadata": "*@dev || ^3.1",
         "api-platform/state": "*@dev || ^3.1",
-        "symfony/console": "^6.1",
-        "symfony/property-access": "^6.1",
-        "symfony/serializer": "^6.1",
+        "symfony/console": "^6.4 || ^7.0",
+        "symfony/property-access": "^6.4 || ^7.0",
+        "symfony/serializer": "^6.4 || ^7.0",
         "sebastian/comparator": "<5.0"
     },
     "require-dev": {
         "phpspec/prophecy-phpunit": "^2.0",
-        "symfony/phpunit-bridge": "^6.1"
+        "symfony/phpunit-bridge": "^6.4 || ^7.0",
+        "api-platform/doctrine-common": "*@dev || ^3.2",
+        "api-platform/doctrine-orm": "*@dev || ^3.2",
+        "api-platform/doctrine-odm": "*@dev || ^3.2"
     },
     "autoload": {
         "psr-4": {
@@ -48,6 +51,9 @@
             "/Tests/"
         ]
     },
+    "conflict": {
+        "sebastian/comparator": ">=5.0"
+    },
     "config": {
         "preferred-install": {
             "*": "dist"
@@ -63,7 +69,7 @@
             "dev-main": "3.2.x-dev"
         },
         "symfony": {
-            "require": "^6.1"
+            "require": "^6.4"
         }
     },
     "repositories": [
@@ -78,6 +84,18 @@
         {
             "type": "path",
             "url": "../State"
+        },
+        {
+            "type": "path",
+            "url": "../Doctrine/Orm"
+        },
+        {
+            "type": "path",
+            "url": "../Doctrine/Odm"
+        },
+        {
+            "type": "path",
+            "url": "../Doctrine/Common"
         }
     ]
 }
