Skip to main content

OpenAPI 3.1 Features

This document describes the OpenAPI 3.1 specific features supported by php-swagger-test.

Table of Contents

Version Detection

The library automatically detects OpenAPI 3.1 schemas based on the openapi field:

use ByJG\ApiTools\Base\Schema;

// OpenAPI 3.0 - returns OpenApiSchema
$schema30 = Schema::fromJson('{"openapi": "3.0.3", ...}');

// OpenAPI 3.1 - returns OpenApi31Schema
$schema31 = Schema::fromJson('{"openapi": "3.1.0", ...}');

// Check version
echo $schema31->getSpecificationVersion(); // "3.1"

Nullable Types

OpenAPI 3.0 Approach

OpenAPI 3.0 uses the nullable keyword:

{
"type": "string",
"nullable": true
}

OpenAPI 3.1 Approach

OpenAPI 3.1 uses JSON Schema union types:

{
"type": [
"string",
"null"
]
}

Testing Nullable Fields

$schema = Schema::fromFile('openapi31.json');
$responseBody = $schema->getResponseParameters('/users', 'get', 200);

// Both null and string values are valid
$responseBody->match(['email' => null]); // Valid
$responseBody->match(['email' => '[email protected]']); // Valid

Multiple Nullable Types

You can have multiple types including null:

{
"type": [
"string",
"number",
"null"
]
}

Nullable Objects with Required Fields

OpenAPI 3.1 supports nullable objects that have required fields and nested properties using $ref:

{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"manager": {
"type": ["object", "null"],
"required": ["phone"],
"properties": {
"email": {
"$ref": "#/components/schemas/emailProperty"
},
"phone": {
"$ref": "#/components/schemas/phoneNumberProperty"
},
"firstName": {
"$ref": "#/components/schemas/firstNameProperty"
}
}
}
},
"required": ["name"]
}

When validating nullable objects with required fields:

// Valid: manager is null
$requestBody->match([
'name' => 'ACME Corp',
'manager' => null
]);

// Valid: manager is omitted (not required)
$requestBody->match([
'name' => 'ACME Corp'
]);

// Valid: manager has required phone field
$requestBody->match([
'name' => 'ACME Corp',
'manager' => [
'phone' => '+1234567890'
]
]);

// Invalid: manager is present but missing required phone
$requestBody->match([
'name' => 'ACME Corp',
'manager' => [
'email' => '[email protected]'
]
]); // Throws NotMatchedException

This feature is particularly useful when modeling optional complex objects that, when present, must satisfy specific requirements.

Webhooks

Webhooks allow you to describe incoming HTTP requests that your API will receive.

Schema Definition

{
"openapi": "3.1.0",
"webhooks": {
"newUser": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"userId": {
"type": "integer"
},
"event": {
"type": "string"
}
}
}
}
}
}
}
}
}
}

Testing Webhooks

$schema = Schema::fromFile('openapi31.json');

// Check if webhooks exist
if ($schema->hasWebhooks()) {
// Get all webhook names
$webhooks = $schema->getWebhookNames(); // ['newUser', 'orderUpdated']

// Validate webhook request
$requestBody = $schema->getWebhookRequestParameters('newUser', 'post');
$requestBody->match([
'userId' => 123,
'event' => 'user.created'
]);

// Validate webhook response
$responseBody = $schema->getWebhookResponseParameters('newUser', 'post', 200);
$responseBody->match($responseData);
}

JSON Schema 2020-12 Features

const Keyword

Validate that a value is exactly a constant:

{
"type": "object",
"properties": {
"status": {
"const": "active"
}
}
}
$requestBody->match(['status' => 'active']);      // Valid
$requestBody->match(['status' => 'inactive']); // Throws NotMatchedException

Conditional Schemas (if/then/else)

Apply different validation rules based on conditions:

{
"type": "object",
"properties": {
"country": {
"type": "string"
},
"postalCode": {
"type": "string"
}
},
"if": {
"properties": {
"country": {
"const": "US"
}
}
},
"then": {
"properties": {
"postalCode": {
"pattern": "^[0-9]{5}(-[0-9]{4})?$"
}
}
},
"else": {
"properties": {
"postalCode": {
"pattern": "^[A-Z0-9 -]+$"
}
}
}
}
// US postal code
$requestBody->match([
'country' => 'US',
'postalCode' => '12345'
]); // Valid

// Non-US postal code
$requestBody->match([
'country' => 'CA',
'postalCode' => 'K1A 0B1'
]); // Valid

Tuple Validation (prefixItems)

Validate arrays with specific types at specific positions:

{
"type": "array",
"prefixItems": [
{
"type": "number",
"description": "latitude"
},
{
"type": "number",
"description": "longitude"
}
],
"minItems": 2,
"maxItems": 2
}
$requestBody->match([40.7128, -74.0060]);         // Valid (lat, lng)
$requestBody->match([40.7128]); // Invalid - too few items
$requestBody->match(['40.7128', -74.0060]); // Invalid - first item is string

$ref with Sibling Keywords

In OpenAPI 3.1, you can have sibling keywords alongside $ref:

{
"$ref": "#/components/schemas/User",
"description": "The authenticated user",
"example": {
"id": 1,
"name": "John"
}
}

In OpenAPI 3.0, you would need to wrap this in allOf.

Migration from 3.0 to 3.1

1. Update openapi Version

{
"openapi": "3.1.0",
// Changed from "3.0.3"
...
}

2. Replace nullable with Type Arrays

Before (3.0):

{
"type": "string",
"nullable": true
}

After (3.1):

{
"type": [
"string",
"null"
]
}

3. Simplify $ref Usage

Before (3.0):

{
"allOf": [
{
"$ref": "#/components/schemas/User"
},
{
"description": "Additional info"
}
]
}

After (3.1):

{
"$ref": "#/components/schemas/User",
"description": "Additional info"
}

4. Optional Server Variable Defaults

In 3.1, server variable defaults are optional:

Before (3.0 - required):

{
"servers": [
{
"url": "http://{host}",
"variables": {
"host": {
"default": "localhost"
// Required
}
}
}
]
}

After (3.1 - optional):

{
"servers": [
{
"url": "http://{host}",
"variables": {
"host": {
"enum": [
"localhost",
"example.com"
]
// default is optional
}
}
}
]
}

5. Use Modern JSON Schema Keywords

Take advantage of new keywords:

  • Use const instead of single-value enum
  • Use prefixItems for tuple validation
  • Use if/then/else for conditional validation

Compatibility Notes

  • Backward Compatibility: OpenAPI 3.0 schemas continue to work without changes
  • Mixed Usage: You can use both 3.0 and 3.1 schemas in the same project
  • Automatic Detection: The library automatically detects the version and applies appropriate validation rules
  • No Breaking Changes: Existing tests and code continue to work

Examples

See the /tests/example/ directory for complete working examples:

  • openapi31.json - Basic 3.1 schema
  • openapi31-nullable.json - Nullable type examples
  • openapi31-nested-ref-required.json - Nullable objects with required fields and nested $ref
  • openapi31-webhooks.json - Webhook definitions
  • openapi31-conditional.json - Conditional schemas
  • openapi31-tuples.json - Tuple validation

Further Reading