Skip to content

Commit 43e5874

Browse files
authored
feat: Allow additional properties to accept schema (#4)
1 parent 4f54c07 commit 43e5874

3 files changed

Lines changed: 121 additions & 5 deletions

File tree

README.md

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,13 +431,43 @@ $schema = SchemaFactory::object('config')
431431
// Control number of properties
432432
->minProperties(1)
433433
->maxProperties(10)
434-
->additionalProperties(false);
434+
// Control additional properties
435+
->additionalProperties(false); // Disallow additional properties
436+
437+
// Or validate additional properties against a schema
438+
$schema = SchemaFactory::object('config')
439+
->properties(
440+
SchemaFactory::string('name')->required(),
441+
SchemaFactory::string('type')->required(),
442+
)
443+
->additionalProperties(
444+
SchemaFactory::string()->minLength(3)
445+
);
435446
```
436447

437448
```php
438449
// Property names must be alphabetic
439450
$schema->isValid(['123' => 'value']); // false
440451
$schema->isValid(['validKey' => 'value']); // true
452+
453+
// Additional properties must match schema
454+
$schema->isValid([
455+
'name' => 'config1',
456+
'type' => 'test',
457+
'extra' => 'valid', // valid: string with length >= 3
458+
]); // true
459+
460+
$schema->isValid([
461+
'name' => 'config1',
462+
'type' => 'test',
463+
'extra' => 'no', // invalid: string too short
464+
]); // false
465+
466+
$schema->isValid([
467+
'name' => 'config1',
468+
'type' => 'test',
469+
'extra' => 123, // invalid: wrong type
470+
]); // false
441471
```
442472

443473
Objects also support pattern-based property validation using `patternProperties`:
@@ -918,6 +948,43 @@ $schema->toJson();
918948
}
919949
```
920950

951+
### From an Backed Enum
952+
953+
```php
954+
use Cortex\JsonSchema\SchemaFactory;
955+
956+
/**
957+
* This is the description of the enum
958+
*/
959+
enum PostType: int
960+
{
961+
case Article = 1;
962+
case News = 2;
963+
case Tutorial = 3;
964+
}
965+
966+
// Build the schema from the enum
967+
$schema = SchemaFactory::fromEnum(PostType::class);
968+
969+
// Convert to JSON Schema
970+
$schema->toJson();
971+
```
972+
973+
```json
974+
{
975+
"$schema": "http://json-schema.org/draft-07/schema#",
976+
"type": "object",
977+
"description": "This is the description of the enum",
978+
"properties": {
979+
"PostType": {
980+
"type": "integer",
981+
"enum": [1, 2, 3]
982+
}
983+
},
984+
"required": ["PostType"]
985+
}
986+
```
987+
921988
## Credits
922989

923990
- [Sean Tymon](https://github.com/tymondesigns)

src/Types/Concerns/HasProperties.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ trait HasProperties
2020
*/
2121
protected array $requiredProperties = [];
2222

23-
protected ?bool $additionalProperties = null;
23+
/**
24+
* @var bool|\Cortex\JsonSchema\Contracts\Schema|null
25+
*/
26+
protected mixed $additionalProperties = null;
2427

2528
protected ?int $minProperties = null;
2629

@@ -58,9 +61,11 @@ public function properties(Schema ...$properties): static
5861
}
5962

6063
/**
61-
* Allow or disallow additional properties
64+
* Set whether additional properties are allowed and optionally their schema
65+
*
66+
* @param bool|\Cortex\JsonSchema\Contracts\Schema $allowed Whether additional properties are allowed, or a schema they must match
6267
*/
63-
public function additionalProperties(bool $allowed): static
68+
public function additionalProperties(bool|Schema $allowed): static
6469
{
6570
$this->additionalProperties = $allowed;
6671

@@ -188,7 +193,9 @@ protected function addPropertiesToSchema(array $schema): array
188193
}
189194

190195
if ($this->additionalProperties !== null) {
191-
$schema['additionalProperties'] = $this->additionalProperties;
196+
$schema['additionalProperties'] = $this->additionalProperties instanceof Schema
197+
? $this->additionalProperties->toArray(includeSchemaRef: false, includeTitle: false)
198+
: $this->additionalProperties;
192199
}
193200

194201
if ($this->propertyNames !== null) {

tests/Unit/Targets/ObjectSchemaTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,48 @@
136136
]))->not->toThrow(SchemaException::class);
137137
});
138138

139+
it('can create an object schema with additional properties schema', function (): void {
140+
$schema = Schema::object('config')
141+
->description('Configuration object')
142+
->properties(
143+
Schema::string('name')->required(),
144+
Schema::string('type')->required(),
145+
)
146+
->additionalProperties(Schema::string()->minLength(3));
147+
148+
$schemaArray = $schema->toArray();
149+
150+
expect($schemaArray['additionalProperties'])->toHaveKey('type', 'string');
151+
expect($schemaArray['additionalProperties'])->toHaveKey('minLength', 3);
152+
153+
// Validation tests - valid additional property
154+
expect(fn() => $schema->validate([
155+
'name' => 'config1',
156+
'type' => 'test',
157+
'extra' => 'valid', // additional property matching schema
158+
]))->not->toThrow(SchemaException::class);
159+
160+
// Validation tests - invalid additional property (too short)
161+
expect(fn() => $schema->validate([
162+
'name' => 'config1',
163+
'type' => 'test',
164+
'extra' => 'no', // additional property not matching schema
165+
]))->toThrow(
166+
SchemaException::class,
167+
'All additional object properties must match schema: extra',
168+
);
169+
170+
// Validation tests - invalid additional property (wrong type)
171+
expect(fn() => $schema->validate([
172+
'name' => 'config1',
173+
'type' => 'test',
174+
'extra' => 123, // additional property not matching schema
175+
]))->toThrow(
176+
SchemaException::class,
177+
'All additional object properties must match schema: extra',
178+
);
179+
});
180+
139181
it('can create an object schema with property count constraints', function (): void {
140182
$schema = Schema::object('metadata')
141183
->description('Metadata object')

0 commit comments

Comments
 (0)