Validation

Introduction

IHP provides a simple infrastructure for validating incoming data. This guide will tell you more about validating new and existing records, as well as how to use more complex validations with database access.

Quickstart

Setting up the controller

Let’s assume we have generated a Posts controller using the code generator. Our Post has a title and a body. The CreatePostAction looks like this:

    action CreatePostAction = do
        let post = newRecord @Post
        post
            |> buildPost
            |> ifValid \case
                Left post -> render NewView { .. }
                Right post -> do
                    post <- post |> createRecord
                    setSuccessMessage "Post created"
                    redirectTo PostsAction

This action is executed when a form like the one below is submitted:

module Web.View.Posts.New where
import Web.View.Prelude

data NewView = NewView { post :: Post }

instance View NewView where
    html NewView { .. } = [hsx|
        <h1>New Post</h1>
        {renderForm post}
    |]

renderForm :: Post -> Html
renderForm post = formFor post [hsx|
    {textField #title}
    {textField #body}
    {submitButton}
|]

Now let’s add some validation.

Adding Validation Logic

To make sure that the title and body are not empty, we can validateField ... nonEmpty like this:

    action CreatePostAction = do
        let post = newRecord @Post
        post
            |> buildPost
            |> validateField #title nonEmpty
            |> validateField #body nonEmpty
            |> ifValid \case
                Left post -> render NewView { post }
                Right post -> do
                    post <- post |> createRecord
                    setSuccessMessage "Post created"
                    redirectTo PostsAction

buildPost post = post
    |> fill @'["title", "body"]

The syntax to validate a record is always like this:

record
    |> validateField #fieldName validatorName

Common Validators

Here is a list of the most common validators:

Works with Text fields:

Works with ints:

You can find the full list of built-in validators in the API Documentation.

Fill Validation

When using fill, like |> fill @'["title", "body"], any error parsing the input is also added as a validation error.

E.g. when fill fills in an integer attribute, but the string "hello" is submitted, an error will be added to the record and the record is not valid anymore. The record attribute will then keep its old value (before applying fill) and later re-render in the error case of ifValid. This applies to all arguments read via fill. It’s very helpful when dealing with strings expected in a certain format, like date times.

As IHP is never putting raw strings into the records, without previously parsing them, it does not provide validators like Rails’s :only_integer.

Rendering Errors

When a post with an empty title is now submitted to this action, an error message will be written into the record. The call to |> ifValid will see that there is an error, and then run the Left post -> render NewView { post }, which displays the form to the user again. The form will be rendered, and errors will automatically pop up next to the form field.

The default form helpers like {textField #title} automatically render the error message below the field. Here is how this looks:

Validation Error Message Below Title Input

Validating An Email Is Unique

For example, when dealing with users, you usually want to make sure that an email is only used once for a single user. You can use |> validateIsUnique #email to validate that an email is unique for a given record.

This function queries the database and checks whether there exists a record with the same email value. The function ignores the current entity of course.

This function does IO, so any further arrows have to be >>=, like this:

action CreateUserAction = do
    let user = newRecord @User
    user
        |> fill @'["email"]
        |> validateIsUnique #email
        >>= ifValid \case
            Left user -> render NewView { .. }
            Right user -> do
                createRecord user
                redirectTo UsersAction

Case Insensitive Uniqueness

Usually emails like someone@example.com and Someone@example.com belong to the same person. You can use validateIsUniqueCaseInsensitive to ignore the case when checking for uniqueness:

action CreateUserAction = do
    let user = newRecord @User
    user
        |> fill @'["email"]
        |> validateIsUniqueCaseInsensitive #email
        >>= ifValid \case
            Left user -> render NewView { .. }
            Right user -> do
                createRecord user
                redirectTo UsersAction

For good performance in production it’s recommended to add an index on the column in your Schema.sql:

CREATE UNIQUE INDEX users_email_index ON users ((LOWER(email)));

Sharing Between Create and Update Action

Usually, you have a lot of the same validation logic when creating and updating a record. To avoid duplicating the validation rules, you can apply them inside the buildPost function. This function is used by the create as well as the update action to read in the form values.

Here is how this can look:

    action CreatePostAction = do
        let post = newRecord @Post
        post
            |> buildPost
            -- <------------ Here we removed the `validateField ...`
            |> ifValid \case
                Left post -> render NewView { post }
                Right post -> do
                    post <- post |> createRecord
                    setSuccessMessage "Post created"
                    redirectTo PostsAction

buildPost post = post
    |> fill @'["title", "body"]
    |> validateField #title nonEmpty -- <---------- Here we added them
    |> validateField #body nonEmpty

In case a validation should only be used for e.g. updating a record or creating a record, just keep it there in the action only and don’t move it to the buildPost function.

Creating a custom validator

You can just write your constraint like this:

nonEmpty :: Text -> ValidatorResult
nonEmpty "" = Failure "This field cannot be empty"
nonEmpty _ = Success

isAge :: Int -> ValidatorResult
isAge = isInRange (0, 100)

Then just call it like:

user |> validateField #age isAge`

Checking If A Record Is Valid

Use ifValid to check for validity of a record:

post |> ifValid \case
    Left post -> do
        putStrLn "The Post is invalid"
    Right post -> do
        putStrLn "The Post is valid"

This will call the Left post -> do ... block when the record is not valid.

You can also use it like this:

let message = post |> ifValid \case
    Left post -> "The post is invalid"
    Right post -> "The post is valid"

putStrLn message

Customizing Error Messages

Use withCustomErrorMessage to customize the error message when validation failed:

user
    |> fill @'["firstname"]
    |> validateField #firstname (nonEmpty |> withCustomErrorMessage "Please enter your firstname")

In this example, when the nonEmpty adds an error to the user, the message Please enter your firstname will be used instead of the default This field cannot be empty.

Use withCustomErrorMessageIO to customize the error message when using IO functions:

user
    |> fill @'["email"]
    |> withCustomErrorMessageIO "Email Has Already Been Used" validateIsUnique #email
    >>= ifValid \case
        Left user -> ...
        Right user -> ...

In this example, when the validateIsUnique function adds an error to the user, the message Email Has Already Been Used will be used instead of the default This is already in use.

Internals

IHP’s validation is built with a few small operations.

validateField

The primary operation is validateField #field validationFunction record.

This function does the following thing:

  1. Read the #field from the record
  2. Apply the validationFunction to the field value
  3. When the validator returns errors, store the errors inside the meta attribute of the record.

The validateField function expects the record to have a field meta :: MetaBag. This meta field is used to store validation errors.

Let’s say we have an example data type Post:

data Post = Post { title :: Text, meta :: MetaBag }

A call to validateField will result in the following:

let post = Post { title = "" , meta = def } -- def stands for default :)

post |> validateField #title nonEmpty
-- This will return:
--
-- Post {
--     title = "",
--     meta = MetaBag {
--         annotations = [
--             ("title", "This field cannot be empty")
--         ]
--     }
-- }

As you can see, the errors are tracked inside the MetaBag. When you apply another validateField to the record, the errors will be appended to the annotations list.

Validation Functions

A validation function is just a function which, given a value, returns Success or Failure "some error message".

Here is an example:

isColor :: Text -> ValidatorResult
isColor text | ("#" `isPrefixOf` text) && (length text == 7) = Success
isColor text = Failure "is not a valid color"

Calling isColor "#ffffff" will return Success. Calling isColor "something bad" will result in Failure "is not a valid color".

It might be useful to take a look at the definition of some more validation functions to see how it works. You can find them in the API Docs.

Attaching Errors To A Record Field

You can attach errors to a specific field of a record even when not validating. These errors will then also show up when rendering a form.

Here is an example:

post
    |> attachFailure #title "This error will show up"

This record now has a validation error attached to its title.

Attaching Errors with HTML

If you try to use HTML code within attachFailure, the HTML code will be escaped and not rendered as expected:

post
    |> attachFailure #title "Invalid value. <a href="https://example.com/docs">Check the documentation</a>"
    -- Link will not be clickable as the HTML is escaped

Use attachFailureHtml instead:

post
    |> attachFailureHtml #title [hsx|Invalid value. <a href="https://example.com/docs">Check the documentation</a>|]
    -- Link will work as expected, as it's HSX

Retrieving The First Error Message For A Field

You can also access an error for a specific field using getValidationFailure:

post
    |> validateField #name nonEmpty
    |> getValidationFailure #name

This returns Just "Field cannot be empty" or Nothing when the post has a title.

Retrieving All Error Messages For A Record

Access them from the meta :: MetaBag attribute like this:

record.meta.annotations

This returns a [(Text, Violation)], e.g. [("name", "This field cannot be empty")].