Skip to content
Merged
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
292 changes: 212 additions & 80 deletions README.md

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion context.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,3 @@ documents:
- type: file
sourcePaths:
- src
- tests/Unit
18 changes: 18 additions & 0 deletions src/Attribute/AdditionalProperties.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Spiral\JsonSchemaGenerator\Attribute;

#[\Attribute(\Attribute::TARGET_PROPERTY)]
final readonly class AdditionalProperties
{
/**
* @param string $valueType The type of values for additional properties (e.g., 'string', 'int', 'number', 'boolean', 'mixed')
* @param class-string|null $valueClass Optional class reference for object-typed additional properties
*/
public function __construct(
public string $valueType,
public ?string $valueClass = null,
) {}
}
19 changes: 16 additions & 3 deletions src/Schema/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@ public function jsonSerialize(): array

// Check if we have an array shape constraint that should override the type
if (isset($this->validationRules['type']) && $this->validationRules['type'] === 'object') {
// Array shape overrides normal type processing
// Array shape or additional properties overrides normal type processing
$property = \array_merge($property, $this->validationRules);

// Clean up internal metadata keys
unset($property['_additionalPropertiesClass']);

return $property;
}

Expand All @@ -61,7 +65,7 @@ public function jsonSerialize(): array
// Apply validation rules from PHPDoc constraints (except type overrides)
$filteredValidationRules = $this->validationRules;
if (isset($filteredValidationRules['type'])) {
unset($filteredValidationRules['type'], $filteredValidationRules['properties'], $filteredValidationRules['required'], $filteredValidationRules['additionalProperties']);
unset($filteredValidationRules['type'], $filteredValidationRules['properties'], $filteredValidationRules['required'], $filteredValidationRules['additionalProperties'], $filteredValidationRules['_additionalPropertiesClass']);
}
$property = \array_merge($property, $filteredValidationRules);

Expand All @@ -84,6 +88,11 @@ public function getDependencies(): array
}
}

// Extract dependencies from additional properties references
if (isset($this->validationRules['_additionalPropertiesClass'])) {
$dependencies[] = $this->validationRules['_additionalPropertiesClass'];
}

return $dependencies;
}

Expand All @@ -107,7 +116,11 @@ protected function propertyTypeToDefinition(PropertyType $propertyType): array
}
$property['items']['anyOf'][] = $schemaType;
} else {
$property['items']['anyOf'][] = ['$ref' => (new Reference($collectionType->type))->jsonSerialize()];
$property['items']['anyOf'][] = [
'$ref' => (new Reference(
$collectionType->type,
))->jsonSerialize(),
];
}
}
} elseif ($collectionTypeCount === 1) {
Expand Down
78 changes: 78 additions & 0 deletions src/Validation/AdditionalPropertiesExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace Spiral\JsonSchemaGenerator\Validation;

use Spiral\JsonSchemaGenerator\Attribute\AdditionalProperties;
use Spiral\JsonSchemaGenerator\Parser\PropertyInterface;
use Spiral\JsonSchemaGenerator\Schema\Reference;
use Spiral\JsonSchemaGenerator\Schema\Type;

final readonly class AdditionalPropertiesExtractor implements PropertyDataExtractorInterface
{
public function extractValidationRules(PropertyInterface $property, Type $jsonSchemaType): array
{
$validationRules = [];

// Only process array types for additional properties
if ($jsonSchemaType !== Type::Array) {
return $validationRules;
}

$additionalProperties = $property->findAttribute(AdditionalProperties::class);
if (!$additionalProperties instanceof AdditionalProperties) {
return $validationRules;
}

// Override array type to object type for additional properties
$validationRules['type'] = 'object';

// Process the additional properties value type
$additionalPropertiesSchema = $this->processValueType(
$additionalProperties->valueType,
$additionalProperties->valueClass,
);

$validationRules['additionalProperties'] = $additionalPropertiesSchema;

// Store class dependencies for later extraction
if ($additionalProperties->valueType === 'object' && $additionalProperties->valueClass !== null) {
$validationRules['_additionalPropertiesClass'] = $additionalProperties->valueClass;
}

return $validationRules;
}

/**
* Process the value type and return appropriate JSON schema structure.
*
* @param class-string|null $valueClass
*/
private function processValueType(string $valueType, ?string $valueClass): array|bool
{
return match ($valueType) {
'int', 'integer' => ['type' => 'integer'],
'number', 'float' => ['type' => 'number'],
'boolean', 'bool' => ['type' => 'boolean'],
'mixed' => true, // Allow any type
'object' => $this->processObjectType($valueClass),
default => ['type' => 'string'], // fallback to string
};
}

/**
* Process object type with class reference.
*
* @param class-string|null $valueClass
*/
private function processObjectType(?string $valueClass): array
{
if ($valueClass === null) {
return ['type' => 'object'];
}

// Create reference to the class definition
return ['$ref' => (new Reference($valueClass))->jsonSerialize()];
}
}
1 change: 1 addition & 0 deletions src/Validation/CompositePropertyDataExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static function createDefault(): self
return new self([
new PhpDocValidationConstraintExtractor(),
new AttributeConstraintExtractor(),
new AdditionalPropertiesExtractor(), // Add the new extractor
]);
}

Expand Down
Loading
Loading