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

api-platform / core / 14726067612

29 Apr 2025 07:47AM UTC coverage: 23.443% (+15.2%) from 8.252%
14726067612

push

github

web-flow
feat(symfony): Autoconfigure classes using `#[ApiResource]` attribute (#6943)

0 of 12 new or added lines in 4 files covered. (0.0%)

3578 existing lines in 159 files now uncovered.

11517 of 49127 relevant lines covered (23.44%)

54.29 hits per line

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

50.58
/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.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\Metadata\Property\Factory;
15

16
use ApiPlatform\JsonSchema\Schema;
17
use ApiPlatform\Metadata\ApiProperty;
18
use ApiPlatform\Metadata\Exception\PropertyNotFoundException;
19
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
20
use ApiPlatform\Metadata\ResourceClassResolverInterface;
21
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait;
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Ramsey\Uuid\UuidInterface;
24
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
25
use Symfony\Component\PropertyInfo\Type as LegacyType;
26
use Symfony\Component\TypeInfo\Type;
27
use Symfony\Component\TypeInfo\Type\BuiltinType;
28
use Symfony\Component\TypeInfo\Type\CollectionType;
29
use Symfony\Component\TypeInfo\Type\IntersectionType;
30
use Symfony\Component\TypeInfo\Type\ObjectType;
31
use Symfony\Component\TypeInfo\Type\UnionType;
32
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
33
use Symfony\Component\TypeInfo\TypeIdentifier;
34
use Symfony\Component\Uid\Ulid;
35
use Symfony\Component\Uid\Uuid;
36

37
/**
38
 * Build ApiProperty::schema.
39
 */
40
final class SchemaPropertyMetadataFactory implements PropertyMetadataFactoryInterface
41
{
42
    use ResourceClassInfoTrait;
43

44
    public const JSON_SCHEMA_USER_DEFINED = 'user_defined_schema';
45

46
    public function __construct(
47
        ResourceClassResolverInterface $resourceClassResolver,
48
        private readonly ?PropertyMetadataFactoryInterface $decorated = null,
49
    ) {
50
        $this->resourceClassResolver = $resourceClassResolver;
1,552✔
51
    }
52

53
    public function create(string $resourceClass, string $property, array $options = []): ApiProperty
54
    {
55
        if (null === $this->decorated) {
376✔
UNCOV
56
            $propertyMetadata = new ApiProperty();
×
57
        } else {
58
            try {
59
                $propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
376✔
UNCOV
60
            } catch (PropertyNotFoundException) {
×
61
                $propertyMetadata = new ApiProperty();
×
62
            }
63
        }
64

65
        $extraProperties = $propertyMetadata->getExtraProperties() ?? [];
376✔
66
        // see AttributePropertyMetadataFactory
67
        if (true === ($extraProperties[self::JSON_SCHEMA_USER_DEFINED] ?? false)) {
376✔
68
            // schema seems to have been declared by the user: do not override nor complete user value
69
            return $propertyMetadata;
17✔
70
        }
71

72
        $link = (($options['schema_type'] ?? null) === Schema::TYPE_INPUT) ? $propertyMetadata->isWritableLink() : $propertyMetadata->isReadableLink();
376✔
73
        $propertySchema = $propertyMetadata->getSchema() ?? [];
376✔
74

75
        if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable())) {
376✔
76
            $propertySchema['readOnly'] = true;
262✔
77
        }
78

79
        if (!\array_key_exists('writeOnly', $propertySchema) && false === $propertyMetadata->isReadable()) {
376✔
80
            $propertySchema['writeOnly'] = true;
82✔
81
        }
82

83
        if (!\array_key_exists('description', $propertySchema) && null !== ($description = $propertyMetadata->getDescription())) {
376✔
84
            $propertySchema['description'] = $description;
140✔
85
        }
86

87
        // see https://github.com/json-schema-org/json-schema-spec/pull/737
88
        if (!\array_key_exists('deprecated', $propertySchema) && null !== $propertyMetadata->getDeprecationReason()) {
376✔
89
            $propertySchema['deprecated'] = true;
23✔
90
        }
91

92
        // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it
93
        // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4
94
        if (!\array_key_exists('externalDocs', $propertySchema) && null !== ($iri = $propertyMetadata->getTypes()[0] ?? null)) {
376✔
95
            $propertySchema['externalDocs'] = ['url' => $iri];
50✔
96
        }
97

98
        if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
376✔
UNCOV
99
            return $propertyMetadata->withSchema($this->getLegacyTypeSchema($propertyMetadata, $propertySchema, $resourceClass, $property, $link));
×
100
        }
