Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/Utils/SchemaGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,14 @@ private function buildParameterSchema(array $paramInfo, ?array $methodLevelParam
$parameterLevelSchema = $paramInfo['parameter_schema'];
if (!empty($parameterLevelSchema)) {
if (isset($parameterLevelSchema['definition']) && is_array($parameterLevelSchema['definition'])) {
return $parameterLevelSchema['definition'];
$definitionSchema = $parameterLevelSchema['definition'];

// Preserve PHP default values even when using a full definition
if ($paramInfo['has_default'] && !array_key_exists('default', $definitionSchema)) {
$definitionSchema['default'] = $paramInfo['default_value'];
}

return $definitionSchema;
}

$mergedSchema = $this->mergeSchemas($mergedSchema, $parameterLevelSchema);
Expand All @@ -203,6 +210,12 @@ private function mergeSchemas(array $recessiveSchema, array $dominantSchema): ar
{
$mergedSchema = array_merge($recessiveSchema, $dominantSchema);

$dominantHasPolymorphic = isset($dominantSchema['anyOf']) || isset($dominantSchema['oneOf']) || isset($dominantSchema['allOf']);
if ($dominantHasPolymorphic && !isset($dominantSchema['type'])) {
// Remove inferred/recessive type to avoid conflicts with polymorphic schemas
unset($mergedSchema['type']);
}

return $mergedSchema;
}

Expand Down
33 changes: 33 additions & 0 deletions tests/Fixtures/Utils/SchemaGeneratorFixture.php
Original file line number Diff line number Diff line change
Expand Up @@ -410,4 +410,37 @@ public function parameterWithRawDefinition(
string $custom
): void {
}

/**
* Parameter with anyOf in definition; default comes from PHP signature.
* This tests the fix for schema merge conflicts where inferred "type": "array"
* from PHP type hint would conflict with anyOf allowing object notation.
*/
public function parameterWithAnyOfDefinition(
#[Schema(definition: [
'anyOf' => [
['type' => 'object', 'additionalProperties' => true],
['type' => 'array', 'items' => ['type' => 'object']]
],
'description' => 'Properties as {"key":"value"} object or array format',
])]
array|stdClass $properties = []
): void {
}

/**
* Method-level schema with anyOf property and array type hint to ensure inferred type is dropped.
*/
#[Schema(properties: [
'payload' => [
'anyOf' => [
['type' => 'object', 'additionalProperties' => true],
['type' => 'array', 'items' => ['type' => 'object']]
],
'description' => 'Payload as object or array of objects'
]
])]
public function methodLevelAnyOfProperty(array $payload): void
{
}
}
35 changes: 35 additions & 0 deletions tests/Integration/SchemaGenerationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,38 @@

expect($schema['required'])->toEqual(['custom']);
});

it('handles anyOf schema without type conflict and preserves default from signature', function () {
$method = new ReflectionMethod(SchemaGeneratorFixture::class, 'parameterWithAnyOfDefinition');
$schema = $this->schemaGenerator->generate($method);

// The key test: anyOf should be present without conflicting "type": "array"
expect($schema['properties']['properties'])->toHaveKey('anyOf');
expect($schema['properties']['properties'])->not->toHaveKey('type');

expect($schema['properties']['properties'])->toEqual([
'anyOf' => [
['type' => 'object', 'additionalProperties' => true],
['type' => 'array', 'items' => ['type' => 'object']]
],
'description' => 'Properties as {"key":"value"} object or array format',
'default' => []
]);
});

it('drops inferred type when method-level schema uses anyOf without type', function () {
$method = new ReflectionMethod(SchemaGeneratorFixture::class, 'methodLevelAnyOfProperty');
$schema = $this->schemaGenerator->generate($method);

expect($schema['properties']['payload'])->toHaveKey('anyOf');
expect($schema['properties']['payload'])->not->toHaveKey('type');
expect($schema['properties']['payload'])->toEqual([
'anyOf' => [
['type' => 'object', 'additionalProperties' => true],
['type' => 'array', 'items' => ['type' => 'object']]
],
'description' => 'Payload as object or array of objects'
]);

expect($schema['required'])->toContain('payload');
});