• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 18116719337

30 Sep 2025 02:25AM UTC coverage: 0.0% (-22.0%) from 21.956%
18116719337

Pull #7397

github

web-flow
Merge ad9be8ad8 into 55fd65795
Pull Request #7397: fix(jsonschema/jsonld): make `@id` and `@type` properties required only in the JSON-LD schema for output

0 of 15 new or added lines in 1 file covered. (0.0%)

12143 existing lines in 402 files now uncovered.

0 of 53916 relevant lines covered (0.0%)

0.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Hydra/JsonSchema/SchemaFactory.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace ApiPlatform\Hydra\JsonSchema;
15

16
use ApiPlatform\JsonLd\ContextBuilder;
17
use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait;
18
use ApiPlatform\JsonSchema\DefinitionNameFactory;
19
use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface;
20
use ApiPlatform\JsonSchema\ResourceMetadataTrait;
21
use ApiPlatform\JsonSchema\Schema;
22
use ApiPlatform\JsonSchema\SchemaFactoryAwareInterface;
23
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
24
use ApiPlatform\JsonSchema\SchemaUriPrefixTrait;
25
use ApiPlatform\Metadata\Operation;
26
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
27

28
/**
29
 * Decorator factory which adds Hydra properties to the JSON Schema document.
30
 *
31
 * @author Kévin Dunglas <dunglas@gmail.com>
32
 */
33
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
34
{
35
    use HydraPrefixTrait;
36
    use ResourceMetadataTrait;
37
    use SchemaUriPrefixTrait;
38

39
    private const ITEM_BASE_SCHEMA_NAME = 'HydraItemBaseSchema';
40
    private const ITEM_BASE_SCHEMA_OUTPUT_NAME = 'HydraOutputBaseSchema';
41
    private const COLLECTION_BASE_SCHEMA_NAME = 'HydraCollectionBaseSchema';
42
    private const BASE_PROP = [
43
        'type' => 'string',
44
    ];
45
    private const BASE_PROPS = [
46
        '@id' => self::BASE_PROP,
47
        '@type' => self::BASE_PROP,
48
    ];
49
    private const ITEM_BASE_SCHEMA = [
50
        'type' => 'object',
51
        'properties' => [
52
            '@context' => [
53
                'oneOf' => [
54
                    ['type' => 'string'],
55
                    [
56
                        'type' => 'object',
57
                        'properties' => [
58
                            '@vocab' => [
59
                                'type' => 'string',
60
                            ],
61
                            'hydra' => [
62
                                'type' => 'string',
63
                                'enum' => [ContextBuilder::HYDRA_NS],
64
                            ],
65
                        ],
66
                        'required' => ['@vocab', 'hydra'],
67
                        'additionalProperties' => true,
68
                    ],
69
                ],
70
            ],
71
        ] + self::BASE_PROPS,
72
    ];
73

74
    private const ITEM_BASE_SCHEMA_OUTPUT = [
75
        'required' => ['@id', '@type'],
76
    ] + self::ITEM_BASE_SCHEMA;
77

78
    /**
79
     * @var array<string, true>
80
     */
81
    private array $transformed = [];
82

83
    /**
84
     * @param array<string, mixed> $defaultContext
85
     */
86
    public function __construct(
87
        private readonly SchemaFactoryInterface $schemaFactory,
88
        private readonly array $defaultContext = [],
89
        private ?DefinitionNameFactoryInterface $definitionNameFactory = null,
90
        ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null,
91
    ) {
UNCOV
92
        if (!$definitionNameFactory) {
×
93
            $this->definitionNameFactory = new DefinitionNameFactory();
×
94
        }
UNCOV
95
        $this->resourceMetadataFactory = $resourceMetadataFactory;
×
96

UNCOV
97
        if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
×
UNCOV
98
            $this->schemaFactory->setSchemaFactory($this);
×
99
        }
100
    }
101

102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function buildSchema(string $className, string $format = 'jsonld', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
106
    {
NEW
107
        if ('jsonld' !== $format) {
×
UNCOV
108
            return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
×
109
        }
UNCOV
110
        if (!$this->isResourceClass($className)) {
×
UNCOV
111
            $operation = null;
×
UNCOV
112
            $inputOrOutputClass = null;
×
UNCOV
113
            $serializerContext ??= [];
×
114
        } else {
UNCOV
115
            $operation = $this->findOperation($className, $type, $operation, $serializerContext, $format);
×
UNCOV
116
            $inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext);
×
UNCOV
117
            $serializerContext ??= $this->getSerializerContext($operation, $type);
×
118
        }
119

UNCOV
120
        if (null === $inputOrOutputClass) {
×
121
            // input or output disabled
UNCOV
122
            return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
×
123
        }
124

125
        // JSON-LD is slightly different then JSON:API or HAL
126
        // All the references that are resources must also be in JSON-LD therefore combining
127
        // the HydraItemBaseSchema and the JSON schema is harder (unless we loop again through all relationship)
128
        // The less intensive path is to compute the jsonld schemas, then to combine in an allOf
UNCOV
129
        $schema = $this->schemaFactory->buildSchema($className, 'jsonld', $type, $operation, $schema, $serializerContext, $forceCollection);
×
UNCOV
130
        $definitions = $schema->getDefinitions();
×
131

UNCOV
132
        $prefix = $this->getSchemaUriPrefix($schema->getVersion());
×
UNCOV
133
        $collectionKey = $schema->getItemsDefinitionKey();
×
134

NEW
135
        $definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext);
