diff --git a/src/Utils/SchemaGenerator.php b/src/Utils/SchemaGenerator.php index 874dbdd..d21989a 100644 --- a/src/Utils/SchemaGenerator.php +++ b/src/Utils/SchemaGenerator.php @@ -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); @@ -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; } diff --git a/tests/Fixtures/Utils/SchemaGeneratorFixture.php b/tests/Fixtures/Utils/SchemaGeneratorFixture.php index db8373b..cc2909e 100644 --- a/tests/Fixtures/Utils/SchemaGeneratorFixture.php +++ b/tests/Fixtures/Utils/SchemaGeneratorFixture.php @@ -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 + { + } } diff --git a/tests/Integration/SchemaGenerationTest.php b/tests/Integration/SchemaGenerationTest.php index 9cf7215..da4563f 100644 --- a/tests/Integration/SchemaGenerationTest.php +++ b/tests/Integration/SchemaGenerationTest.php @@ -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'); +});