101

102
        return $propertyMetadata->withSchema($this->getTypeSchema($propertyMetadata, $propertySchema, $link));
376✔
103
    }
104

105
    private function getTypeSchema(ApiProperty $propertyMetadata, array $propertySchema, ?bool $link): array
106
    {
107
        $type = $propertyMetadata->getNativeType();
376✔
108

109
        $typeIsResourceClass = function (Type $type) use (&$className): bool {
376✔
110
            return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
66✔
111
        };
376✔
112

113
        if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && !$type?->isSatisfiedBy($typeIsResourceClass)) {
376✔
114
            if ($default instanceof \BackedEnum) {
64✔
115
                $default = $default->value;
10✔
116
            }
117
            $propertySchema['default'] = $default;
64✔
118
        }
119

120
        if (!\array_key_exists('example', $propertySchema) && !empty($example = $propertyMetadata->getExample())) {
376✔
121
            $propertySchema['example'] = $example;
6✔
122
        }
123

124
        if (!\array_key_exists('example', $propertySchema) && \array_key_exists('default', $propertySchema)) {
376✔
125
            $propertySchema['example'] = $propertySchema['default'];
64✔
126
        }
127

128
        // never override the following keys if at least one is already set or if there's a custom openapi context
129
        if (
130
            null === $type
376✔
131
            || ($propertySchema['type'] ?? $propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false)
376✔
132
            || \array_key_exists('type', $propertyMetadata->getOpenapiContext() ?? [])
376✔
133
        ) {
134
            return $propertySchema;
53✔
135
        }
136

137
        if ($type instanceof CollectionType && null !== $propertyMetadata->getUriTemplate()) {
343✔
138
            $type = $type->getCollectionValueType();
10✔
139
        }
140

141
        return $propertySchema + $this->getJsonSchemaFromType($type, $link);
343✔
142
    }
143

144
    /**
145
     * Applies nullability rules to a generated JSON schema based on the original type's nullability.
146
     *
147
     * @param array<string, mixed> $schema     the base JSON schema generated for the non-null type
148
     * @param bool                 $isNullable whether the original type allows null
149
     *
150
     * @return array<string, mixed> the JSON schema with nullability applied
151
     */
152
    private function applyNullability(array $schema, bool $isNullable): array
153
    {
154
        if (!$isNullable) {
343✔
155
            return $schema;
343✔
156
        }
157

158
        if (isset($schema['type']) && 'null' === $schema['type'] && 1 === \count($schema)) {
198✔
159
            return $schema;
28✔
160
        }
161

162
        if (isset($schema['anyOf']) && \is_array($schema['anyOf'])) {
194✔
163
            $hasNull = false;
7✔
164
            foreach ($schema['anyOf'] as $anyOfSchema) {
7✔
165
                if (isset($anyOfSchema['type']) && 'null' === $anyOfSchema['type']) {
7✔
UNCOV
166
                    $hasNull = true;
×
UNCOV
167
                    break;
×
168
                }
169
            }
170
            if (!$hasNull) {
7✔
171
                $schema['anyOf'][] = ['type' => 'null'];
7✔
172
            }
173

174
            return $schema;
7✔
175
        }
176

177
        if (isset($schema['type'])) {
194✔
178
            $currentType = $schema['type'];
194✔
179
            $schema['type'] = \is_array($currentType) ? array_merge($currentType, ['null']) : [$currentType, 'null'];
194✔
180

181
            if (isset($schema['enum'])) {
194✔
182
                $schema['enum'][] = null;
10✔
183

184
                return $schema;
10✔
185
            }
186

187
            return $schema;
192✔
188
        }
189

UNCOV
190
        return ['anyOf' => [$schema, ['type' => 'null']]];
×
191
    }
