Validation

How it works

Handling server-side validation errors in Inertia works a little different than a classic ajax-driven form, where you catch the validation errors from 422 responses and manually update the form's error state. That's because Inertia never receives 422 responses. Rather, Inertia operates much more like a standard full page form submission. Here's how:

First, you submit your form using Inertia. In the event that there are server-side validation errors, you don't immediately return those errors as a 422 JSON response. Instead, you redirect (server-side) back to the form page you are on, flashing the validation errors in the session. Frameworks like Laravel do this automatically.

Next, 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.

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. Some adapters, such as the Laravel adapter, do this automatically. For others, you may need to do this manually. Please refer to your specific server-side adapter documentation for more information.

class UsersController extends Controller
{
    public function create()
    {
        return Inertia::render('Users/Create');
    }

    public function store()
    {
      Request::validate([
          'first_name' => ['required', 'max:50'],
          'last_name' => ['required', 'max:50'],
          'email' => ['required', 'max:50', 'email'],
      ]);

      $user = User::create(
        Request::only('first_name', 'last_name', 'email')
      );

      return Redirect::route('users.show', $user);
    }
}

Displaying errors

Since validation errors are made available client-side as page component props, you can conditionally display them based on their existence. Here's an example how.

<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>

<script>
export default {
  props: {
    errors: Object,
  },
  data() {
    return {
      form: {
        first_name: null,
        last_name: null,
        email: null,
      },
    }
  },
  methods: {
    submit() {
      this.$inertia.post('/users', this.form)
    },
  },
}
</script>

Note, in the Vue adapters, you can also access the errors via the $page.props.errors object.

Repopulating input

While handling errors in Inertia is similar to full page form submissions, this approach is actually much nicer, since you don't need to manually repopulate old form input data.

When validation errors occur, the user is automatically redirected back to the form page they are already on. And, by default, Inertia automatically preserves the component state for post, put, patch, and delete requests. Meaning, all the old form input data remains exactly as it is.

The only thing that changes is the errors prop, which now contains the validation errors.

Resolving errors

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 onError() callback will be called instead of the onSuccess() callback.

If your application shares errors using a different prop than errors, you must tell Inertia how to resolve these errors. This can be done using the resolveErrors() callback, applied at the adapter level.

/*
|----------------------------------------------------------------
| Vue 3
|----------------------------------------------------------------
*/

import { createApp, h } from 'vue'
import { App, plugin } from '@inertiajs/inertia-vue3'

const el = document.getElementById('app')

createApp({
  render: () => h(App, {
    initialPage: JSON.parse(el.dataset.page),
    resolveComponent: name => require(`./Pages/${name}`).default,
    resolveErrors: page => (page.props.errors || {}),
  })
}).use(plugin).mount(el)


/*
|----------------------------------------------------------------
| Vue 2
|----------------------------------------------------------------
*/

import { App, plugin } from '@inertiajs/inertia-vue'
import Vue from 'vue'

Vue.use(plugin)

const el = document.getElementById('app')

new Vue({
  render: h => h(App, {
    props: {
      initialPage: JSON.parse(el.dataset.page),
      resolveComponent: name => require(`./Pages/${name}`).default,
      resolveErrors: page => (page.props.errors || {}),
    },
  }),
}).$mount(el)

Note, the resolveErrors() callback must always return an object, even if there are no errors.

Error bags

For pages that have more than one form, it's possible to run into conflicts when displaying validation errors if two forms share the same field names. For example, imagine a "create company" form and a "create user" form that both have a name field. Since both forms will be displaying the page.props.errors.name validation error, generating a validation error for the name field in either form will cause the error to appear in both forms.

To get around this, you can use error bags. Error bags scope the validation errors returned from the server within a unique key specific to that form. Continuing with our example above, you might have a createCompany error bag for the first form, and a createUser error bag for the second form.

Inertia.post('/companies', data, {
  errorBag: 'createCompany',
})

Inertia.post('/users', data, {
  errorBag: 'createUser',
})

Doing this will cause the validation errors to come back from the server as page.props.errors.createCompany and page.props.errors.createUser.

Note, if you're using the form helper, it's not necessary to use error bags, since validation errors are automatically scoped to the form object.