Repository: prism-php/prism (PHP)

Issue:
Add Support for OpenAI Extended Schema (Structured Output JSON)
I’d like to suggest adding support for OpenAI’s Structured Output schema, which allows us to define richer and more precise response formats when using tools/function calling.

OpenAI supports a subset of JSON Schema with some extended properties and types, including:

Supported `string` properties:

- pattern: A regular expression that the string must match
- format: [date-time, time, date, duration, email, hostname, ipv4, ipv6, uuid]

Supported `number` properties:

- multipleOf — The number must be a multiple of this value.
- maximum — The number must be less than or equal to this value.
- exclusiveMaximum — The number must be less than this value.
- minimum — The number must be greater than or equal to this value.
- exclusiveMinimum — The number must be greater than this value.


Supported `array` properties:

- minItems — The array must have at least this many items.
- maxItems — The array must have at most this many items.

This is the [Reference](https://platform.openai.com/docs/guides/structured-outputs#supported_schemas) URL for OpenAI

These extensions are extremely helpful when defining expected response formats and improving the reliability of model outputs. It would be awesome if the schema builder could support generating this structure natively.

Happy to help if you’d like PR to implement it.




Discussion: Also support of `anyOf` would be extremely helpful
I'm open to support the extended schema and would welcome a PR, also with `anyOf` support



Fix (patch):
diff --git a/docs/core-concepts/schemas.md b/docs/core-concepts/schemas.md
index a2d0b8b32..345dfcf50 100644
--- a/docs/core-concepts/schemas.md
+++ b/docs/core-concepts/schemas.md
@@ -131,6 +131,66 @@ $profileSchema = new ObjectSchema(
 );
 ```
 
+### AnyOfSchema
+
+For flexible data that can match one of several schemas. This is particularly useful when you need to handle different data types or structures in the same field.
+
+> [!IMPORTANT]
+> **OpenAI Compatibility**: The AnyOfSchema is designed to work with OpenAI's structured outputs `anyOf` specification. Each nested schema must be a valid JSON schema according to OpenAI's subset requirements. For best results, ensure each nested schema has a proper `type` field.
+
+```php
+use Prism\Prism\Schema\AnyOfSchema;
+use Prism\Prism\Schema\StringSchema;
+use Prism\Prism\Schema\NumberSchema;
+use Prism\Prism\Schema\ObjectSchema;
+
+// Simple example: A value that can be either a string or number
+$flexibleValueSchema = new AnyOfSchema(
+    schemas: [
+        new StringSchema('text', 'A text value'),
+        new NumberSchema('number', 'A numeric value'),
+    ],
+    name: 'flexible_value',
+    description: 'A value that can be either text or numeric'
+);
+
+// Complex example: Different content types
+$contentSchema = new AnyOfSchema(
+    schemas: [
+        new ObjectSchema(
+            name: 'article',
+            description: 'A blog article',
+            properties: [
+                new StringSchema('title', 'Article title'),
+                new StringSchema('content', 'Article content'),
+                new StringSchema('author', 'Article author'),
+            ],
+            requiredFields: ['title', 'content']
+        ),
+        new ObjectSchema(
+            name: 'image',
+            description: 'An image post',
+            properties: [
+                new StringSchema('url', 'Image URL'),
+                new StringSchema('caption', 'Image caption'),
+                new NumberSchema('width', 'Image width in pixels'),
+                new NumberSchema('height', 'Image height in pixels'),
+            ],
+            requiredFields: ['url']
+        ),
+    ],
+    name: 'content',
+    description: 'Content that can be either an article or an image'
+);
+```
+
+**Key Features:**
+- Accepts an array of schema objects that define the possible types
+- Automatically validates nested schemas for OpenAI compatibility
+- Supports nullable values through the `nullable` parameter
+- Optional name and description parameters
+- Removes unsupported JSON schema properties automatically
+
 ## Nullable Fields
 
 Sometimes, not every field is required. You can make any schema nullable by setting the `nullable` parameter to `true`:
diff --git a/src/Providers/Gemini/Maps/SchemaMap.php b/src/Providers/Gemini/Maps/SchemaMap.php
index 3d4fa7b80..0a7e51ed1 100644
--- a/src/Providers/Gemini/Maps/SchemaMap.php
+++ b/src/Providers/Gemini/Maps/SchemaMap.php
@@ -3,6 +3,7 @@
 namespace Prism\Prism\Providers\Gemini\Maps;
 
 use Prism\Prism\Contracts\Schema;
+use Prism\Prism\Schema\AnyOfSchema;
 use Prism\Prism\Schema\ArraySchema;
 use Prism\Prism\Schema\BooleanSchema;
 use Prism\Prism\Schema\NumberSchema;
@@ -24,6 +25,14 @@ public function toArray(): array
         // Remove unsupported fields
         unset($schemaArray['additionalProperties'], $schemaArray['description'], $schemaArray['name']);
 
+        // Handle AnyOfSchema - Gemini doesn't support anyOf, so we'll return the schema as-is
+        // or we could choose the first schema type as a fallback
+        if ($this->schema instanceof AnyOfSchema) {
+            // For Gemini, we'll just return the raw anyOf structure
+            // This might not be ideal, but it preserves the intent
+            return $schemaArray;
+        }
+
         return array_merge(
             array_filter([
                 ...$schemaArray,
diff --git a/src/Schema/AnyOfSchema.php b/src/Schema/AnyOfSchema.php
new file mode 100644
index 000000000..52ba3aa1f
--- /dev/null
+++ b/src/Schema/AnyOfSchema.php
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Prism\Prism\Schema;
+
+use Prism\Prism\Concerns\NullableSchema;
+use Prism\Prism\Contracts\Schema;
+
+class AnyOfSchema implements Schema
+{
+    use NullableSchema;
+
+    /**
+     * @param  array<Schema>  $schemas  Array of schema instances to match any of
+     * @param  string|null  $name  Optional name for the schema
+     * @param  string|null  $description  Optional description for the schema
+     * @param  bool  $nullable  Whether the schema can be null
+     */
+    public function __construct(
+        public readonly array $schemas,
+        public readonly ?string $name = null,
+        public readonly ?string $description = null,
+        public readonly bool $nullable = false,
+    ) {}
+
+    #[\Override]
+    public function name(): string
+    {
+        return $this->name ?? 'item';
+    }
+
+    #[\Override]
+    public function toArray(): array
+    {
+        // Validate that all nested schemas are valid according to OpenAI requirements
+        $validatedSchemas = array_map(
+            fn (Schema $schema): array => $this->validateNestedSchema($schema->toArray()),
+            $this->schemas
+        );
+
+        $schema = [
+            'anyOf' => $validatedSchemas,
+        ];
+
+        // Add description only if provided
+        if ($this->description !== null) {
+            $schema['description'] = $this->description;
+        }
+
+        // If nullable, add null as one of the anyOf options
+        if ($this->nullable) {
+            $schema['anyOf'][] = ['type' => 'null'];
+        }
+
+        return $schema;
+    }
+
+    /**
+     * Validate that nested schema conforms to OpenAI's subset requirements.
+     *
+     * @param  array<string, mixed>  $schemaArray
+     * @return array<string, mixed>
+     */
+    protected function validateNestedSchema(array $schemaArray): array
+    {
+        // For anyOf, nested schemas must each be a valid JSON schema per OpenAI's subset
+        // This includes ensuring they have proper structure and don't contain unsupported features
+
+        // Ensure the schema has a type (required by OpenAI)
+        if (! isset($schemaArray['type']) && ! isset($schemaArray['anyOf']) && ! isset($schemaArray['oneOf'])) {
+            throw new \InvalidArgumentException(
+                'Each nested schema in anyOf must have a "type" or be a composition schema (anyOf/oneOf)'
+            );
+        }
+
+        // Remove unsupported properties that might cause issues with OpenAI
+        $unsupportedKeys = ['examples', 'default', 'if', 'then', 'else', 'not'];
+        foreach ($unsupportedKeys as $key) {
+            unset($schemaArray[$key]);
+        }
+
+        return $schemaArray;
+    }
+}
diff --git a/src/Schema/ArraySchema.php b/src/Schema/ArraySchema.php
index afea515fb..1e871a0ca 100644
--- a/src/Schema/ArraySchema.php
+++ b/src/Schema/ArraySchema.php
@@ -16,6 +16,8 @@ public function __construct(
         public readonly string $description,
         public readonly Schema $items,
         public readonly bool $nullable = false,
+        public readonly ?int $minItems = null,
+        public readonly ?int $maxItems = null,
     ) {}
 
     #[\Override]
@@ -27,12 +29,21 @@ public function name(): string
     #[\Override]
     public function toArray(): array
     {
-        return [
+        $schema = [
             'description' => $this->description,
             'type' => $this->nullable
                 ? $this->castToNullable('array')
                 : 'array',
             'items' => $this->items->toArray(),
         ];
+
+        if ($this->minItems !== null) {
+            $schema['minItems'] = $this->minItems;
+        }
+        if ($this->maxItems !== null) {
+            $schema['maxItems'] = $this->maxItems;
+        }
+
+        return $schema;
     }
 }
diff --git a/src/Schema/NumberSchema.php b/src/Schema/NumberSchema.php
index a20a4f9e9..2cf4e4995 100644
--- a/src/Schema/NumberSchema.php
+++ b/src/Schema/NumberSchema.php
@@ -15,6 +15,11 @@ public function __construct(
         public readonly string $name,
         public readonly string $description,
         public readonly bool $nullable = false,
+        public readonly ?float $multipleOf = null,
+        public readonly ?float $maximum = null,
+        public readonly ?float $exclusiveMaximum = null,
+        public readonly ?float $minimum = null,
+        public readonly ?float $exclusiveMinimum = null,
     ) {}
 
     #[\Override]
@@ -26,11 +31,29 @@ public function name(): string
     #[\Override]
     public function toArray(): array
     {
-        return [
+        $schema = [
             'description' => $this->description,
             'type' => $this->nullable
                 ? $this->castToNullable('number')
                 : 'number',
         ];
+
+        if ($this->multipleOf !== null) {
+            $schema['multipleOf'] = $this->multipleOf;
+        }
+        if ($this->maximum !== null) {
+            $schema['maximum'] = $this->maximum;
+        }
+        if ($this->exclusiveMaximum !== null) {
+            $schema['exclusiveMaximum'] = $this->exclusiveMaximum;
+        }
+        if ($this->minimum !== null) {
+            $schema['minimum'] = $this->minimum;
+        }
+        if ($this->exclusiveMinimum !== null) {
+            $schema['exclusiveMinimum'] = $this->exclusiveMinimum;
+        }
+
+        return $schema;
     }
 }
diff --git a/src/Schema/StringSchema.php b/src/Schema/StringSchema.php
index 0773972f0..d357bcf79 100644
--- a/src/Schema/StringSchema.php
+++ b/src/Schema/StringSchema.php
@@ -15,6 +15,8 @@ public function __construct(
         public readonly string $name,
         public readonly string $description,
         public readonly bool $nullable = false,
+        public readonly ?string $pattern = null,
+        public readonly ?string $format = null,
     ) {}
 
     #[\Override]
@@ -26,11 +28,20 @@ public function name(): string
     #[\Override]
     public function toArray(): array
     {
-        return [
+        $schema = [
             'description' => $this->description,
             'type' => $this->nullable
                 ? $this->castToNullable('string')
                 : 'string',
         ];
+
+        if ($this->pattern !== null) {
+            $schema['pattern'] = $this->pattern;
+        }
+        if ($this->format !== null) {
+            $schema['format'] = $this->format;
+        }
+
+        return $schema;
     }
 }