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.

Validate Maybe Fields.

You can use all the existing validators with Maybe fields. The validator will only be applied when the field is not Nothing.

buildPost :: Post -> Post
buildPost post = post
    |> validateField #title nonEmpty
    -- Assuming sourceUrl is optional.
    |> validateField #sourceUrl (validateMaybe nonEmpty)

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.

Security Concerns and Conditional fill

It’s important to remember that any kind of validations you might have on the form level are not enough to ensure the security of your application. You should always have validations on the backend as well. The user might manipulate the form data and send invalid data to your application.

So if a field is disabled or has an integer field with a min/max value, you should always have a validation on the backend.

Another point is that you don’t have to always fill all fields in one go. Sometimes you’d like to conditionally fill based on the current user or based on the current logic.

Let’s see those examples in action. Let’s say we have a Comment record that has a postId that references a Post, a body field, and a moderation field allowing admin users to indicate if they are approved or rejected.

Here’s an excerpt from the Schema.sql:

-- Schema.sql

-- Add a moderation status column to the comments table
CREATE TYPE comment_moderation AS ENUM ('comment_moderation_pending', 'comment_moderation_approved', 'comment_moderation_rejected');

CREATE TABLE comments (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
    post_id UUID NOT NULL,
    body TEXT NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
    comment_moderation comment_moderation NOT NULL
);

Once we generate a controller for the Comment record, we’ll the buildComment function that is called from both the CreateCommentAction and the UpdateCommentAction:

-- Controller/Comments.hs

buildComment comment = comment
    |> fill @'["postId", "body", "commentModeration"]

We’ll start with the postId. Once a comment is refernecing a post it will never have the reference change. So it means we should fill it only upon creation.

buildComment comment = comment
    |> fill @'["body", "commentModeration"]
    |> fillIsNew
    where
        fillIsNew record =
            if isNew record
            -- Record is new, so fill the `postId`.
            then fill @'["postId"] record
            -- Otherwise, leave the record as is.
            else record

Next, imagine we have a currentUserIsAdmin indicating if the current user is an admin. We’d like to allow only admins to set the moderation status of a comment. So we’ll allow fill on the commentModeration field only in that case:

-- A fake implementation of `currentUserIsAdmin`.
-- Will return true if the user's email is `admin@example.com`.
currentUserIsAdmin :: (?context :: ControllerContext) => Bool
currentUserIsAdmin =
    case currentUserOrNothing of
        Just user -> user.email == "admin@example.com"
        Nothing -> False

buildComment comment = comment
    |> fill @'["body"]
    |> fillIsNew
    |> fillCurrentUserIsAdmin
    where
        fillIsNew record =
            if isNew record
            -- Record is new, so fill the `postId`.
            then fill @'["postId"] record
            -- Otherwise, leave the record as is.
            else record

        fillCurrentUserIsAdmin record =
            if currentUserIsAdmin
            then fill @'["commentModeration"] record
            else record

Let’s finish with a final example. Let’s assume there was also a score integer field between 1 - 5 that only the admin could set. As mentioned, we’d need to have a validation on the backend to ensure that the user didn’t manipulate the form data. And use fill to ensure that a non-admin user can’t set the score in the first place. Here’s the final code, where we conditionally fill the score field only if the user is an admin, and perform validation on it:

buildComment comment = comment
    |> fill @'["body"]
    |> fillIsNew
    |> fillCurrentUserIsAdmin
    where
        fillIsNew record =
            if isNew record
            -- Record is new, so fill the `postId`.
            then fill @'["postId"] record
            -- Otherwise, leave the record as is.
            else record

        fillCurrentUserIsAdmin record =
            if currentUserIsAdmin
            then fill @'["commentModeration", "score"] record
                    -- Make sure that star can be only between 1 and 5.
                    |> validateField #score (isInRange (1, 5))
            else record


currentUserIsAdmin :: (?context :: ControllerContext) => Bool
currentUserIsAdmin =
    case currentUserOrNothing of
        Just user -> user.email == "admin@example.com"
        Nothing -> False

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")].