Validating Grails Forms with Command Objects
HTML form data is untrusted input, even when the browser has required fields and the page looks correct. Grails provides a useful boundary between transport and persistence: a command object receives submitted parameters, validates them, and supplies accepted values to a service or domain class. Keeping this boundary clear prevents a convenient form submission from becoming unrestricted object binding.
A command object is usually a small Groovy class with typed properties and a constraints closure. Because it is not persisted, it can carry form-specific data such as a workshop code or attendee count without exposing the database model to mass assignment. Its rules can be exercised in isolation, which is valuable when a Java developer is learning Grails conventions.
That separation is useful for an Australian training business running workshops in Sydney and Melbourne. The same booking form may need to show local dates, take prices in AUD, and display GST at 10%, while remaining usable under en_AU and other locales. Command objects let the application enforce input rules once while the view handles regional presentation.
Build a dedicated command object
Start by naming the form’s purpose and selecting property types that represent what the browser sends. For a workshop enquiry, the command can live under grails-app/controllers in many Grails applications and can be passed directly to a controller action. It remains a plain Groovy object rather than a Workshop domain class.
class WorkshopBookingCommand {
String name
String email
String workshopCode
String organisationName
Integer attendees
Date preferredDate
}
Grails can use WorkshopBookingCommand as an action parameter and bind matching request parameters before the action body runs. A property absent from the command is ignored, so fields such as totalAmount should never be added merely because the browser sends them in a hidden input. Pricing, permissions and other server-controlled values belong in trusted application code.
Choose types carefully and keep defaults on the server. A missing attendee count can remain null so that a nullable constraint rejects it; coercing every malformed value to zero hides the original problem. For dates, decide whether typed Date binding is suitable or retain the submitted string for strict parsing. Either approach needs tests for empty, malformed and valid input.
Define constraints that match the form
The constraints closure declares the basic rules that every request must satisfy. Keep these rules explicit so another developer can understand the accepted input without reading the controller or view.
static constraints = {
name(nullable: false, blank: false, maxSize: 80)
email(nullable: false, blank: false, email: true)
workshopCode(nullable: false, blank: false)
organisationName(nullable: true, blank: true)
attendees(nullable: false, range: 1..12)
preferredDate(nullable: true)
}
nullable controls whether a property may be absent, while blank handles strings containing only whitespace. Length, range and email constraints reject values that have the wrong shape. Constraint blocks are useful for local rules, but an inList constraint is appropriate only for a small, stable set; a list of workshop codes loaded from a catalogue should be checked against current server-side data.
Grails stores failed constraints in the command object’s errors collection. Each error can carry a field name and message code, allowing a view to place the right message beside the relevant input. Avoid replacing validation with scattered if statements, since that makes the accepted rules harder to see and leaves some code paths without error metadata.
Bind, validate and redisplay safely
A controller should validate the command before passing it to a service. If binding or validation fails, render the original form with the same object so the user can correct it. Return immediately after the failed render to prevent later lines from processing invalid data.
def saveWorkshopBooking(WorkshopBookingCommand command) {
if (!command.validate()) {
render view: 'book', model: [bookingCommand: command]
return
}
bookingService.createBooking(command)
flash.message = 'Your workshop place is confirmed.'
redirect(action: 'confirmation')
}
The GSP should use values already held by the command rather than reading request parameters directly. This preserves ordinary entries after a failed submission and creates one predictable source for both values and errors.
<g:form controller="booking" action="saveWorkshopBooking">
<g:textField name="name" value="${bookingCommand?.name}" />
<g:textField name="email" value="${bookingCommand?.email}" />
<g:renderErrors bean="${bookingCommand}" as="list" />
<g:submitButton name="submit" value="Book workshop" />
</g:form>
After a successful save, use redirect rather than rendering the confirmation page directly. This post-redirect-get pattern reduces duplicate submissions when a browser refreshes the response. It also separates the action that processes the form from the page that confirms the completed booking, making retries and browser history easier to reason about.
Add cross-field and business validation
Some rules depend on more than one property or on data outside the request. For example, a private workshop requires an organisation name, while a public workshop does not. Put that check in a validator attached to a required property and return a message code rather than a complete sentence.
workshopCode(nullable: false, blank: false, validator: { String value, WorkshopBookingCommand booking ->
if (value == 'PRIVATE' && !booking.organisationName?.trim()) {
return ['booking.company.required']
}
})
Define booking.company.required in the application’s message bundle. Returning a stable code keeps wording out of the validation logic and gives translators a clear unit to work with. Property-level validators are also suitable for date ranges, confirmation fields and other rules involving the current command object.
Checks that require current data—workshop existence, remaining capacity, duplicate bookings or cancellation status—usually belong in a service running within a transaction. A concurrent request can invalidate a result obtained earlier, so retain suitable database constraints as a final guard. A more detailed internationalisation and localisation setup also helps keep translated validation messages separate from controller code.
Locale handling deserves its own tests. An en_AU page can display a preferred date as dd/MM/yyyy, but validation must parse that value according to a declared format rather than guessing from the browser. Ambiguous values such as 03/04/2026 should not be silently interpreted differently across routes or environments.
Return useful, secure error responses
An HTML response should provide an error summary and field-level messages that remain understandable when JavaScript is unavailable. Connect each message to its input with stable IDs and attributes such as aria-describedby. For JSON endpoints, return a consistent structure containing field names, codes and safe messages; an invalid submitted document can use HTTP status 422.
Never return stack traces, SQL details, internal class names or the contents of unrelated domain objects. Preserve useful submitted values, but blank password and payment fields before redisplaying or logging a form. Grails form tags encode ordinary field values, so avoid marking untrusted content as safe HTML, and retain the configured CSRF protection for state-changing requests.
Treat hidden inputs as hints rather than trusted records. Resolve a workshop code through the service, calculate its AUD price on the server, and add GST according to the business rule rather than accepting a total from the browser. A shipping quotation can similarly validate an Australian four-digit postcode before asking Australia Post for eligible services, while still allowing international postcodes when the service supports them.
Put validation into practice
Test the command object as a domain boundary. Unit tests are fast because they do not require a running server, and they make each expected failure obvious. A Spock specification can assert both the validation result and the field carrying the error.
void 'rejects an invalid email address'() {
def command = new WorkshopBookingCommand(
name: 'Alex',
email: 'not-an-email',
workshopCode: 'GRAILS',
attendees: 2
)
assert !command.validate()
assert command.errors.getFieldError('email')
}
Extend the suite with empty required values, maximum lengths, numeric boundaries, malformed dates, conditional fields and tampered identifiers. Controller tests should confirm that invalid input returns to the form, valid input reaches the service and a successful submission redirects. Service tests should cover capacity and duplicate-booking rules that cannot be established by the command object alone.
Coverage can expose branches that no test reaches, and this guide to JaCoCo coverage reports explains how to generate and inspect a Grails report. Use the results to find gaps, not as the only measure of quality; a high percentage cannot replace assertions about security, accessibility and actual booking behaviour. Run the complete suite in continuous integration whenever constraints, messages or form bindings change.
Create the command object for your next Grails form, exercise its invalid cases through tests, and verify the complete booking flow before deployment.