192

193
    /**
194
     * Converts a TypeInfo Type into a JSON Schema definition array.
195
     *
196
     * @return array<string, mixed>
197
     */
198
    private function getJsonSchemaFromType(Type $type, ?bool $readableLink = null): array
199
    {
200
        $isNullable = $type->isNullable();
343✔
201

202
        if ($type instanceof UnionType) {
343✔
203
            $subTypes = array_filter($type->getTypes(), fn ($t) => !($t instanceof BuiltinType && $t->isIdentifiedBy(TypeIdentifier::NULL)));
211✔
204

205
            foreach ($subTypes as $t) {
211✔
206
                $s = $this->getJsonSchemaFromType($t, $readableLink);
211✔
207
                // We can not find what type this is, let it be computed at runtime by the SchemaFactory
208
                if (($s['type'] ?? null) === Schema::UNKNOWN_TYPE) {
211✔
209
                    return $s;
51✔
210
                }
211
            }
212

213
            $schemas = array_map(fn ($t) => $this->getJsonSchemaFromType($t, $readableLink), $subTypes);
194✔
214

215
            if (0 === \count($schemas)) {
194✔
UNCOV
216
                $schema = [];
×
217
            } elseif (1 === \count($schemas)) {
194✔
218
                $schema = current($schemas);
194✔
219
            } else {
220
                $schema = ['anyOf' => $schemas];
9✔
221
            }
222

223
            return $this->applyNullability($schema, $isNullable);
194✔
224
        }
225

226
        if ($type instanceof IntersectionType) {
343✔
227
            $schemas = [];
6✔
228
            foreach ($type->getTypes() as $t) {
6✔
229
                while ($t instanceof WrappingTypeInterface) {
6✔
UNCOV
230
                    $t = $t->getWrappedType();
×
231
                }
232

233
                $subSchema = $this->getJsonSchemaFromType($t, $readableLink);
6✔
234
                if (!empty($subSchema)) {
6✔
235
                    $schemas[] = $subSchema;
6✔
236
                }
237
            }
238

239
            return $this->applyNullability(['allOf' => $schemas], $isNullable);
6✔
240
        }
241

242
        if ($type instanceof CollectionType) {
343✔
243
            $valueType = $type->getCollectionValueType();
98✔
244
            $valueSchema = $this->getJsonSchemaFromType($valueType, $readableLink);
98✔
245
            $keyType = $type->getCollectionKeyType();
98✔
246

247
            // Associative array (string keys)
248
            if ($keyType->isSatisfiedBy(fn (Type $t) => $t instanceof BuiltinType && $t->isIdentifiedBy(TypeIdentifier::INT))) {
98✔
249
                $schema = [
97✔
250
                    'type' => 'array',
97✔
251
                    'items' => $valueSchema,
97✔
252
                ];
97✔
253
            } else { // List (int keys)
254
                $schema = [
11✔
255
                    'type' => 'object',
11✔
256
                    'additionalProperties' => $valueSchema,
11✔
257
                ];
11✔
258
            }
259

260
            return $this->applyNullability($schema, $isNullable);
98✔
261
        }
262

263
        if ($type instanceof ObjectType) {
343✔
264
            $schema = $this->getClassSchemaDefinition($type->getClassName(), $readableLink);
185✔
265

266
            return $this->applyNullability($schema, $isNullable);
185✔
267
        }
268

269
        if ($type instanceof BuiltinType) {
331✔
270
            $schema = match ($type->getTypeIdentifier()) {
331✔
271
                TypeIdentifier::INT => ['type' => 'integer'],
331✔
272
                TypeIdentifier::FLOAT => ['type' => 'number'],
294✔
273
                TypeIdentifier::BOOL => ['type' => 'boolean'],
294✔
274
                TypeIdentifier::TRUE => ['type' => 'boolean', 'const' => true],
285✔
275
                TypeIdentifier::FALSE => ['type' => 'boolean', 'const' => false],
285✔
276
                TypeIdentifier::STRING => ['type' => 'string'],
285✔
277
                TypeIdentifier::ARRAY => ['type' => 'array', 'items' => []],
28✔
278
                TypeIdentifier::ITERABLE => ['type' => 'array', 'items' => []],
28✔
279
                TypeIdentifier::OBJECT => ['type' => 'object'],
28✔
280
                TypeIdentifier::RESOURCE => ['type' => 'string'],
28✔
281
                TypeIdentifier::CALLABLE => ['type' => 'string'],
28✔
282
                default => ['type' => 'null'],
28✔
283
            };
331✔
284

285
            return $this->applyNullability($schema, $isNullable);
331✔
286
        }
287

UNCOV
288
        return ['type' => Schema::UNKNOWN_TYPE];
×
289
    }