×
NEW
136
        if (Schema::TYPE_INPUT === $type) {
×
NEW
137
            $definitionName .= '.input';
×
138
        }
139

NEW
140
        $jsonSchema = $this->schemaFactory->buildSchema($className, 'json', $type, $operation, new Schema(version: $schema->getVersion()), $serializerContext, $forceCollection);
×
NEW
141
        $jsonKey = $jsonSchema->getRootDefinitionKey() ?? $jsonSchema->getItemsDefinitionKey();
×
NEW
142
        $jsonDefinition = $jsonSchema->getDefinitions()[$jsonKey] ?? null;
×
143

UNCOV
144
        if (!$collectionKey) {
×
NEW
145
            $schema['$ref'] = $prefix.$definitionName;
×
146

UNCOV
147
            if ($this->transformed[$definitionName] ?? false) {
×
UNCOV
148
                return $schema;
×
149
            }
150

NEW
151
            $baseName = Schema::TYPE_OUTPUT === $type ? self::ITEM_BASE_SCHEMA_OUTPUT_NAME : self::ITEM_BASE_SCHEMA_NAME;
×
152

UNCOV
153
            if ($this->isResourceClass($inputOrOutputClass)) {
×
UNCOV
154
                if (!isset($definitions[$baseName])) {
×
UNCOV
155
                    $definitions[$baseName] = Schema::TYPE_OUTPUT === $type ? self::ITEM_BASE_SCHEMA_OUTPUT : self::ITEM_BASE_SCHEMA;
×
156
                }
157
            }
158

UNCOV
159
            $allOf = new \ArrayObject(['allOf' => [
×
UNCOV
160
                ['$ref' => $prefix.$baseName],
×
161
                // It cannot always be referenced, as there may be resources that don't have a JSON schema but only a JSON-LD schema.
162
                // It's not certain at the time the JSON-LD schema is being generated whether a JSON schema will ultimately be defined.
163
                // Therefore, the JSON schema will be referenced only if it's already defined.
NEW
164
                isset($definitions[$jsonKey]) ? ['$ref' => $prefix.$jsonKey] : $jsonDefinition,
×
UNCOV
165
            ]]);
×
166

NEW
167
            if (isset($definitions[$jsonKey]['description'])) {
×
NEW
168
                $allOf['description'] = $definitions[$jsonKey]['description'];
×
169
            }
170

UNCOV
171
            $definitions[$definitionName] = $allOf;
×
UNCOV
172
            unset($definitions[$definitionName]['allOf'][1]['description']);
×
173

UNCOV
174
            $this->transformed[$definitionName] = true;
×
175

UNCOV
176
            return $schema;
×
177
        }
178

UNCOV
179
        if (($schema['type'] ?? '') !== 'array') {
×
180
            return $schema;
×
181
        }
182

UNCOV
183
        $hydraPrefix = $this->getHydraPrefix($serializerContext + $this->defaultContext);
×
184

