Skip to content

swaggest/php-json-schema

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Swaggest JSON-schema implementation for PHP

Build Status codecov time tracker Code lines Comments

High definition PHP structures with JSON-schema based validation.

Supported schemas:

Installation

composer require swaggest/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 Swaggest\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 Scrutinizer Code Quality ) to allow best performance at maintenance cost.

Those violations are secured by comprehensive test coverage:

Contributing

Issues and pull requests are welcome!

Development supported by JetBrains.

About

High definition PHP structures with JSON-schema based validation

Topics

Resources

License

Stars

Watchers

Forks

Contributors 14