290

291
    /**
292
     * Gets the JSON Schema definition for a class.
293
     */
294
    private function getClassSchemaDefinition(?string $className, ?bool $readableLink): array
295
    {
296
        if (null === $className) {
185✔
UNCOV
297
            return ['type' => 'string'];
×
298
        }
299

300
        if (is_a($className, \DateTimeInterface::class, true)) {
185✔
301
            return ['type' => 'string', 'format' => 'date-time'];
51✔
302
        }
303

304
        if (is_a($className, \DateInterval::class, true)) {
176✔
UNCOV
305
            return ['type' => 'string', 'format' => 'duration'];
×
306
        }
307

308
        if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) {
176✔
309
            return ['type' => 'string', 'format' => 'uuid'];
7✔
310
        }
311

312
        if (is_a($className, Ulid::class, true)) {
176✔
UNCOV
313
            return ['type' => 'string', 'format' => 'ulid'];
×
314
        }
315

316
        if (is_a($className, \SplFileInfo::class, true)) {
176✔
UNCOV
317
            return ['type' => 'string', 'format' => 'binary'];
×
318
        }
319

320
        $isResourceClass = $this->isResourceClass($className);
176✔
321
        if (!$isResourceClass && is_a($className, \BackedEnum::class, true)) {
176✔
322
            $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases());
10✔
323
            $type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer';
10✔
324

325
            return ['type' => $type, 'enum' => $enumCases];
10✔
326
        }
327

328
        // If it's a resource and links are not readable, represent as IRI string.
329
        if ($isResourceClass && true !== $readableLink) {
176✔
330
            return [
122✔
331
                'type' => 'string',
122✔
332
                'format' => 'iri-reference',
122✔
333
                'example' => 'https://example.com/', // Add a generic example
122✔
334
            ];
122✔
335
        }
336

337
        return ['type' => Schema::UNKNOWN_TYPE];
95✔
338
    }
339

340
    private function getLegacyTypeSchema(ApiProperty $propertyMetadata, array $propertySchema, string $resourceClass, string $property, ?bool $link): array
341
    {
UNCOV
342
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
×
343

UNCOV
344
        if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && (!\count($types) || null === ($className = $types[0]->getClassName()) || !$this->isResourceClass($className))) {
×
UNCOV
345
            if ($default instanceof \BackedEnum) {
×
UNCOV
346
                $default = $default->value;
×
347
            }
348
            $propertySchema['default'] = $default;
×
349
        }
350

351
        if (!\array_key_exists('example', $propertySchema) && !empty($example = $propertyMetadata->getExample())) {
×
352
            $propertySchema['example'] = $example;
×
353
        }
354

UNCOV
355
        if (!\array_key_exists('example', $propertySchema) && \array_key_exists('default', $propertySchema)) {
×
UNCOV
356
            $propertySchema['example'] = $propertySchema['default'];
×
357
        }
