Validation
How it works
Handling server-side validation errors in Inertia works differently than a classic XHR-driven form that requires you to catch the validation errors from 422 responses and manually update the form's error state - because Inertia never receives 422 responses. Instead, Inertia operates much more like a standard full page form submission. Here's how:
First, you submit your form using Inertia. If there are server-side validation errors, you don't return those errors as a 422 JSON response. Instead, you redirect (server-side) the user back to the form page they were previously on, flashing the validation errors in the session.
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
redirect_to users_url
else
redirect_to new_user_url, inertia: { errors: user.errors }
end
end
private
def user_params
params.require(:user).permit(:name, :email)
end
endNext, any time these validation errors are present in the session, they automatically get shared with Inertia, making them available client-side as page props which you can display in your form. Since props are reactive, they are automatically shown when the form submission completes.
Finally, since Inertia apps never generate 422 responses, Inertia needs another way to determine if a response includes validation errors. To do this, Inertia checks the page.props.errors object for the existence of any errors. In the event that errors are present, the request's onError() callback will be called instead of the onSuccess() callback.
Sharing errors
In order for your server-side validation errors to be available client-side, your server-side framework must share them via the errors prop. Inertia's Rails adapter does this automatically.
Displaying errors
Since validation errors are made available client-side as page component props, you can conditionally display them based on their existence. Remember, when using Rails server adapter, the errors prop will automatically be available to your page.
<script setup>
import { reactive } from 'vue'
import { router } from '@inertiajs/vue3'
defineProps({ errors: Object })
const form = reactive({
first_name: null,
last_name: null,
email: null,
})
function submit() {
router.post('/users', form)
}
</script>
<template>
<form @submit.prevent="submit">
<label for="first_name">First name:</label>
<input id="first_name" v-model="form.first_name" />
<div v-if="errors.first_name">{{ errors.first_name }}</div>
<label for="last_name">Last name:</label>
<input id="last_name" v-model="form.last_name" />
<div v-if="errors.last_name">{{ errors.last_name }}</div>
<label for="email">Email:</label>
<input id="email" v-model="form.email" />
<div v-if="errors.email">{{ errors.email }}</div>
<button type="submit">Submit</button>
</form>
</template>NOTE
When using the Vue adapters, you may also access the errors via the $page.props.errors object.
Repopulating input
While handling errors in Inertia is similar to full page form submissions, Inertia offers even more benefits. In fact, you don't even need to manually repopulate old form input data.
When validation errors occur, the user is typically redirected back to the form page they were previously on. And, by default, Inertia automatically preserves the component state for post, put, patch, and delete requests. Therefore, all the old form input data remains exactly as it was when the user submitted the form.
So, the only work remaining is to display any validation errors using the errors prop.
Error bags
NOTE
Error bags are a Laravel-specific feature that relies on Laravel's ViewErrorBag system. In Rails, this feature is typically unnecessary because:
- Forms submit to separate controller actions, so only one set of errors is returned per request
- The form helper automatically scopes validation errors to each form instance
If you have multiple forms on a page, use the useForm() helper and each form will maintain its own isolated error state.
Precognition
Precognition enables real-time validation of form data without executing the full controller action. When using Inertia's useForm() helper with the precognitive option, validation requests are sent to your server with special headers, and the server responds with validation results immediately.
Basic usage
Use precognition! or precognition in your controller to handle precognition requests:
class UsersController < ApplicationController
def create
@user = User.new(user_params)
precognition!(@user)
if @user.save
redirect_to @user
else
redirect_back_or_to new_user_path, inertia: { errors: @user.errors }
end
end
endTwo controller methods are available:
precognition!(model_or_errors)— raises an exception to halt the action on precognition requests. Noreturn ifneeded — the action simply stops. For non-precognition requests, returnsfalseand continues normally.precognition(model_or_errors)— returnstrueif a precognition response was rendered,falseotherwise. Use withreturn if precognition(@user)if you prefer the explicit return pattern.
Both methods accept an ActiveModel-like object (calls valid? automatically) or an errors hash:
- For valid data, responds with
204 No ContentwithPrecognition: trueandPrecognition-Success: trueheaders - For invalid data, responds with
422 Unprocessable Entitywith errors as JSON and aPrecognition: trueheader
One call per action
You can only call precognition! or precognition once per controller action. Calling it a second time raises InertiaRails::DoublePrecognitionError. This is intentional — precognition validates a single form submission, so there should be exactly one validation point per action.
If you need to validate multiple models, use a form object that combines all validations into a single valid? call:
# Bad — raises DoublePrecognitionError
def create
precognition!(@user)
precognition!(@profile) # Error!
end
# Good — validate everything in one call
def create
form = RegistrationForm.new(params)
precognition!(form) # Validates user + profile together
endModule-level API
InertiaRails.precognition! works the same way as the controller method but can be called from anywhere in the request cycle — form objects, service objects, or any Ruby code:
InertiaRails.precognition!(@user)This is useful when you want to handle precognition outside the controller.
Form objects
When your form doesn't map directly to a single model, you can create a plain Ruby class with validations using ActiveModel::API and ActiveModel::Attributes. This gives you the same validation interface as a model:
class RegistrationForm
include ActiveModel::API
include ActiveModel::Attributes
attribute :name, :string
attribute :email, :string
attribute :company_name, :string
validates :name, :email, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :company_name, presence: true
def save
InertiaRails.precognition!(self)
return false unless valid?
# Create user, company, etc.
end
endSince InertiaRails.precognition! works from anywhere, form objects handle precognition themselves — no controller changes needed:
class RegistrationsController < ApplicationController
def create
form = RegistrationForm.new(params)
if form.save
redirect_to form.user
else
redirect_back_or_to new_registration_path, inertia: { errors: form.errors }
end
end
endOther validation libraries
For libraries like dry-validation, pass the errors hash:
class UsersController < ApplicationController
def create
result = UserContract.new.call(user_params.to_h)
precognition!(result.errors.to_h)
if result.success?
@user = User.create!(result.to_h)
redirect_to @user
else
redirect_back_or_to new_user_path, inertia: { errors: result.errors.to_h }
end
end
endPreventing side effects
Since precognition requests reuse your existing controller actions, it's important to place the precognition! call before any side-effect-producing code (saving records, sending emails, calling external APIs, enqueuing jobs). The precognition! method halts the action on precognition requests, so any code after it only runs during real form submissions.
You can also enable precognition_prevent_writes to automatically block database writes during precognition requests as an extra safety net.
Client-side setup
For detailed client-side usage of precognition with the <Form> component and useForm helper, see the forms documentation.
Field-specific validation
Inertia's client-side form helper can request validation of specific fields using the Precognition-Validate-Only header. The server automatically filters the errors to only include the requested fields.
Using transform with precognition
When using the <Form> component with a transform prop that wraps data under a key (e.g., transform={(data) => ({ user: data })}), the field names passed to validate() must match the keys in the transformed data structure, not the input name attributes.
This is because validate('name') looks up the value using the field name in the transformed data. If the transform wraps inputs under user, the transformed data is { user: { name: '...' } }, and validate('name') won't find a value at the top level.
To work around this, you have two options:
Option 1: Use dotted input names instead of a transform
Use name="user.name" (or name="user[name]") input attributes. The <Form> component automatically converts these into a nested { user: { name: '...' } } structure without needing a transform, so validate('user.name') will correctly find the value. Note that server error keys must match (e.g., user.name), so you may need to prefix errors in your precognition response.
Option 2: Use useForm with withPrecognition instead
The useForm helper tracks data internally, so validate('name') always works regardless of transforms:
const form = useForm({
name: '',
email: '',
}).withPrecognition('post', '/users')
// The transform only applies to the submitted data, not the validate() lookup
form.transform((data) => ({ user: data }))