Skip to main content

Response Models

Response models define the structure of data you want to extract from text. They are PHP classes with typed properties that guide the LLM in generating structured output.

Creating Response Models

Using Artisan Command

# Basic response model
php artisan make:response-model PersonData

# Collection response model
php artisan make:response-model ProductList --collection

# Nested objects response model
php artisan make:response-model CompanyProfile --nested

# With description
php artisan make:response-model Invoice --description="Invoice extracted from PDF"
# @doctest id="5fdb"

Manual Creation

Create a class in app/ResponseModels/:
<?php

namespace App\ResponseModels;

final class PersonData
{
    public function __construct(
        /** The person's full name */
        public readonly string $name,

        /** The person's age in years */
        public readonly int $age,

        /** The person's email address */
        public readonly ?string $email = null,
    ) {}
}
// @doctest id="c3e0"

Property Types

Basic Types

final class BasicTypes
{
    public function __construct(
        public readonly string $text,
        public readonly int $count,
        public readonly float $price,
        public readonly bool $isActive,
    ) {}
}
// @doctest id="72f6"

Nullable Properties

final class WithOptional
{
    public function __construct(
        public readonly string $required,
        public readonly ?string $optional = null,
        public readonly ?int $maybeNumber = null,
    ) {}
}
// @doctest id="1570"

Arrays

final class WithArrays
{
    public function __construct(
        /** @var string[] List of tags */
        public readonly array $tags,

        /** @var int[] List of scores */
        public readonly array $scores,
    ) {}
}
// @doctest id="c850"

Enums

enum Priority: string
{
    case Low = 'low';
    case Medium = 'medium';
    case High = 'high';
}

final class Task
{
    public function __construct(
        public readonly string $title,
        public readonly Priority $priority,
    ) {}
}
// @doctest id="4315"

Nested Objects

final class Address
{
    public function __construct(
        public readonly string $street,
        public readonly string $city,
        public readonly string $country,
    ) {}
}

final class Person
{
    public function __construct(
        public readonly string $name,
        public readonly Address $address,
    ) {}
}
// @doctest id="a7fb"

Collections

final class OrderItem
{
    public function __construct(
        public readonly string $product,
        public readonly int $quantity,
        public readonly float $price,
    ) {}
}

final class Order
{
    public function __construct(
        public readonly string $orderId,

        /** @var OrderItem[] */
        public readonly array $items,

        public readonly float $total,
    ) {}
}
// @doctest id="08f8"

Property Descriptions

Property descriptions in docblocks guide the LLM:
final class ProductReview
{
    public function __construct(
        /** The overall sentiment: positive, negative, or neutral */
        public readonly string $sentiment,

        /** A score from 1-5 indicating review quality */
        public readonly int $rating,

        /** Key points mentioned in the review */
        public readonly array $highlights,

        /** Any concerns or complaints raised */
        public readonly ?array $concerns = null,
    ) {}
}
// @doctest id="ae56"

Using Response Models

Basic Extraction

use Cognesy\Instructor\Laravel\Facades\StructuredOutput;
use App\ResponseModels\PersonData;

$person = StructuredOutput::with(
    messages: 'John Smith is a 30-year-old developer at [email protected]',
    responseModel: PersonData::class,
)->get();

echo $person->name;  // "John Smith"
echo $person->age;   // 30
echo $person->email; // "[email protected]"
// @doctest id="7414"

With Array Schema

For simple cases, you can use an array schema instead of a class:
$person = StructuredOutput::with(
    messages: 'John is 30 years old',
    responseModel: [
        'type' => 'object',
        'properties' => [
            'name' => ['type' => 'string', 'description' => 'Person name'],
            'age' => ['type' => 'integer', 'description' => 'Person age'],
        ],
        'required' => ['name', 'age'],
    ],
)->get();

echo $person['name']; // "John"
echo $person['age'];  // 30
// @doctest id="fb2e"

Extracting Collections

final class Product
{
    public function __construct(
        public readonly string $name,
        public readonly float $price,
    ) {}
}

// Wrap in array for multiple items
$products = StructuredOutput::with(
    messages: 'Products: iPhone $999, MacBook $1299, AirPods $199',
    responseModel: [
        'type' => 'array',
        'items' => Product::class,
    ],
)->get();

foreach ($products as $product) {
    echo "{$product->name}: \${$product->price}\n";
}
// @doctest id="a5d3"

Validation

Using Symfony Validator

use Symfony\Component\Validator\Constraints as Assert;

final class UserRegistration
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Length(min: 2, max: 100)]
        public readonly string $name,

        #[Assert\NotBlank]
        #[Assert\Email]
        public readonly string $email,

        #[Assert\Range(min: 18, max: 120)]
        public readonly int $age,
    ) {}
}
// @doctest id="691b"

Custom Validation

use Cognesy\Instructor\Validation\Contracts\CanValidateObject;

class AgeValidator implements CanValidateObject
{
    public function validate(object $object): array
    {
        $errors = [];

        if ($object->age < 0) {
            $errors[] = 'Age cannot be negative';
        }

        return $errors;
    }
}

// Use the validator
$user = StructuredOutput::with(
    messages: 'User: John, age -5',
    responseModel: UserData::class,
)
->withValidators(AgeValidator::class)
->get();
// @doctest id="a501"

Best Practices

1. Use Descriptive Property Names

// Good
public readonly string $customerEmailAddress;

// Less clear
public readonly string $email;
// @doctest id="0551"

2. Add Detailed Descriptions

public function __construct(
    /**
     * The product SKU in format XXX-YYYY-ZZZ
     * Example: ABC-1234-XYZ
     */
    public readonly string $sku,
) {}
// @doctest id="79fb"

3. Use Appropriate Types

// Use int for counts
public readonly int $quantity;

// Use float for prices
public readonly float $price;

// Use enums for fixed options
public readonly Status $status;
// @doctest id="9a29"

4. Make Optional Properties Nullable

// Required
public readonly string $name,

// Optional
public readonly ?string $nickname = null,
// @doctest id="f95f"

5. Use Readonly Properties

// Immutable - recommended
public readonly string $name;

// Mutable - avoid unless necessary
public string $name;
// @doctest id="cccf"

Generated Stubs

The make:response-model command generates these stub types:

Basic Stub

final class {{ class }}
{
    public function __construct(
        /** The name of the person */
        public readonly string $name,

        /** The age of the person in years */
        public readonly int $age,

        /** Optional email address */
        public readonly ?string $email = null,
    ) {}
}
// @doctest id="797c"

Collection Stub (--collection)

final class {{ class }}
{
    public function __construct(
        /** List of extracted items */
        public readonly array $items,
    ) {}
}

final class {{ class }}Item
{
    public function __construct(
        public readonly string $name,
        public readonly ?string $description = null,
    ) {}
}
// @doctest id="2ea9"

Nested Stub (--nested)

final class {{ class }}
{
    public function __construct(
        public readonly string $title,
        public readonly {{ class }}Contact $contact,
        public readonly ?{{ class }}Address $address = null,
    ) {}
}

final class {{ class }}Contact
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
    ) {}
}

final class {{ class }}Address
{
    public function __construct(
        public readonly string $street,
        public readonly string $city,
        public readonly string $country,
    ) {}
}
// @doctest id="6a70"