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

api-platform / core / 20528769615

26 Dec 2025 08:17PM UTC coverage: 25.119%. First build
20528769615

Pull #7629

github

web-flow
Merge 4691c25d0 into 38d474d1b
Pull Request #7629: fix: add support for normalization/denormalization with attributes

24 of 236 new or added lines in 8 files covered. (10.17%)

14638 of 58274 relevant lines covered (25.12%)

29.5 hits per line

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

61.67
/src/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\JsonSchema;
15

16
use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory;
17
use ApiPlatform\Metadata\ApiProperty;
18
use ApiPlatform\Metadata\CollectionOperationInterface;
19
use ApiPlatform\Metadata\HttpOperation;
20
use ApiPlatform\Metadata\Operation;
21
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
22
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
23
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
24
use ApiPlatform\Metadata\ResourceClassResolverInterface;
25
use ApiPlatform\Metadata\Util\TypeHelper;
26
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
27
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
28
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
29
use Symfony\Component\TypeInfo\Type\BuiltinType;
30
use Symfony\Component\TypeInfo\Type\CollectionType;
31
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
32
use Symfony\Component\TypeInfo\Type\GenericType;
33
use Symfony\Component\TypeInfo\Type\ObjectType;
34
use Symfony\Component\TypeInfo\TypeIdentifier;
35

36
/**
37
 * {@inheritdoc}
38
 *
39
 * @author Kévin Dunglas <dunglas@gmail.com>
40
 */
41
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
42
{
43
    use ResourceMetadataTrait;
44
    use SchemaUriPrefixTrait;
45

46
    private ?SchemaFactoryInterface $schemaFactory = null;
47
    // Edge case where the related resource is not readable (for example: NotExposed) but we have groups to read the whole related object
48
    public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name';
49

50
    public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, ?array $distinctFormats = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null)
51
    {
52
        if (!$definitionNameFactory) {
821✔
53
            $this->definitionNameFactory = new DefinitionNameFactory($distinctFormats);
×
54
        }
55

56
        $this->resourceMetadataFactory = $resourceMetadataFactory;
821✔
57
        $this->resourceClassResolver = $resourceClassResolver;
821✔
58
    }
59

60
    /**
61
     * {@inheritdoc}
62
     */
63
    public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
64
    {
65
        $schema = $schema ? clone $schema : new Schema();
166✔
66

67
        if (!$this->isResourceClass($className)) {
166✔
68
            $operation = null;
90✔
69
            $inputOrOutputClass = $className;
90✔
70
            $serializerContext ??= [];
90✔
71
        } else {
72
            $operation = $this->findOperation($className, $type, $operation, $serializerContext, $format);
146✔
73
            $inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext);
146✔
74
            $serializerContext ??= $this->getSerializerContext($operation, $type);
146✔
75
        }
76

77
        if (null === $inputOrOutputClass) {
166✔
78
            // input or output disabled
79
            return $schema;
×
80
        }
81

82
        $validationGroups = $operation ? $this->getValidationGroups($operation) : [];
166✔
83
        $version = $schema->getVersion();
166✔
84
        $method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET';
166✔
85
        if (!$operation) {
166✔
86
            $method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET';
90✔
87
        }
88

89
        // In case of FORCE_SUBSCHEMA an object can be writable through another class even though it has no POST operation
90
        if (!($serializerContext[self::FORCE_SUBSCHEMA] ?? false) && Schema::TYPE_OUTPUT !== $type && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) {
166✔
91
            return $schema;
×
92
        }
93

94
        $isJsonMergePatch = 'json' === $format && 'PATCH' === $method && Schema::TYPE_INPUT === $type;
166✔
95
        $definitionFormat = match (true) {
166✔
96
            default => $format,
166✔
97
            $isJsonMergePatch => 'merge-patch+json',
166✔
98
        };
166✔
99

100
        $definitionName = $this->definitionNameFactory->create($className, $definitionFormat, $inputOrOutputClass, $operation, $serializerContext + ['schema_type' => $type]);
166✔
101

102
        if (!isset($schema['$ref']) && !isset($schema['type'])) {
166✔
103
            $ref = $this->getSchemaUriPrefix($version).$definitionName;
166✔
104
            if ($forceCollection || ('POST' !== $method && $operation instanceof CollectionOperationInterface)) {
166✔
105
                $schema['type'] = 'array';
94✔
106
                $schema['items'] = ['$ref' => $ref];
94✔
107
            } else {
108
                $schema['$ref'] = $ref;
142✔
109
            }
110
        }
111

112
        $definitions = $schema->getDefinitions();
166✔
113
        if (isset($definitions[$definitionName])) {
166✔
114
            // Already computed
115
            return $schema;
62✔
116
        }
117

118
        /** @var \ArrayObject<string, mixed> $definition */
119
        $definition = new \ArrayObject(['type' => 'object']);
166✔
120
        $definitions[$definitionName] = $definition;
166✔
121
        if ($description = $operation?->getDescription()) {
166✔
122
            $definition['description'] = $description;
80✔
123
        }
124

125
        // additionalProperties are allowed by default, so it does not need to be set explicitly, unless allow_extra_attributes is false
126
        // See https://json-schema.org/understanding-json-schema/reference/object.html#properties
127
        if (false === ($serializerContext[AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES] ?? true)) {
166✔
128
            $definition['additionalProperties'] = false;
×
129
        }
130

131
        // see https://github.com/json-schema-org/json-schema-spec/pull/737
132
        if (Schema::VERSION_SWAGGER !== $version && $operation && $operation->getDeprecationReason()) {
166✔
133
            $definition['deprecated'] = true;
28✔
134
        }
135

136
        // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it
137
        // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4
138
        if ($operation instanceof HttpOperation && ($operation->getTypes()[0] ?? null)) {
166✔
139
            $definition['externalDocs'] = ['url' => $operation->getTypes()[0]];
36✔
140
        }
141

142
        $options = ['schema_type' => $type] + $this->getFactoryOptions($serializerContext, $validationGroups, $operation instanceof HttpOperation ? $operation : null);
166✔
143
        foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) {
166✔
144
            $propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName, $options);
166✔
145

146
            if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) {
166✔
147
                continue;
56✔
148
            }
149

150
            $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $inputOrOutputClass, $format, $serializerContext) : $propertyName;
166✔
151
            if ($propertyMetadata->isRequired() && !$isJsonMergePatch) {
166✔
152
                $definition['required'][] = $normalizedPropertyName;
50✔
153
            }
154

155
            if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
166✔
156
                $this->buildLegacyPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format, $type);
×
157
            } else {
158
                $this->buildPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format, $type);
166✔
159
            }
160
        }
161

162
        return $schema;
166✔
163
    }
164

165
    /**
166
     * Builds the JSON Schema for a property using the legacy PropertyInfo component.
167
     */
168
    private function buildLegacyPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void
169
    {
170
        $version = $schema->getVersion();
×
171
        if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
×
172
            $additionalPropertySchema = $propertyMetadata->getOpenapiContext();
×
173
        } else {
174
            $additionalPropertySchema = $propertyMetadata->getJsonSchemaContext();
×
175
        }
176

177
        $propertySchema = array_merge(
×
178
            $propertyMetadata->getSchema() ?? [],
×
179
            $additionalPropertySchema ?? []
×
180
        );
×
181

182
        // @see https://github.com/api-platform/core/issues/6299
183
        if (Schema::UNKNOWN_TYPE === ($propertySchema['type'] ?? null) && isset($propertySchema['$ref'])) {
×
184
            unset($propertySchema['type']);
×
185
        }
186

187
        $extraProperties = $propertyMetadata->getExtraProperties();
×
188
        // see AttributePropertyMetadataFactory
189
        if (true === ($extraProperties[SchemaPropertyMetadataFactory::JSON_SCHEMA_USER_DEFINED] ?? false)) {
×
190
            // schema seems to have been declared by the user: do not override nor complete user value
191
            $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
×
192

193
            return;
×
194
        }
195

196
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
×
197

198
        // never override the following keys if at least one is already set
199
        // or if property has no type(s) defined
200
        // or if property schema is already fully defined (type=string + format || enum)
201
        $propertySchemaType = $propertySchema['type'] ?? false;
×
202

203
        $isUnknown = Schema::UNKNOWN_TYPE === $propertySchemaType
×
204
            || ('array' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['items']['type'] ?? null))
×
205
            || ('object' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['additionalProperties']['type'] ?? null));
×
206

207
        // Scalar properties
208
        if (
209
            !$isUnknown && (
×
210
                [] === $types
×
211
                || ($propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false)
×
212
                || (\is_array($propertySchemaType) ? \array_key_exists('string', $propertySchemaType) : 'string' !== $propertySchemaType)
×
213
                || ($propertySchema['format'] ?? $propertySchema['enum'] ?? false)
×
214
            )
215
        ) {
216
            if (isset($propertySchema['$ref'])) {
×
217
                unset($propertySchema['type']);
×
218
            }
219

220
            $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
×
221

222
            return;
×
223
        }
224

225
        // property schema is created in SchemaPropertyMetadataFactory, but it cannot build resource reference ($ref)
226
        // complete property schema with resource reference ($ref) only if it's related to an object
227
        $version = $schema->getVersion();
×
228
        $refs = [];
×
229
        $isNullable = null;
×
230

231
        foreach ($types as $type) {
×
232
            $subSchema = new Schema($version);
×
233
            $subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema
×
234

235
            $isCollection = $type->isCollection();
×
236
            if ($isCollection) {
×
237
                $valueType = $type->getCollectionValueTypes()[0] ?? null;
×
238
            } else {
239
                $valueType = $type;
×
240
            }
241

242
            $className = $valueType?->getClassName();
×
243
            if (null === $className) {
×
244
                continue;
×
245
            }
246

NEW
247
            $childSerializerContext = $serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true];
×
NEW
248
            if (isset($serializerContext[AbstractNormalizer::ATTRIBUTES])) {
×
NEW
249
                $attributes = $serializerContext[AbstractNormalizer::ATTRIBUTES];
×
NEW
250
                if (\is_array($attributes) && \array_key_exists($normalizedPropertyName, $attributes) && \is_array($attributes[$normalizedPropertyName])) {
×
NEW
251
                    $childSerializerContext[AbstractNormalizer::ATTRIBUTES] = $attributes[$normalizedPropertyName];
×
252
                } else {
NEW
253
                    unset($childSerializerContext[AbstractNormalizer::ATTRIBUTES]);
×
254
                }
255
            }
256

257
            $subSchemaFactory = $this->schemaFactory ?: $this;
×
258
            $subSchema = $subSchemaFactory->buildSchema(
×
259
                $className,
×
260
                $format,
×
261
                $parentType,
×
262
                null,
×
263
                $subSchema,
×
NEW
264
                $childSerializerContext,
×
265
                false,
×
266
            );
×
267

268
            if (!isset($subSchema['$ref'])) {
×
269
                continue;
×
270
            }
271

272
            if ($isCollection) {
×
273
                $key = ($propertySchema['type'] ?? null) === 'object' ? 'additionalProperties' : 'items';
×
274
                $propertySchema[$key]['$ref'] = $subSchema['$ref'];
×
275
                unset($propertySchema[$key]['type']);
×
276
                break;
×
277
            }
278

279
            $refs[] = ['$ref' => $subSchema['$ref']];
×
280
            $isNullable = $isNullable ?? $type->isNullable();
×
281
        }
282

283
        if ($isNullable) {
×
284
            $refs[] = ['type' => 'null'];
×
285
        }
286

287
        $c = \count($refs);
×
288
        if ($c > 1) {
×
289
            $propertySchema['anyOf'] = $refs;
×
290
            unset($propertySchema['type']);
×
291
        } elseif (1 === $c) {
×
292
            $propertySchema['$ref'] = $refs[0]['$ref'];
×
293
            unset($propertySchema['type']);
×
294
        }
295

296
        $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
×
297
    }
298

299
    private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void
300
    {
301
        $version = $schema->getVersion();
166✔
302
        if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
166✔
303
            $additionalPropertySchema = $propertyMetadata->getOpenapiContext();
48✔
304
        } else {
305
            $additionalPropertySchema = $propertyMetadata->getJsonSchemaContext();
118✔
306
        }
307

308
        $propertySchema = array_merge(
166✔
309
            $propertyMetadata->getSchema() ?? [],
166✔
310
            $additionalPropertySchema ?? []
166✔
311
        );
166✔
312

313
        $extraProperties = $propertyMetadata->getExtraProperties();
166✔
314
        // see AttributePropertyMetadataFactory
315
        if (true === ($extraProperties[SchemaPropertyMetadataFactory::JSON_SCHEMA_USER_DEFINED] ?? false)) {
166✔
316
            // schema seems to have been declared by the user: do not override nor complete user value
317
            $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
58✔
318

319
            return;
58✔
320
        }
321

322
        $type = $propertyMetadata->getNativeType();
166✔
323

324
        // Type is defined in an allOf, anyOf, or oneOf
325
        $propertySchemaType = $this->getSchemaValue($propertySchema, 'type');
166✔
326
        $currentRef = $this->getSchemaValue($propertySchema, '$ref');
166✔
327
        $isSchemaDefined = null !== ($currentRef ?? $this->getSchemaValue($propertySchema, 'format') ?? $this->getSchemaValue($propertySchema, 'enum'));
166✔
328
        if (!$isSchemaDefined && Schema::UNKNOWN_TYPE !== $propertySchemaType) {
166✔
329
            $isSchemaDefined = true;
164✔
330
        }
331

332
        // Check if the type is considered "unknown" by SchemaPropertyMetadataFactory
333
        if (isset($propertySchema['additionalProperties']['type']) && Schema::UNKNOWN_TYPE === $propertySchema['additionalProperties']['type']) {
166✔
334
            $isSchemaDefined = false;
6✔
335
        }
336

337
        if ($isSchemaDefined && Schema::UNKNOWN_TYPE !== $propertySchemaType) {
166✔
338
            // If schema is defined and not marked as unknown, or if no type info exists, return early
339
            $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
166✔
340

341
            return;
166✔
342
        }
343

344
        if (Schema::UNKNOWN_TYPE === $propertySchemaType) {
94✔
345
            $propertySchema = [];
94✔
346
        }
347

348
        // property schema is created in SchemaPropertyMetadataFactory, but it cannot build resource reference ($ref)
349
        // complete property schema with resource reference ($ref) if it's related to an object/resource
350
        $refs = [];
94✔
351
        $isNullable = $type?->isNullable() ?? false;
94✔
352

353
        // TODO: refactor this with TypeInfo we shouldn't have to loop like this, the below code handles object refs
354
        if ($type) {
94✔
355
            foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) {
94✔
356
                if ($t instanceof BuiltinType && TypeIdentifier::NULL === $t->getTypeIdentifier()) {
94✔
357
                    continue;
52✔
358
                }
359

360
                $valueType = $t;
94✔
361
                $isCollection = $t instanceof CollectionType;
94✔
362

363
                if ($isCollection) {
94✔
364
                    $valueType = TypeHelper::getCollectionValueType($t);
66✔
365
                }
366

367
                if (!$valueType instanceof ObjectType && !$valueType instanceof GenericType) {
94✔
368
                    continue;
×
369
                }
370

371
                if ($valueType instanceof ObjectType) {
94✔
372
                    $className = $valueType->getClassName();
94✔
373
                } else {
374
                    // GenericType
375
                    $className = $valueType->getWrappedType()->getClassName();
×
376
                }
377

378
                $childSerializerContext = $serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true];
94✔
379
                if (isset($serializerContext[AbstractNormalizer::ATTRIBUTES])) {
94✔
NEW
380
                    $attributes = $serializerContext[AbstractNormalizer::ATTRIBUTES];
×
NEW
381
                    if (\is_array($attributes) && \array_key_exists($normalizedPropertyName, $attributes) && \is_array($attributes[$normalizedPropertyName])) {
×
NEW
382
                        $childSerializerContext[AbstractNormalizer::ATTRIBUTES] = $attributes[$normalizedPropertyName];
×
383
                    } else {
NEW
384
                        unset($childSerializerContext[AbstractNormalizer::ATTRIBUTES]);
×
385
                    }
386
                }
387

388
                $subSchemaInstance = new Schema($version);
94✔
389
                $subSchemaInstance->setDefinitions($schema->getDefinitions());
94✔
390
                $subSchemaFactory = $this->schemaFactory ?: $this;
94✔
391
                $subSchemaResult = $subSchemaFactory->buildSchema(
94✔
392
                    $className,
94✔
393
                    $format,
94✔
394
                    $parentType,
94✔
395
                    null,
94✔
396
                    $subSchemaInstance,
94✔
397
                    $childSerializerContext,
94✔
398
                    false,
94✔
399
                );
94✔
400
                if (!isset($subSchemaResult['$ref'])) {
94✔
401
                    continue;
×
402
                }
403

