diff --git a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml
index ac713781345..c76cf0ff672 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml
@@ -54,6 +54,12 @@
+
+
+
+
+
+
diff --git a/src/Hal/JsonSchema/SchemaFactory.php b/src/Hal/JsonSchema/SchemaFactory.php
new file mode 100644
index 00000000000..ca14e872481
--- /dev/null
+++ b/src/Hal/JsonSchema/SchemaFactory.php
@@ -0,0 +1,157 @@
+
+ *
+ * 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\Core\Hal\JsonSchema;
+
+use ApiPlatform\Core\JsonSchema\Schema;
+use ApiPlatform\Core\JsonSchema\SchemaFactory as BaseSchemaFactory;
+use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;
+
+/**
+ * Decorator factory which adds HAL properties to the JSON Schema document.
+ *
+ * @experimental
+ *
+ * @author Kévin Dunglas
+ * @author Jachim Coudenys
+ */
+final class SchemaFactory implements SchemaFactoryInterface
+{
+ private const BASE_PROPS = [
+ '_links' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'self' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'href' => [
+ 'type' => 'string',
+ 'format' => 'iri-reference',
+ ]
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ private $schemaFactory;
+
+ public function __construct(SchemaFactoryInterface $schemaFactory)
+ {
+ $this->schemaFactory = $schemaFactory;
+
+ if ($schemaFactory instanceof BaseSchemaFactory) {
+ $schemaFactory->addDistinctFormat('jsonhal');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildSchema(string $className, string $format = 'jsonhal', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
+ {
+ $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
+ if ('jsonhal' !== $format) {
+ return $schema;
+ }
+
+ $definitions = $schema->getDefinitions();
+ if ($key = $schema->getRootDefinitionKey()) {
+ $definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []);
+
+ return $schema;
+ }
+ if ($key = $schema->getItemsDefinitionKey()) {
+ $definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []);
+ }
+
+ if (($schema['type'] ?? '') === 'array') {
+ $items = $schema['items'];
+ unset($schema['items']);
+
+ $schema['type'] = 'object';
+ $schema['properties'] = [
+ '_embedded' => [
+ 'type' => 'array',
+ 'items' => $items,
+ ],
+ 'totalItems' => [
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ],
+ 'itemsPerPage' => [
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ],
+ '_links' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'self' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'href' => [
+ 'type' => 'string',
+ 'format' => 'iri-reference',
+ ]
+ ],
+ ],
+ 'first' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'href' => [
+ 'type' => 'string',
+ 'format' => 'iri-reference',
+ ]
+ ],
+ ],
+ 'last' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'href' => [
+ 'type' => 'string',
+ 'format' => 'iri-reference',
+ ]
+ ],
+ ],
+ 'next' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'href' => [
+ 'type' => 'string',
+ 'format' => 'iri-reference',
+ ]
+ ],
+ ],
+ 'previous' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'href' => [
+ 'type' => 'string',
+ 'format' => 'iri-reference',
+ ]
+ ],
+ ],
+ ],
+ ],
+ ];
+ $schema['required'] = [
+ '_links',
+ '_embedded',
+ ];
+
+ return $schema;
+ }
+
+ return $schema;
+ }
+}