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:
- Collects all validation errors (from Symfony constraints,
ValidationMixin, or both). - Formats them into a retry prompt that describes what went wrong.
- Appends the retry prompt to the conversation history.
- 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: