Skip to content

Commit 864bd94

Browse files
authored
fix: Ensure object properties can have a different title (#21)
1 parent 290bbe6 commit 864bd94

9 files changed

Lines changed: 95 additions & 6 deletions

File tree

.github/workflows/static-analysis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,4 @@ jobs:
7070
ecs-cache-
7171
7272
- name: Run format checks
73-
run: composer format --no-progress-bar
73+
run: composer format:check --no-progress-bar

composer.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,19 @@
4444
"scripts": {
4545
"test": "pest",
4646
"ecs": "ecs check --fix",
47+
"ecs:check": "ecs check",
4748
"rector": "rector process",
49+
"rector:check": "rector process --dry-run",
4850
"stan": "phpstan analyse",
4951
"type-coverage": "pest --type-coverage --min=100",
5052
"format": [
5153
"@rector",
5254
"@ecs"
5355
],
56+
"format:check": [
57+
"@rector:check",
58+
"@ecs:check"
59+
],
5460
"check": [
5561
"@format",
5662
"@test",

docs/json-schema/schema-types/object.mdx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,29 @@ $productSchema = Schema::object('product')
103103
```
104104
</Accordion>
105105

106+
## Property Titles
107+
108+
Property names are defined by the schema constructor argument, while `title()` is metadata and only included when it differs from the property name:
109+
110+
```php
111+
$schema = Schema::object()
112+
->properties(
113+
Schema::string('email')->title('Email Address')
114+
);
115+
```
116+
117+
```json
118+
{
119+
"type": "object",
120+
"properties": {
121+
"email": {
122+
"type": "string",
123+
"title": "Email Address"
124+
}
125+
}
126+
}
127+
```
128+
106129
## Required Properties
107130

108131
Specify which properties are mandatory:

src/Contracts/JsonSchema.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,9 @@ public function version(SchemaVersion $schemaVersion): static;
9898
* Get the JSON Schema version for this schema.
9999
*/
100100
public function getVersion(): SchemaVersion;
101+
102+
/**
103+
* Get the initial title.
104+
*/
105+
public function getInitialTitle(): ?string;
101106
}

src/Schema.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ public static function from(
174174
// @phpstan-ignore argument.type
175175
is_array($value) || (is_string($value) && json_validate($value)) => self::fromJson($value, $schemaVersion),
176176
default => throw new SchemaException(
177-
'Unsupported value type. Only closures, enums, and classes are supported.',
177+
'Unsupported value type. Only closures, enums, arrays and classes are supported.',
178178
),
179179
};
180180
}

src/Types/AbstractSchema.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Cortex\JsonSchema\Types\Concerns\HasDefinitions;
2020
use Cortex\JsonSchema\Types\Concerns\HasDescription;
2121
use Cortex\JsonSchema\Types\Concerns\HasConditionals;
22+
use Cortex\JsonSchema\Types\Concerns\HasInitialTitle;
2223
use Cortex\JsonSchema\Types\Concerns\ValidatesVersionFeatures;
2324

2425
abstract class AbstractSchema implements JsonSchema
@@ -33,8 +34,9 @@ abstract class AbstractSchema implements JsonSchema
3334
use HasReadWrite;
3435
use HasValidation;
3536
use HasDescription;
36-
use HasConditionals;
3737
use HasDefinitions;
38+
use HasConditionals;
39+
use HasInitialTitle;
3840
use ValidatesVersionFeatures;
3941

4042
protected SchemaVersion $schemaVersion = SchemaVersion::Draft_2020_12;
@@ -47,6 +49,7 @@ public function __construct(
4749
?string $title = null,
4850
?SchemaVersion $schemaVersion = null,
4951
) {
52+
$this->initialTitle = $title;
5053
$this->title = $title;
5154
$this->schemaVersion = $schemaVersion ?? SchemaVersion::default();
5255
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cortex\JsonSchema\Types\Concerns;
6+
7+
/** @mixin \Cortex\JsonSchema\Contracts\JsonSchema */
8+
trait HasInitialTitle
9+
{
10+
protected ?string $initialTitle = null;
11+
12+
/**
13+
* Get the initial title.
14+
*
15+
* @internal
16+
*/
17+
public function getInitialTitle(): ?string
18+
{
19+
return $this->initialTitle;
20+
}
21+
}

src/Types/Concerns/HasProperties.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ trait HasProperties
5555
public function properties(JsonSchema ...$properties): static
5656
{
5757
foreach ($properties as $property) {
58-
$title = $property->getTitle();
58+
$title = $this->resolvePropertyTitle($property);
5959

6060
if ($title === null) {
6161
throw new SchemaException('Property must have a title');
@@ -254,7 +254,7 @@ public function hasRequiredProperties(): bool
254254
public function requireAll(): static
255255
{
256256
foreach ($this->properties as $property) {
257-
$title = $property->getTitle();
257+
$title = $this->resolvePropertyTitle($property);
258258

259259
if ($title !== null) {
260260
$this->requiredProperties[] = $title;
@@ -277,7 +277,15 @@ protected function addPropertiesToSchema(array $schema): array
277277
$schema['properties'] = [];
278278

279279
foreach ($this->properties as $name => $prop) {
280-
$schema['properties'][$name] = $prop->toArray(includeSchemaRef: false, includeTitle: false);
280+
$propertySchema = $prop->toArray(includeSchemaRef: false, includeTitle: true);
281+
282+
// If the property schema has a title and it matches the name,
283+
// then we don't need to include it in the schema
284+
if (array_key_exists('title', $propertySchema) && $propertySchema['title'] === $name) {
285+
unset($propertySchema['title']);
286+
}
287+
288+
$schema['properties'][$name] = $propertySchema;
281289
}
282290
}
283291

@@ -331,6 +339,14 @@ protected function addPropertiesToSchema(array $schema): array
331339
return $schema;
332340
}
333341

342+
/**
343+
* Resolve the property title from a schema instance.
344+
*/
345+
protected function resolvePropertyTitle(JsonSchema $jsonSchema): ?string
346+
{
347+
return $jsonSchema->getInitialTitle() ?? $jsonSchema->getTitle();
348+
}
349+
334350
/**
335351
* Get unevaluated properties features used by this schema.
336352
*

tests/Unit/Types/ObjectSchemaTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,21 @@
113113
}
114114
})->throws(SchemaException::class);
115115

116+
it('can set the property title separately from the property key', function (): void {
117+
$objectSchema = Schema::object()
118+
->properties(
119+
Schema::string('foo')->title('Foo'),
120+
Schema::string('bar'),
121+
);
122+
123+
$schemaArray = $objectSchema->toArray();
124+
125+
expect($schemaArray)->toHaveKey('properties.foo.type', 'string');
126+
expect($schemaArray)->toHaveKey('properties.foo.title', 'Foo');
127+
expect($schemaArray)->toHaveKey('properties.bar.type', 'string');
128+
expect($schemaArray['properties']['bar'])->not->toHaveKey('title');
129+
});
130+
116131
it('can create an object schema with additional properties control', function (): void {
117132
$objectSchema = Schema::object('config')
118133
->description('Configuration object')

0 commit comments

Comments
 (0)