UNCOV
185
        if (!isset($definitions[self::COLLECTION_BASE_SCHEMA_NAME])) {
×
UNCOV
186
            switch ($schema->getVersion()) {
×
187
                // JSON Schema + OpenAPI 3.1
UNCOV
188
                case Schema::VERSION_OPENAPI:
×
UNCOV
189
                case Schema::VERSION_JSON_SCHEMA:
×
UNCOV
190
                    $nullableStringDefinition = ['type' => ['string', 'null']];
×
UNCOV
191
                    break;
×
192
                    // Swagger
193
                default:
194
                    $nullableStringDefinition = ['type' => 'string'];
×
195
                    break;
×
196
            }
197

UNCOV
198
            $definitions[self::COLLECTION_BASE_SCHEMA_NAME] = [
×
UNCOV
199
                'type' => 'object',
×
UNCOV
200
                'required' => [
×
UNCOV
201
                    $hydraPrefix.'member',
×
UNCOV
202
                    'items' => ['type' => 'object'],
×
UNCOV
203
                ],
×
UNCOV
204
                'properties' => [
×
UNCOV
205
                    $hydraPrefix.'member' => [
×
UNCOV
206
                        'type' => 'array',
×
UNCOV
207
                    ],
×
UNCOV
208
                    $hydraPrefix.'totalItems' => [
×
UNCOV
209
                        'type' => 'integer',
×
UNCOV
210
                        'minimum' => 0,
×
UNCOV
211
                    ],
×
UNCOV
212
                    $hydraPrefix.'view' => [
×
UNCOV
213
                        'type' => 'object',
×
UNCOV
214
                        'properties' => [
×
UNCOV
215
                            '@id' => [
×
UNCOV
216
                                'type' => 'string',
×
UNCOV
217
                                'format' => 'iri-reference',
×
UNCOV
218
                            ],
×
UNCOV
219
                            '@type' => [
×
UNCOV
220
                                'type' => 'string',
×
UNCOV
221
                            ],
×
UNCOV
222
                            $hydraPrefix.'first' => [
×
UNCOV
223
                                'type' => 'string',
×
UNCOV
224
                                'format' => 'iri-reference',
×
UNCOV
225
                            ],
×
UNCOV
226
                            $hydraPrefix.'last' => [
×
UNCOV
227
                                'type' => 'string',
×
UNCOV
228
                                'format' => 'iri-reference',
×
UNCOV
229
                            ],
×
UNCOV
230
                            $hydraPrefix.'previous' => [
×
UNCOV
231
                                'type' => 'string',
×
UNCOV
232
                                'format' => 'iri-reference',
×
UNCOV
233
                            ],
×
UNCOV
234
                            $hydraPrefix.'next' => [
×
UNCOV
235
                                'type' => 'string',
×
UNCOV
236
                                'format' => 'iri-reference',
×
UNCOV
237
                            ],
×
UNCOV
238
                        ],
×
UNCOV
239
                        'example' => [
×
UNCOV
240
                            '@id' => 'string',
×
UNCOV
241
                            'type' => 'string',
×
UNCOV
242
                            $hydraPrefix.'first' => 'string',
×
UNCOV
243
                            $hydraPrefix.'last' => 'string',
×
UNCOV
244
                            $hydraPrefix.'previous' => 'string',
×
UNCOV
245
                            $hydraPrefix.'next' => 'string',
×
UNCOV
246
                        ],
×
UNCOV
247
                    ],
×
UNCOV
248
                    $hydraPrefix.'search' => [
×
UNCOV
249
                        'type' => 'object',
×
UNCOV
250
                        'properties' => [
×
UNCOV
251
                            '@type' => ['type' => 'string'],
×
UNCOV
252
                            $hydraPrefix.'template' => ['type' => 'string'],
×
UNCOV
253
                            $hydraPrefix.'variableRepresentation' => ['type' => 'string'],
×
UNCOV
254
                            $hydraPrefix.'mapping' => [
×
UNCOV
255
                                'type' => 'array',
×
UNCOV
256
                                'items' => [
×
UNCOV
257
                                    'type' => 'object',
×
UNCOV
258
                                    'properties' => [
×
UNCOV
259
                                        '@type' => ['type' => 'string'],
×
UNCOV
260
                                        'variable' => ['type' => 'string'],
×
UNCOV
261
                                        'property' => $nullableStringDefinition,
×
UNCOV
262
                                        'required' => ['type' => 'boolean'],
×
UNCOV
263
                                    ],
×
UNCOV
264
                                ],
×
UNCOV
265
                            ],
×
UNCOV
266
                        ],
×
UNCOV
267
                    ],
×
UNCOV
268
                ],
×
UNCOV
269
            ];
×
270
        }
271

UNCOV
272
        $schema['type'] = 'object';
×
UNCOV
273
        $schema['description'] = "$definitionName collection.";
×
UNCOV
274
        $schema['allOf'] = [
×
UNCOV
275
            ['$ref' => $prefix.self::COLLECTION_BASE_SCHEMA_NAME],
×
UNCOV
276
            [
×
UNCOV
277
                'type' => 'object',
×
UNCOV
278
                'properties' => [
×
UNCOV
279
                    $hydraPrefix.'member' => [
×
UNCOV
280
                        'type' => 'array',
×
NEW
281
                        'items' => [
×
NEW
282
                            '$ref' => $prefix.$definitionName,
×
NEW
283
                        ],
×
UNCOV
284
                    ],
×
UNCOV
285
                ],
×
UNCOV
286
            ],
×
UNCOV
287
        ];
×
288

UNCOV
289
        unset($schema['items']);
×
290

UNCOV
291
        return $schema;
×
292
    }
293

294
    public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
295
    {
UNCOV
296
        if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
×
UNCOV
297
            $this->schemaFactory->setSchemaFactory($schemaFactory);
×
298
        }
299
    }
300
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc