Forms

Forms #

Introduction #

Forms are generally how user data flows into and out of your application. Typically, the forms that you present to your users are representative of your domain models and data transfer objects.

Usually, processing data submitted by users entails the following steps:

  • Getting all the form parameters from the request
  • Creating an instance of your Data Transfer Object
  • Copying all of the form parameters from the request to the domain model
  • Performing validations (and notifying the user of any errors)
  • Finally, using the data

That’s quite a lot! This process can be tedious and error prone, but luckily working with forms in Diego only entails a few easy steps.

Step 1: Define a model #

First, we define a model. A model is any domain object with properties (and optional constraints). For our example, we will be working with the following model:

import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.AssertTrue
import org.hibernate.validator.constraints.Length

class SignupForm {
    @NotBlank String email
    @NotBlank 
    @Length(min = 8, message = 'must be at least 10 characters') 
    String password
    @NotBlank String passwordConfirm
}
In Groovy, fields declared like this are given getters and setters automatically. If you prefer to use Java, you'll have to implement these according to JavaBean Conventions. This is essential for the next step.

Note the special @NotBlank annotation. This annotation serves as a constraint and is part of the wider Jakarta EE Validation package. In our example, this constraint ensures that the email and password fields do not contain null values or empty strings.

Step 2: Parse the request #

Diego provides the diego.validation.Form class to parse request bodies and request parameters and perform validation using the Hibernate Validator.

The following example shows how this is done:

app.post("/login", (req, res) -> {
    SignupForm form = Form.create(SignupForm).parse(req) // Parse the request using the SignupForm class as a schema
    // Use the data in the form...
})

Here, the request is parsed and a SignupForm object is returned. This object is populated according to the data in the request.

If there is a parsing error, Diego will throw a FormValidationException. You can catch this exception yourself, or rely on Diego’s default handling which will return an HTTP 400 (Bad Request) response with the following body:

{
    "statusCode": 400,
    "message": "Invalid data submitted",
    "errors": { [key]: ["Violation message 1", "Violation message n"] }
}

The Form#parse(HttpServerRequest) method works for both JSON requests and regular HTML form submissions (URL encoded and multipart). For JSON requests, the shape of the JSON object must match the shape of the model, but for HTML forms, the form parameters must map to properties of the model according to the established naming conventions.

Step 3: Use the data #

You’re done! Use the data in your SignupForm to complete you business logic.

Custom validation #

The JSR 303 spec features @AssertTrue and @AssertFalse annotations that can be leveraged to perform some custom validation logic.

Let’s use them to add some extra validation logic to our signup form:

import jakarta.validation.constraints.AssertTrue

/* Other boilerplate */

@AssertTrue(message = 'Password and confirmation do not match')
private boolean getPasswordMatch() {
    return password == passwordConfirm
}

@AssertTrue(message = 'The email address is already in use')
private boolean isEmailAvailable() {
    return checkEmailExists(email)
}

Note that the return type is boolean not Boolean and the method name begins with “is” or “get”. This is to align with the JavaBeans specification (Section 8.3.2).

Custom constraints #

You can create a custom validation constraints by following this guide.

TODO: Validation Groups #

Modify the Form class to allow the use of Hibernate Validation Groups

File uploads and forms #

You can include file uploads in your forms as well. Simply create a FileUpload field that also adheres to the naming conventions.

class ProfileForm {
    @Required @MimeType('image/*') FileUpload profilePic
}

Here, we use the diego.validation.constraints.Required and diego.validation.constraints.MimeType constraints to ensure that the profilePic field is required and is an image type.

Naming conventions for HTML forms #

When submitting data using regular HTML forms, the form.parse() method uses the built-in data mapper to inspect the form parameters and map them to properties on the supplied class. This mapping will only be successful if the form parameter names adhere to certain conventions.

Regular properties #

<!-- Set the fullName attribute to the fullName property. Use camelCase -->
<input type="text" name="fullName" />

Lists #

<!-- Use square bracket notation for collection items -->
<input type="checkbox" name="interests[]" value="swimming" />
<input type="checkbox" name="interests[]" value="reading" />
<input type="checkbox" name="interests[]" value="biking" />

Nested objects #

<!-- Use dot notation to map to fields in nested objects. -->
<input type="text" name="address.street" />    
<input type="text" name="address.town" />

Nested Lists #

<!-- Use a combination of dot notation and square brackets to map to nested lists. -->
<input type="checkbox" name="building.features[]" value="balcony" />
<input type="checkbox" name="building.features[]" value="pool" />
<input type="checkbox" name="building.features[]" value="office" />

Limitations #

Presently, the data binder cannot represent lists of objects or nested lists of objects.

<input type="text" name="customers[0].name" /> <!-- This will throw an error. -->

This limitations will be addressed in a future release, although the infrequent use of URL encoded parameters for data transfer makes this a low priority item.

This limitation does not apply when using JSON requests.

Further reading #

If you like, you can read up more about the JSR 303 specification.

Next: Handling file uploads