Friday, November 05, 2010

Web Forms (1): Validation

This post is more of a note-to-self than anything. Part one of a two-parter, concentrating on web form processing – first from a server-side point of view, and then later, from a user-centric client-side point of view. The aim is to document a pattern for forms processing that is consistent and repeatable for any website. Think PRG (Post-Redirect-Get), with some server-side detail added in for good measure. PRG+.

In this post, I just want to focus on server-side data validation, and how to deal with the different types of invalid data entry. The first point to note is that you should never make any assumption about the origin of the data. You must ignore any notion of client-side validation – the data that your controller receives as the request could have come from anywhere, and may contain anything. Data is sent over the wire as text key-value pairs, and should therefore go through a number of validation steps:

  • Checking the data type – everything arrives as a string over the wire, so needs checking
  • Simple data type logic – is an email in a valid format, is a date of birth set in the past
  • Domain logic – is an email / username a duplicate, does a product exist, is the price ‘right’?

Only at this point can you attempt to process the form with any confidence.

When an rule fails, there are a couple of different ways in which the user can be notified:

  • Return to the form, and allow them to amend the invalid data
  • Proceed to a new page, with appropriate messaging (e.g. PRG)

I will go into more detail around this side of the process in the next post (when to redirect, how to notify users etc.), however one thing I would highlight here is the difference between recoverable and unrecoverable errors: if the data fails validation before any changes are made server-side, then I believe the user should be alerted via a warning message and not an error message. Errors should indicate that something went wrong, and if a form fails validation then the user should always have the option to amend their data and resubmit it. In project management terminology, warnings indicate a risk (something that could go wrong if mitigating action is not taken), whilst errors indicate an issue – something that has gone wrong already.

I also believe that there is a difference between the simple data validation and the contextual business domain validation, and that only the first type should ever be replicated client-side (ignoring AJAX for now – more of that next time). In an MVC world, I think the first type can be done by the controller, and that this validation should match client-side validation, whilst the second type should be done deeper into the model and / or application domain. (Assuming that the controller is just that – a controller – and that it delegates the ‘doing’ to other components.)

Below is some pseudo-code demonstrating what I believe** to be the ideal processing and validation for a sample form request handler.

ProcessForm(request)

    /* first do some 'dumb' data type validation of input values – remember
    that all request values are passed over the wire as text, so they need to
    be validated according to the basic destination data type. At this point
    client-side validation should be ignored – we don’t know that the information
    was submitted using the form – we simply know which controller was called. */
    if (! isEmail(request.email))
        view-data.errors.add(new InvalidPropertyException("email"))
    if (! isZipcode(request.zip))
        view-data.errors.add(new InvalidPropertyException("zipcode"))
   
if (! isDate(request.dob))
        view-data.errors.add(new InvalidPropertyException("dob"))
    etc.
 
        
    /* if we have any errors so far then don't bother continuing, return to the
    form and prompt the user for new values. This is WARNING, and not an ERROR,
    as nothing has been changed server-side, and the user can always amend the
    values and resubmit. This is the server-side equivalent of client-side JS
    validation. */ 
    if (view-data.hasErrors)
        returnToFormAndHighlightIssues()

    /* if we get here then we know that the values are 'correct' but that doesn't
   
mean they will work. This next validation step may require more context than
    the simple
validation above – it is not something that can (or should) be 
    replicated in client-side JS. e.g. is the price in the acceptable range, is the
    username available. */
    try
    {
        model = new model(request.email, request.zip, request.dob, request.price)
        doSomethingWithModel(model)
    } 
   
    /* exception thrown by any property setter that doesn't like the value it's
    given; this is functionally equivalent to the case above - nothing has really
    happened so log this as a WARNING, and not an ERROR */

    catch (InvalidPropertyException)
    {
        view-data.errors.add(theException)
        returnToFormAndHighlightIssues()
    } 
    
    /* exception thrown by the doSomethingWithModel method that occurs BEFORE
    anything has been committed, and whilst there is the opportunity to resubmit
    the data. An example of this might be an attempt to register a duplicate
    username; this can be marked as a WARNING or an ERROR, depending on context.*/
    catch (RecoverableException)
    {
        view-data.errors.add(theException)
        returnToFormAndHighlightErrors()
    } 
    
    /* exception thrown by the doSomethingWithModel method after data has been
    irreversibly committed. In this case resubmitting the data is not desirable,
    and the user should be alerted! An example of this might be a database
    exception after the record has been partially committed. */
    catch (UnrecoverableException, AnyOtherUnexpectedException)
    {
        view-data.errors.add(theException)
        renderDifferentPageWithMoreInformation()
    }

    // phew - if we got here it all went well, so we render the anticipated page
    renderExpectedPage()
}

** It would be fair to say that my views on this aren’t universally accepted – comments welcome.

No comments: