bobahbi44 / json-schema
High definition PHP structures with JSON-schema based validation
Requires
- php: >=5.4
- ext-bcmath: *
- ext-json: *
- phplang/scope-exit: ^1.0
- swaggest/json-diff: ^3.8.2
- symfony/polyfill-mbstring: ^1.19
Requires (Dev)
- phperf/phpunit: 4.8.37
Suggests
- ext-mbstring: For better performance
This package is not auto-updated.
Last update: 2024-11-27 15:48:10 UTC
README
High definition PHP structures with JSON-schema based validation.
Supported schemas:
Installation
composer require bobahbi44/php-json-schema
Usage
Structure definition can be done either with json-schema
or with
PHP
class extending Swaggest\JsonSchema\Structure\ClassStructure
Validating JSON data against given schema
Define your json-schema
$schemaJson = <<<'JSON' { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "orders": { "type": "array", "items": { "$ref": "#/definitions/order" } } }, "required":["id"], "definitions": { "order": { "type": "object", "properties": { "id": { "type": "integer" }, "price": { "type": "number" }, "updated": { "type": "string", "format": "date-time" } }, "required":["id"] } } } JSON;
Load it
use Bobahbi44\JsonSchema\Schema; $schema = Schema::import(json_decode($schemaJson));
Validate data
$schema->in(json_decode(<<<'JSON' { "id": 1, "name":"John Doe", "orders":[ { "id":1 }, { "price":1.0 } ] } JSON )); // Exception: Required property missing: id at #->properties:orders->items[1]->#/definitions/order
You can also call Schema::import
on string uri
to schema json data.
$schema = Schema::import('http://localhost:1234/my_schema.json');
Or with boolean argument.
$schema = Schema::import(true); // permissive schema, always validates $schema = Schema::import(false); // restrictive schema, always invalidates
Understanding error cause
With complex schemas it may be hard to find out what's wrong with your data. Exception message can look like:
No valid results for oneOf {
0: Enum failed, enum: ["a"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[0]
1: Enum failed, enum: ["b"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[1]
2: No valid results for anyOf {
0: Enum failed, enum: ["c"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[2]->$ref[#/cde]->anyOf[0]
1: Enum failed, enum: ["d"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[2]->$ref[#/cde]->anyOf[1]
2: Enum failed, enum: ["e"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[2]->$ref[#/cde]->anyOf[2]
} at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[2]->$ref[#/cde]
} at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo
For ambiguous schemas defined with oneOf
/anyOf
message is indented multi-line string.
Processing path is a combination of schema and data pointers. You can use InvalidValue->getSchemaPointer()
and InvalidValue->getDataPointer()
to extract schema/data pointer.
You can receive Schema
instance that failed validation with InvalidValue->getFailedSubSchema
.
You can build error tree using InvalidValue->inspect()
.
PHP structured classes with validation
/** * @property int $quantity PHPDoc defined dynamic properties will be validated on every set */ class User extends ClassStructure { /* Native (public) properties will be validated only on import and export of structure data */ /** @var int */ public $id; public $name; /** @var Order[] */ public $orders; /** @var UserInfo */ public $info; /** * @param Properties|static $properties * @param Schema $ownerSchema */ public static function setUpProperties($properties, Schema $ownerSchema) { // You can add custom meta to your schema $dbTable = new DbTable; $dbTable->tableName = 'users'; $ownerSchema->addMeta($dbTable); // Setup property schemas $properties->id = Schema::integer(); $properties->id->addMeta(new DbId($dbTable)); // You can add meta to property. $properties->name = Schema::string(); // You can embed structures to main level with nested schemas $properties->info = UserInfo::schema()->nested(); // You can set default value for property $defaultOptions = new UserOptions(); $defaultOptions->autoLogin = true; $defaultOptions->groupName = 'guest'; // UserOptions::schema() is safe to change as it is protected with lazy cloning $properties->options = UserOptions::schema()->setDefault(UserOptions::export($defaultOptions)); // Dynamic (phpdoc-defined) properties can be used as well $properties->quantity = Schema::integer(); $properties->quantity->minimum = 0; // Property can be any complex structure $properties->orders = Schema::create(); $properties->orders->items = Order::schema(); $ownerSchema->required = array(self::names()->id); } } class UserInfo extends ClassStructure { public $firstName; public $lastName; public $birthDay; /** * @param Properties|static $properties * @param Schema $ownerSchema */ public static function setUpProperties($properties, Schema $ownerSchema) { $properties->firstName = Schema::string(); $properties->lastName = Schema::string(); $properties->birthDay = Schema::string(); } } class UserOptions extends ClassStructure { public $autoLogin; public $groupName; /** * @param Properties|static $properties * @param Schema $ownerSchema */ public static function setUpProperties($properties, Schema $ownerSchema) { $properties->autoLogin = Schema::boolean(); $properties->groupName = Schema::string(); } } class Order implements ClassStructureContract { use ClassStructureTrait; // You can use trait if you can't/don't want to extend ClassStructure const FANCY_MAPPING = 'fAnCy'; // You can create additional mapping namespace public $id; public $userId; public $dateTime; public $price; /** * @param Properties|static $properties * @param Schema $ownerSchema */ public static function setUpProperties($properties, Schema $ownerSchema) { // Add some meta data to your schema $dbMeta = new DbTable(); $dbMeta->tableName = 'orders'; $ownerSchema->addMeta($dbMeta); // Define properties $properties->id = Schema::integer(); $properties->userId = User::properties()->id; // referencing property of another schema keeps meta $properties->dateTime = Schema::string(); $properties->dateTime->format = Format::DATE_TIME; $properties->price = Schema::number(); $ownerSchema->setFromRef('#/definitions/order'); // Define default mapping if any. $ownerSchema->addPropertyMapping('date_time', Order::names()->dateTime); // Use mapped name references after the default mapping was configured. $names = self::names($ownerSchema->properties); $ownerSchema->required = array( $names->id, $names->dateTime, // "date_time" $names->price ); // Define additional mapping $ownerSchema->addPropertyMapping('DaTe_TiMe', Order::names()->dateTime, self::FANCY_MAPPING); $ownerSchema->addPropertyMapping('Id', Order::names()->id, self::FANCY_MAPPING); $ownerSchema->addPropertyMapping('PrIcE', Order::names()->price, self::FANCY_MAPPING); } }
Validation of dynamic properties is performed on set, this can help to find source of invalid data at cost of some performance drop
$user = new User(); $user->quantity = -1; // Exception: Value more than 0 expected, -1 received
Validation of native properties is performed only on import/export
$user = new User(); $user->quantity = 10; User::export($user); // Exception: Required property missing: id
Error messages provide a path to invalid data
$user = new User(); $user->id = 1; $user->name = 'John Doe'; $order = new Order(); $order->dateTime = (new \DateTime())->format(DATE_RFC3339); $user->orders[] = $order; User::export($user); // Exception: Required property missing: id at #->properties:orders->items[0]
Nested structures
Nested structures allow you to make composition: flatten several objects in one and separate back.
$user = new User(); $user->id = 1; $info = new UserInfo(); $info->firstName = 'John'; $info->lastName = 'Doe'; $info->birthDay = '1970-01-01'; $user->info = $info; $json = <<<JSON { "id": 1, "firstName": "John", "lastName": "Doe", "birthDay": "1970-01-01" } JSON; $exported = User::export($user); $this->assertSame($json, json_encode($exported, JSON_PRETTY_PRINT)); $imported = User::import(json_decode($json)); $this->assertSame('John', $imported->info->firstName); $this->assertSame('Doe', $imported->info->lastName);
You can also use \Swaggest\JsonSchema\Structure\Composition
to dynamically create schema compositions. This can be
helpful to deal with results of database query on joined data.
$schema = new Composition(UserInfo::schema(), Order::schema()); $json = <<<JSON { "id": 1, "firstName": "John", "lastName": "Doe", "price": 2.66 } JSON; $object = $schema->import(json_decode($json)); // Get particular object with `pick` accessor $info = UserInfo::pick($object); $order = Order::pick($object); // Data is imported objects of according classes $this->assertTrue($order instanceof Order); $this->assertTrue($info instanceof UserInfo); $this->assertSame(1, $order->id); $this->assertSame('John', $info->firstName); $this->assertSame('Doe', $info->lastName); $this->assertSame(2.66, $order->price);
Keys mapping
If property names of PHP objects should be different from raw data you can call ->addPropertyMapping
on owner schema.
// Define default mapping if any $ownerSchema->addPropertyMapping('date_time', Order::names()->dateTime); // Define additional mapping $ownerSchema->addPropertyMapping('DaTe_TiMe', Order::names()->dateTime, self::FANCY_MAPPING); $ownerSchema->addPropertyMapping('Id', Order::names()->id, self::FANCY_MAPPING); $ownerSchema->addPropertyMapping('PrIcE', Order::names()->price, self::FANCY_MAPPING);
It will affect data mapping:
$order = new Order(); $order->id = 1; $order->dateTime = '2015-10-28T07:28:00Z'; $order->price = 2.2; $exported = Order::export($order); $json = <<<JSON { "id": 1, "date_time": "2015-10-28T07:28:00Z", "price": 2.2 } JSON; $this->assertSame($json, json_encode($exported, JSON_PRETTY_PRINT)); $imported = Order::import(json_decode($json)); $this->assertSame('2015-10-28T07:28:00Z', $imported->dateTime);
You can have multiple mapping namespaces, controlling with mapping
property of Context
$options = new Context(); $options->mapping = Order::FANCY_MAPPING; $exported = Order::export($order, $options); $json = <<<JSON { "Id": 1, "DaTe_TiMe": "2015-10-28T07:28:00Z", "PrIcE": 2.2 } JSON; $this->assertSame($json, json_encode($exported, JSON_PRETTY_PRINT)); $imported = Order::import(json_decode($json), $options); $this->assertSame('2015-10-28T07:28:00Z', $imported->dateTime);
You can create your own pre-processor implementing Swaggest\JsonSchema\DataPreProcessor
.
Meta
Meta
is a way to complement Schema
with your own data. You can keep and retrieve it.
You can store it.
$dbMeta = new DbTable(); $dbMeta->tableName = 'orders'; $ownerSchema->addMeta($dbMeta);
And get back.
// Retrieving meta $dbTable = DbTable::get(Order::schema()); $this->assertSame('orders', $dbTable->tableName);
Mapping without validation
If you want to tolerate invalid data or improve mapping performance you can specify skipValidation
flag in
processing Context
$schema = Schema::object(); $schema->setProperty('one', Schema::integer()); $schema->properties->one->minimum = 5; $options = new Context(); $options->skipValidation = true; $res = $schema->in(json_decode('{"one":4}'), $options); $this->assertSame(4, $res->one);
Overriding mapping classes
If you want to map data to a different class you can register mapping at top level of your importer structure.
class CustomSwaggerSchema extends SwaggerSchema { public static function import($data, Context $options = null) { if ($options === null) { $options = new Context(); } $options->objectItemClassMapping[Schema::className()] = CustomSchema::className(); return parent::import($data, $options); } }
Or specify it in processing context
$context = new Context(); $context->objectItemClassMapping[Schema::className()] = CustomSchema::className(); $schema = SwaggerSchema::schema()->in(json_decode( file_get_contents(__DIR__ . '/../../../../spec/petstore-swagger.json') ), $context); $this->assertInstanceOf(CustomSchema::className(), $schema->definitions['User']);
Code quality and test coverage
Some code quality best practices are deliberately violated here ( see ) to allow best performance at maintenance cost.
Those violations are secured by comprehensive test coverage:
- draft-04, draft-06, draft-07 of JSON-Schema-Test-Suite
- test cases (excluding
$data
and few tests) of epoberezkin/ajv (a mature js implementation)
Contributing
Issues and pull requests are welcome!
Development supported by JetBrains.