358

359
        // never override the following keys if at least one is already set or if there's a custom openapi context
360
        if (
361
            [] === $types
×
362
            || ($propertySchema['type'] ?? $propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false)
×
UNCOV
363
            || \array_key_exists('type', $propertyMetadata->getOpenapiContext() ?? [])
×
364
        ) {
UNCOV
365
            return $propertySchema;
×
366
        }
367

368
        $valueSchema = [];
×
369
        foreach ($types as $type) {
×
370
            // Temp fix for https://github.com/symfony/symfony/pull/52699
371
            if (ArrayCollection::class === $type->getClassName()) {
×
UNCOV
372
                $type = new LegacyType($type->getBuiltinType(), $type->isNullable(), $type->getClassName(), true, $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
×
373
            }
374

375
            if ($isCollection = $type->isCollection()) {
×
UNCOV
376
                $keyType = $type->getCollectionKeyTypes()[0] ?? null;
×
377
                $valueType = $type->getCollectionValueTypes()[0] ?? null;
×
378
            } else {
UNCOV
379
                $keyType = null;
×
UNCOV
380
                $valueType = $type;
×
381
            }
382

383
            if (null === $valueType) {
×
UNCOV
384
                $builtinType = 'string';
×
385
                $className = null;
×
386
            } else {
UNCOV
387
                $builtinType = $valueType->getBuiltinType();
×
UNCOV
388
                $className = $valueType->getClassName();
×
389
            }
390

391
            if ($isCollection && null !== $propertyMetadata->getUriTemplate()) {
×
UNCOV
392
                $keyType = null;
×
393
                $isCollection = false;
×
394
            }
395

UNCOV
396
            $propertyType = $this->getLegacyType(new LegacyType($builtinType, $type->isNullable(), $className, $isCollection, $keyType, $valueType), $link);
×
397
            if (!\in_array($propertyType, $valueSchema, true)) {
×
398
                $valueSchema[] = $propertyType;
×
399
            }
400
        }
401

402
        if (1 === \count($valueSchema)) {
×
403
            return $propertySchema + $valueSchema[0];
×
404
        }
405

406
        // multiple builtInTypes detected: determine oneOf/allOf if union vs intersect types
407
        try {
408
            $reflectionClass = new \ReflectionClass($resourceClass);
×
409
            $reflectionProperty = $reflectionClass->getProperty($property);
×
UNCOV
410
            $composition = $reflectionProperty->getType() instanceof \ReflectionUnionType ? 'oneOf' : 'allOf';
×
UNCOV
411
        } catch (\ReflectionException) {
×
412
            // cannot detect types
UNCOV
413
            $composition = 'anyOf';
×
414
        }
415

416
        return $propertySchema + [$composition => $valueSchema];
×
417
    }
418

419
    private function getLegacyType(LegacyType $type, ?bool $readableLink = null): array
420
    {
UNCOV
421
        if (!$type->isCollection()) {
×
422
            return $this->addNullabilityToTypeDefinition($this->legacyTypeToArray($type, $readableLink), $type);
×
423
        }
424

UNCOV
425
        $keyType = $type->getCollectionKeyTypes()[0] ?? null;
×
UNCOV
426
        $subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new LegacyType($type->getBuiltinType(), false, $type->getClassName(), false);
×
427

428
        if (null !== $keyType && LegacyType::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) {
×
UNCOV
429
            return $this->addNullabilityToTypeDefinition([
×
UNCOV
430
                'type' => 'object',
×
431
                'additionalProperties' => $this->getLegacyType($subType, $readableLink),
×
432
            ], $type);
×
433
        }
434

435
        return $this->addNullabilityToTypeDefinition([
×
436
            'type' => 'array',
×
437
            'items' => $this->getLegacyType($subType, $readableLink),
×
438
        ], $type);
×
439
    }
440

441
    private function legacyTypeToArray(LegacyType $type, ?bool $readableLink = null): array
442
    {
443
        return match ($type->getBuiltinType()) {
×
444
            LegacyType::BUILTIN_TYPE_INT => ['type' => 'integer'],
×
UNCOV
445
            LegacyType::BUILTIN_TYPE_FLOAT => ['type' => 'number'],
×
UNCOV
446
            LegacyType::BUILTIN_TYPE_BOOL => ['type' => 'boolean'],
×
UNCOV
447
            LegacyType::BUILTIN_TYPE_OBJECT => $this->getLegacyClassType($type->getClassName(), $type->isNullable(), $readableLink),
×
UNCOV
448
            default => ['type' => 'string'],
×
449
        };
×
450
    }
451

452
    /**
453
     * Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided.
454
     *
455
     * Note: if the class is not part of exceptions listed above, any class is considered as a resource.
456
     *
457
     * @throws PropertyNotFoundException
458
     */
459
    private function getLegacyClassType(?string $className, bool $nullable, ?bool $readableLink): array
460
    {
UNCOV
461
        if (null === $className) {
×
UNCOV
462
            return ['type' => 'string'];
×
463
        }
464

UNCOV
465
        if (is_a($className, \DateTimeInterface::class, true)) {
×
UNCOV
466
            return [
×
467
                'type' => 'string',
×
468
                'format' => 'date-time',
×
UNCOV
469
            ];
×
470
        }
471

472
        if (is_a($className, \DateInterval::class, true)) {
×
473
            return [
×
474
                'type' => 'string',
×
475
                'format' => 'duration',
×
UNCOV
476
            ];
×
477
        }
478

479
        if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) {
×
480
            return [
×
481
                'type' => 'string',
×
482
                'format' => 'uuid',
×
UNCOV
483
            ];
×
484
        }
485

486
        if (is_a($className, Ulid::class, true)) {
×
487
            return [
×
488
                'type' => 'string',
×
489
                'format' => 'ulid',
×
UNCOV
490
            ];
×
491
        }
492

493
        if (is_a($className, \SplFileInfo::class, true)) {
×
494
            return [
×
495
                'type' => 'string',
×
496
                'format' => 'binary',
×
UNCOV
497
            ];
×
498
        }
499

500
        $isResourceClass = $this->isResourceClass($className);
×
501
        if (!$isResourceClass && is_a($className, \BackedEnum::class, true)) {
×
502
            $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases());
×
503

UNCOV
504
            $type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer';
×
505

506
            if ($nullable) {
×
507
                $enumCases[] = null;
×
508
            }
509

510
            return [
×
UNCOV
511
                'type' => $type,
×
512
                'enum' => $enumCases,
×
513
            ];
×
514
        }
515

516
        if (true !== $readableLink && $isResourceClass) {
×
517
            return [
×
518
                'type' => 'string',
×
519
                'format' => 'iri-reference',
×
UNCOV
520
                'example' => 'https://example.com/',
×
UNCOV
521
            ];
×
522
        }
523

524
        return ['type' => Schema::UNKNOWN_TYPE];
×
525
    }
526

527
    /**
528
     * @param array<string, mixed> $jsonSchema
529
     *
530
     * @return array<string, mixed>
531
     */
532
    private function addNullabilityToTypeDefinition(array $jsonSchema, LegacyType $type): array
533
    {
UNCOV
534
        if (!$type->isNullable()) {
×
UNCOV
535
            return $jsonSchema;
×
536
        }
537

UNCOV
538
        if (\array_key_exists('$ref', $jsonSchema)) {
×
UNCOV
539
            return ['anyOf' => [$jsonSchema, ['type' => 'null']]];
×
540
        }
541

UNCOV
542
        return [...$jsonSchema, ...[
×
UNCOV
543
            'type' => \is_array($jsonSchema['type'])
×
544
                ? array_merge($jsonSchema['type'], ['null'])
×
545
                : [$jsonSchema['type'], 'null'],
×
UNCOV
546
        ]];
×
547
    }
548
}
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