Skip to content

Conversation

@butschster
Copy link
Member

@butschster butschster commented Aug 21, 2025

This PR introduces support for defining additional properties in JSON schemas generated from PHP DTOs.

Features Added

  • AdditionalProperties attribute for defining dynamic object schemas
  • AdditionalPropertiesExtractor to process the attribute during schema generation
  • Support for different value types: primitive types, object references, and mixed values
  • Integration with the existing property data extractor system

Example Usage

#[Field(title: 'Settings')]
#[AdditionalProperties(valueType: 'string')]
public readonly array $settings = [];

#[Field(title: 'Metadata')]
#[AdditionalProperties(valueType: 'object', valueClass: ValueObject::class)]
public readonly array $metadata = [];
# AdditionalProperties Support - Code Examples from Unit Tests

## Basic Usage Examples

### Simple String Properties

```php
final readonly class ConfigurableObject
{
    public function __construct(
        public string $name,
        public int $version,
        #[AdditionalProperties(valueType: 'string')]
        #[Field(description: "String-based configuration options")]
        public array $stringOptions = [],
    ) {}
}

Generated Schema:

{
    "description": "String-based configuration options",
    "type": "object",
    "additionalProperties": {"type": "string"},
    "default": []
}

Numeric Properties

final readonly class ConfigurableObject
{
    public function __construct(
        // ... other properties
        #[AdditionalProperties(valueType: 'int')]
        #[Field(description: "Numeric configuration values")]
        public array $numericSettings = [],
    ) {}
}

Generated Schema:

{
    "description": "Numeric configuration values",
    "type": "object",
    "additionalProperties": {"type": "integer"},
    "default": []
}

Mixed Type Properties (Any Value)

final readonly class ApiResponse
{
    public function __construct(
        public bool $success,
        public string $message,
        public int $statusCode,
        #[AdditionalProperties(valueType: 'mixed')]
        #[Field(description: "Dynamic response data")]
        public array $data = [],
    ) {}
}

Generated Schema:

{
    "description": "Dynamic response data",
    "type": "object",
    "additionalProperties": true,
    "default": []
}

Object References

final readonly class ServiceConfiguration
{
    public function __construct(
        public string $serviceName,
        public string $version,
        #[AdditionalProperties(valueType: 'object', valueClass: ServiceEndpoint::class)]
        #[Field(description: "Service endpoints configuration")]
        public array $endpoints = [],
    ) {}
}

final readonly class ServiceEndpoint
{
    public function __construct(
        public string $url,
        public int $timeoutMs,
        public int $maxRetries,
        public bool $enabled,
    ) {}
}

Generated Schema:

{
    "description": "Service endpoints configuration",
    "type": "object",
    "additionalProperties": {"$ref": "#/definitions/ServiceEndpoint"},
    "default": []
}

Real-World Examples

Database Configuration

final readonly class DatabaseConfig
{
    public function __construct(
        #[Field(description: "Database host")]
        public string $host,
        #[Field(description: "Database port")]
        public int $port,
        public string $username,
        public string $password,
        #[AdditionalProperties(valueType: 'string')]
        #[Field(description: "Driver-specific configuration options")]
        public array $driverOptions = [],
    ) {}
}

User Preferences

final readonly class User
{
    public function __construct(
        public int $id,
        public string $email,
        public string $name,
        #[AdditionalProperties(valueType: 'mixed')]
        #[Field(description: "Custom user attributes and metadata")]
        public array $attributes = [],
        #[AdditionalProperties(valueType: 'string')]
        #[Field(description: "User preferences")]
        public array $preferences = [],
    ) {}
}

Localization Support

final readonly class LocalizableContent
{
    public function __construct(
        public string $contentId,
        public string $contentType,
        #[AdditionalProperties(valueType: 'string')]
        #[Field(description: "Translations for different locales")]
        public array $translations = [],
        #[AdditionalProperties(valueType: 'object', valueClass: LocaleMetadata::class)]
        #[Field(description: "Locale-specific metadata")]
        public array $localeMetadata = [],
    ) {}
}

final readonly class LocaleMetadata
{
    public function __construct(
        public string $author,
        public string $lastModified,
        public string $reviewStatus,
    ) {}
}

E-commerce Product

final readonly class Product
{
    public function __construct(
        public string $id,
        public string $name,
        #[Range(min: 0)]
        public float $price,
        #[AdditionalProperties(valueType: 'mixed')]
        #[Field(description: "Product-specific attributes that vary by category")]
        public array $attributes = [],
        #[AdditionalProperties(valueType: 'string')]
        #[Field(description: "SEO and marketing metadata")]
        public array $metadata = [],
    ) {}
}

- Implement AdditionalPropertiesExtractor to handle dynamic object properties
- Add AdditionalProperties attribute for defining property value types
- Update documentation with examples and usage information
- Include unit tests for the new functionality

fixes #10
@butschster butschster requested a review from roxblnfk August 21, 2025 14:32
@butschster butschster self-assigned this Aug 21, 2025
@butschster butschster added the enhancement New feature or request label Aug 21, 2025
@butschster butschster linked an issue Aug 21, 2025 that may be closed by this pull request
@butschster
Copy link
Member Author

Hey @tsantos84

Could you check?

@tsantos84
Copy link

tsantos84 commented Aug 21, 2025

Hi @butschster,

Why not leverage the #[Field] attribute and add the allowAdditionalProperties option to it?

@butschster
Copy link
Member Author

Hi @butschster,

Why not leverage the #[Field] attribute and add the allowAdditionalProperties option to it?

Each attribute should have a clear, focused purpose. The #[Field] attribute is specifically for describing field metadata like title, description, and format. The #[AdditionalProperties] attribute has a different responsibility - defining the structure of dynamic properties.

AdditionalProperties is a specific JSON Schema concept that changes the fundamental nature of how an array is interpreted (as an object with dynamic keys). It's not just another field option - it transforms the field's semantic meaning entirely.

The AdditionalProperties functionality requires multiple parameters (valueType, valueClass) that would make the Field attribute more complex and harder to maintain. Each new option added to Field increases its complexity exponentially.

@butschster butschster merged commit 984af7f into 2.x Aug 22, 2025
5 checks passed
@butschster butschster deleted the issue/10 branch August 22, 2025 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

How to describe an arbitrary property

3 participants