From 664f2994375eea29f0b364346b130ae392e70b4c Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 28 Nov 2025 15:28:53 +0100 Subject: [PATCH 1/2] Add $id property method to Property class --- src/Schema/Property.php | 18 +++++++++++ tests/Unit/AdvancedFeaturesTest.php | 48 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/Schema/Property.php b/src/Schema/Property.php index 3f8eb3e..bb4e81e 100644 --- a/src/Schema/Property.php +++ b/src/Schema/Property.php @@ -17,6 +17,8 @@ class Property protected ?string $schemaVersion = null; + protected ?string $id = null; + protected ?string $title = null; protected bool $isStrict = false; @@ -269,6 +271,18 @@ public function schemaVersion(string $version = 'https://json-schema.org/draft/2 return $this; } + /** + * Set the unique identifier ($id) for the schema. + * + * @param string $id A URI that serves as the canonical identifier for the schema. + */ + public function id(string $id): self + { + $this->id = $id; + + return $this; + } + /** * Add a property to the schema. */ @@ -302,6 +316,10 @@ public function toArray(): array $schema['$schema'] = $this->schemaVersion; } + if ($this->id) { + $schema['$id'] = $this->id; + } + if ($this->title) { $schema['title'] = $this->title; } diff --git a/tests/Unit/AdvancedFeaturesTest.php b/tests/Unit/AdvancedFeaturesTest.php index fafe406..9ca36e5 100644 --- a/tests/Unit/AdvancedFeaturesTest.php +++ b/tests/Unit/AdvancedFeaturesTest.php @@ -207,6 +207,54 @@ }); }); + describe('schema id', function () { + it('can set $id on schema', function () { + $property = new Property('Test'); + $property->id('https://example.com/schemas/user.json'); + $property->string('name'); + + $schema = $property->toArray(); + + expect($schema)->toHaveKey('$id', 'https://example.com/schemas/user.json'); + expect($schema['properties'])->toHaveKey('name'); + }); + + it('does not include $id when not set', function () { + $property = new Property('Test'); + $property->string('name'); + + $schema = $property->toArray(); + + expect($schema)->not->toHaveKey('$id'); + }); + + it('can use $id with Struct::define', function () { + $schema = Struct::define('User', 'A user schema', function (Property $property) { + $property->id('https://example.com/schemas/user.json'); + $property->string('name')->required(); + $property->int('age'); + })->toArray(); + + expect($schema)->toHaveKey('$id', 'https://example.com/schemas/user.json') + ->and($schema)->toHaveKey('type', 'object') + ->and($schema['properties'])->toHaveKey('name') + ->and($schema['properties'])->toHaveKey('age') + ->and($schema['required'])->toContain('name'); + }); + + it('can combine $id with $schema version', function () { + $property = new Property('Test'); + $property->schemaVersion(); + $property->id('https://example.com/schemas/test.json'); + $property->string('name'); + + $schema = $property->toArray(); + + expect($schema)->toHaveKey('$schema', 'https://json-schema.org/draft/2020-12/schema') + ->and($schema)->toHaveKey('$id', 'https://example.com/schemas/test.json'); + }); + }); + describe('title', function () { it('can set title on schema', function () { $property = new Property('Test'); From 0e3d8523ea4ce4087cce2f3eb473f13832af2687 Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 28 Nov 2025 15:40:20 +0100 Subject: [PATCH 2/2] Add documentation for id() method --- README.md | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9aeaf36..5933561 100644 --- a/README.md +++ b/README.md @@ -309,6 +309,7 @@ $property->strict(); // Disallows additional properties AND marks all fields as #### Strict Mode for LLM APIs The `strict()` method is particularly useful for LLM APIs like **OpenAI Structured Outputs** which require: + 1. `additionalProperties: false` 2. All properties in the `required` array @@ -328,6 +329,7 @@ $schema = Struct::define('User', 'A user schema', function (Property $property) ``` This generates: + ```json { "type": "object", @@ -347,8 +349,38 @@ Add metadata to your schemas: $property->title('User Schema'); $property->description('Schema for user data validation'); $property->schemaVersion('https://json-schema.org/draft/2020-12/schema'); +$property->id('https://example.com/schemas/user.json'); +``` + +#### Schema Identifier (`$id`) + +The `id()` method sets a unique identifier (URI) for your schema, following the JSON Schema specification. This is useful for referencing schemas and establishing canonical identifiers: + +```php +$schema = Struct::define('User', 'A user schema', function (Property $property) { + $property->id('https://example.com/schemas/user.json'); + $property->string('name')->required(); + $property->int('age'); +})->toArray(); +``` + +This generates: + +```json +{ + "$id": "https://example.com/schemas/user.json", + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" } + }, + "required": ["name"], + "additionalProperties": false +} ``` +The `$id` is optional and only included in the output when explicitly set. + You can also add titles to individual fields: ```php @@ -368,6 +400,7 @@ use Blaspsoft\Forerunner\Schema\Property; $schema = Struct::define('AdvancedUser', 'Comprehensive user data structure', function (Property $property) { // Schema metadata $property->schemaVersion(); + $property->id('https://example.com/schemas/advanced-user.json'); $property->title('Advanced User Schema'); // Helper methods @@ -408,6 +441,7 @@ This generates: ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/schemas/advanced-user.json", "type": "object", "title": "Advanced User Schema", "description": "Comprehensive user data structure", @@ -632,8 +666,8 @@ Please review [our security policy](../../security/policy) on how to report secu ## Credits -- [Blaspsoft](https://github.com/blaspsoft) -- [All Contributors](../../contributors) +- [Blaspsoft](https://github.com/blaspsoft) +- [All Contributors](../../contributors) ## License