Skip to content

Validation

Validation runs after deserialization and before the result is returned. If the extracted data does not meet your rules, Instructor can automatically retry the request, feeding the validation errors back to the model so it can self-correct.

Symfony Validation Attributes

Instructor uses the Symfony Validator component under the hood. Add constraint attributes to your response model to enforce field-level rules:

use Symfony\Component\Validator\Constraints as Assert;

class Person {
    #[Assert\NotBlank]
    #[Assert\Length(min: 3)]
    public string $name;

    #[Assert\PositiveOrZero]
    public int $age;
}

If the model returns a name shorter than three characters or a negative age, validation fails and Instructor can retry the request automatically.

For a full list of available constraints, see the Symfony Validation documentation.

Retries

Retries are configured on the runtime, not on individual requests. When validation fails and retries are available, Instructor sends the validation errors back to the model and asks it to try again:

use Cognesy\Instructor\StructuredOutputRuntime;
use Cognesy\Polyglot\Inference\Config\LLMConfig;

$runtime = StructuredOutputRuntime::fromConfig(
    LLMConfig::fromPreset('openai')
)->withMaxRetries(3);

The maxRetries value controls how many additional attempts are allowed after the first one. With maxRetries(3), Instructor will try up to 4 times total (1 initial + 3 retries).

If all attempts fail validation, Instructor throws an exception.

use Cognesy\Instructor\StructuredOutput;
use Symfony\Component\Validator\Constraints as Assert;

class Person {
    #[Assert\Length(min: 3)]
    public string $name;

    #[Assert\PositiveOrZero]
    public int $age;
}

$person = (new StructuredOutput)
    ->withRuntime($runtime)
    ->with(
        messages: 'His name is JX, aka Jason, he is -28 years old.',
        responseModel: Person::class,
    )
    ->get();

In this example, the model might initially return name: "JX" and age: -28. Validation catches both issues, and the retry prompt tells the model what went wrong so it can return name: "Jason" and age: 28 on the next attempt.

Custom Validation With ValidationMixin

For object-level validation logic that goes beyond simple field constraints, use the ValidationMixin trait. Implement a validate() method that returns a ValidationResult:

use Cognesy\Instructor\Validation\Traits\ValidationMixin;
use Cognesy\Instructor\Validation\ValidationResult;
use Cognesy\Instructor\Validation\ValidationError;

class UserDetails
{
    use ValidationMixin;

    public string $name;
    public int $age;

    public function validate(): ValidationResult
    {
        if ($this->name !== strtoupper($this->name)) {
            return ValidationResult::fieldError(
                field: 'name',
                value: $this->name,
                message: 'Name must be in uppercase.',
            );
        }

        return ValidationResult::valid();
    }
}

The ValidationResult class provides several factory methods:

Method Purpose
ValidationResult::valid() Indicates the object passed validation
ValidationResult::invalid($errors) Wraps one or more ValidationError instances
ValidationResult::fieldError($field, $value, $message) Shorthand for a single field error
ValidationResult::make($errors, $message) General-purpose constructor
ValidationResult::merge($results) Combines multiple validation results

When validation fails, Instructor feeds the error messages back to the LLM on retry, just like with Symfony constraints:

$user = (new StructuredOutput)
    ->withRuntime($runtime)
    ->with(
        messages: 'jason is 25 years old',
        responseModel: UserDetails::class,
    )
    ->get();

assert($user->name === 'JASON');

Custom Validation With Symfony #[Assert\Callback]

You can also use Symfony's #[Assert\Callback] attribute directly for full access to the Symfony validation context. This is useful when you want to leverage Symfony's violation builder API:

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

class UserDetails
{
    public string $name;
    public int $age;

    #[Assert\Callback]
    public function validateName(ExecutionContextInterface $context, mixed $payload): void
    {
        if ($this->name !== strtoupper($this->name)) {
            $context->buildViolation('Name must be in uppercase.')
                ->atPath('name')
                ->setInvalidValue($this->name)
                ->addViolation();
        }
    }
}

See the Symfony Callback constraint docs for more details on the violation builder API.

How Retries Work

When a response fails validation, Instructor:

  1. Collects all validation errors (from Symfony constraints, ValidationMixin, or both).
  2. Formats them into a retry prompt that describes what went wrong.
  3. Appends the retry prompt to the conversation history.
  4. Sends the updated conversation back to the model for another attempt.

This self-correction loop continues until validation passes or the retry limit is reached. The default retry prompt is "JSON generated incorrectly, fix following errors:\n", followed by the list of violations. You can customize it through StructuredOutputConfig:

use Cognesy\Instructor\Config\StructuredOutputConfig;

$config = new StructuredOutputConfig(
    maxRetries: 3,
    retryPrompt: 'The previous response had errors. Please correct them:',
);