404
                if ($isCollection) {
94✔
405
                    $key = ($propertySchema['type'] ?? null) === 'object' ? 'additionalProperties' : 'items';
66✔
406
                    if (!isset($propertySchema['type'])) {
66✔
407
                        $propertySchema['type'] = 'array';
66✔
408
                    }
409

410
                    if (!isset($propertySchema[$key]) || !\is_array($propertySchema[$key])) {
66✔
411
                        $propertySchema[$key] = [];
66✔
412
                    }
413
                    $propertySchema[$key] = ['$ref' => $subSchemaResult['$ref']];
66✔
414
                    $refs = [];
66✔
415
                    break;
66✔
416
                }
417

418
                $refs[] = ['$ref' => $subSchemaResult['$ref']];
78✔
419
            }
420
        }
421

422
        if (!empty($refs)) {
94✔
423
            if ($isNullable) {
78✔
424
                $refs[] = ['type' => 'null'];
52✔
425
            }
426

427
            if (($c = \count($refs)) > 1) {
78✔
428
                $propertySchema = ['anyOf' => $refs];
54✔
429
            } elseif (1 === $c) {
62✔
430
                $propertySchema = ['$ref' => $refs[0]['$ref']];
62✔
431
            }
432
        }
433

434
        if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) && !isset($propertySchema['$ref'])) {
94✔
435
            $propertySchema['readOnly'] = true;
30✔
436
        }
437

438
        $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
94✔
439
    }
440

441
    private function getValidationGroups(Operation $operation): array
442
    {
443
        $groups = $operation->getValidationContext()['groups'] ?? [];
146✔
444

445
        return \is_array($groups) ? $groups : [$groups];
146✔
446
    }
447

448
    /**
449
     * Gets the options for the property name collection / property metadata factories.
450
     */
451
    private function getFactoryOptions(array $serializerContext, array $validationGroups, ?HttpOperation $operation = null): array
452
    {
453
        $options = [
166✔
454
            /* @see https://github.com/symfony/symfony/blob/v5.1.0/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php */
455
            'enable_getter_setter_extraction' => true,
166✔
456
        ];
166✔
457

458
        if (isset($serializerContext[AbstractNormalizer::GROUPS])) {
166✔
459
            /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */
460
            $options['serializer_groups'] = (array) $serializerContext[AbstractNormalizer::GROUPS];
72✔
461
        }
462

463
        if ($operation && ($normalizationGroups = $operation->getNormalizationContext()['groups'] ?? null)) {
166✔
464
            $options['normalization_groups'] = $normalizationGroups;
74✔
465
        }
466

467
        if ($operation && ($denormalizationGroups = $operation->getDenormalizationContext()['groups'] ?? null)) {
166✔
468
            $options['denormalization_groups'] = $denormalizationGroups;
42✔
469
        }
470

471
        if (isset($serializerContext[AbstractNormalizer::ATTRIBUTES])) {
166✔
NEW
472
            $options['serializer_attributes'] = (array) $serializerContext[AbstractNormalizer::ATTRIBUTES];
×
473
        }
474

475
        if ($operation && ($normalizationAttributes = $operation->getNormalizationContext()['attributes'] ?? null)) {
166✔
NEW
476
            $options['normalization_attributes'] = $normalizationAttributes;
×
477
        }
478

479
        if ($operation && ($denormalizationAttributes = $operation->getDenormalizationContext()['attributes'] ?? null)) {
166✔
NEW
480
            $options['denormalization_attributes'] = $denormalizationAttributes;
×
481
        }
482

483
        if ($validationGroups) {
166✔
484
            $options['validation_groups'] = $validationGroups;
×
485
        }
486

487
        if ($operation && ($ignoredAttributes = $operation->getNormalizationContext()['ignored_attributes'] ?? null)) {
166✔
488
            $options['ignored_attributes'] = $ignoredAttributes;
58✔
489
        }
490

491
        return $options;
166✔
492
    }
493

494
    public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
495
    {
496
        $this->schemaFactory = $schemaFactory;
821✔
497
    }
498

499
    private function getSchemaValue(array $schema, string $key): array|string|null
500
    {
501
        if (isset($schema['items'])) {
166✔
502
            $schema = $schema['items'];
102✔
503
        }
504

505
        return $schema[$key] ?? $schema['allOf'][0][$key] ?? $schema['anyOf'][0][$key] ?? $schema['oneOf'][0][$key] ?? null;
166✔
506
    }
507
}
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