Using Command Objects To Handle Form Data

This guide will explain how you can use command objects to validate input data in a Grails application

Authors: Matthew Moss, Sanjana

Grails Version: 8

1 Getting Started

In this guide you use command objects to process form submissions with automatic type conversion, custom validation, and simple error handling on Apache Grails 8.

Work in the initial/ project and compare your progress with complete/.

1.1 What you will need

  • Approximately 45 minutes

  • JDK 21 (Apache Grails 8 requires Java 21)

  • A text editor or IDE

  • The Gradle wrapper bundled in initial/ and complete/

1.2 How to complete the guide

Clone the companion and start from initial/:

git clone -b grails8 https://github.com/grails-guides/command-objects-and-forms.git
cd command-objects-and-forms/initial
./gradlew bootRun

initial/ already includes the Player domain, scaffold views, and sample data. You will add PlayerController and the PlayerInfo command object.

To skip ahead, use complete/ instead of initial/. Both trees must pass ./gradlew test integrationTest (see .github/workflows/grails8.yml).

2 What are command objects?

As described in the Grails documentation,

A command object is a class that is used in conjunction with data binding, usually to allow validation of data that may not fit into an existing domain class. A class is only considered to be a command object when it is used as a parameter of an action.

A domain class can be used as a command object. So can non-domain classes defined in other files, or even classes defined in the same file as the controller.

3 Writing the Application

This guide walks through a controller whose actions use command objects so form input is bound, validated, and accepted only for the fields you expose.

3.1 Create the controller

Before we can create any actions using command objects, we need a controller. Create grails-app/controllers/demo/PlayerController.groovy with index and show actions that work with the existing Player domain:

grails-app/controllers/demo/PlayerController.groovy
package demo

import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
import org.springframework.http.HttpStatus

@ReadOnly
class PlayerController {

    def index() {
        respond Player.list(params), model: [playerCount: Player.count()]
    }

    def show(Player player) {
        respond player
    }
}

The domain class, views, and sample data are already in initial/. Run the application and open http://localhost:8080/player/ to see a list of players, and click a player’s name to view that individual player. Create, save, edit, and update are not wired yet.

At any point, you can start the application by typing ./gradlew bootRun on the command line; it is running and ready when you see:

Grails application running at http://localhost:8080 in environment: development

Stop the application by pressing Ctrl-C.

3.2 Implement the create and save actions

Next, add the ability to create new players. To PlayerController, add the create action.

grails-app/controllers/demo/PlayerController.groovy
def create() {
    respond new Player(params)
}

While this action does make use of the Player class, the instance of Player is not a command object since it is not an argument to the action. That should make sense: the intent is to render an empty form, so there is no submitted input to bind yet.

From the Player List (http://localhost:8080/player/index), click New Player to see the create form (http://localhost:8080/player/create). If you fill in the form and click Create now, you get a 404 Page Not Found error, because save is not defined yet. Add that next.

    def save(Player player) {
        // ...
    }

Here, player is a command object, because it is an argument to the action. Behind the scenes, Grails rewrites the action to fetch existing records (if necessary), bind input data to the command object, perform dependency injection, and validate the object.

The best feature here is data binding. The create view is already in initial/. Grails 8 scaffolding uses the Fields plugin: f:all renders an input for every Player property.

grails-app/views/player/create.gsp
<g:form action="save">
    <fieldset class="form">
        <f:all bean="player"/>
    </fieldset>
    <fieldset class="buttons">
        <g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
    </fieldset>
</g:form>

The generated input name attributes match the properties of the domain class Player.

grails-app/domain/demo/Player.groovy
package demo

class Player {
    String name
    String game
    String region
    int wins = 0
    int losses = 0

    static constraints = {
        name blank: false, unique: true
        game blank: false
        region nullable: true
        wins min: 0
        losses min: 0
    }
}

Data binding takes the form-submitted string values for name, game, and region (if set) and sets those properties on the command object player. The submitted strings for wins and losses are converted to int. The convention of using the same names for domain properties and input fields is what keeps this code small: view fields are pre-filled from domain objects, and command objects are filled from submitted form data.

After binding, domain constraints run. That is the place to add error handling. The HttpStatus import is already at the top of the controller from the index / show step. Update the save action:

grails-app/controllers/demo/PlayerController.groovy
@Transactional
def save(Player player) {
    if (player == null) {
        render status: HttpStatus.NOT_FOUND
        return
    }

    if (player.hasErrors()) {
        respond player.errors, view: 'create'
        return
    }
}

The generated GSPs do as much client-side checking as they can. To see server-side validation, use curl while the app is running. The create form posts application/x-www-form-urlencoded (not multipart), so match that:

curl --request POST \
  --header "Accept: application/json" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Bob+Smith&wins=42&losses=abc" \
  "http://localhost:8080/player/save.json"

You should receive HTTP 422 Unprocessable Entity with a type-mismatch on losses (displayed nicely here):

{"errors": [
  {"object": "demo.Player",
   "field": "losses",
   "rejected-value": "abc",
   "message": "Property losses is type-mismatched"}
]}

Grails 8 reports the type-mismatch for this request. game is also required (blank: false), but a conversion error on another field is what this particular payload surfaces.

We are going to verify this with a functional test.

Functional Tests involve making HTTP requests against the running application and verifying the resultant behaviour.

The companion uses JDK java.net.http.HttpClient (no extra HTTP-client dependency). Successful form posts follow request.withFormat and redirect, so the spec disables automatic redirect following and then GETs the JSON show representation.

src/integration-test/groovy/demo/PlayerControllerFuncSpec.groovy
package demo

import grails.testing.mixin.integration.Integration
import groovy.json.JsonSlurper
import spock.lang.Specification

import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

@Integration
class PlayerControllerFuncSpec extends Specification {

    HttpClient client = HttpClient.newBuilder()
            .followRedirects(HttpClient.Redirect.NEVER)
            .build()

    void 'test save validation'() {
        given:
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:${serverPort}/player/save.json")) (1)
                .header('Accept', 'application/json') (2)
                .header('Content-Type', 'application/x-www-form-urlencoded') (3)
                .POST(HttpRequest.BodyPublishers.ofString('name=Bob+Smith&wins=42&losses=abc'))
                .build()

        when:
        HttpResponse<String> resp = client.send(request, HttpResponse.BodyHandlers.ofString())
        Map body = new JsonSlurper().parseText(resp.body()) as Map
        List errors = body.errors as List

        then:
        resp.statusCode() == 422 (4)
        errors.find { it.field == 'losses' }.message == 'Property losses is type-mismatched'
    }
}
1 serverPort is injected. It is the random port where the application runs during the functional test.
2 If your client accepts JSON, set the Accept header.
3 This POST is form-urlencoded, the same Content-Type as the create form. Set it explicitly.
4 Failed command-object validation returns 422 Unprocessable Entity.

This first-stage spec is enough to run ./gradlew integrationTest after save exists. The next chapter adds a second test for mass-assignment protection.

Now that binding, validation, and error-checking are in place, complete save so it persists the player and responds to the form:

grails-app/controllers/demo/PlayerController.groovy
@Transactional
def save(Player player) {
    if (player == null) {
        render status: HttpStatus.NOT_FOUND
        return
    }

    if (player.hasErrors()) {
        respond player.errors, view: 'create'
        return
    }

    player.save flush: true

    request.withFormat {
        form multipartForm { redirect player }
        '*' { respond player, status: HttpStatus.CREATED }
    }
}

You should now be able to create and save new players.

The example controller code here is kept small on purpose so the guide can focus on command objects. For a fuller starting point in your own apps:

./grailsw generate-controller demo.Player

3.3 Implement the edit and update actions

Next, add the ability to update existing players. Add edit and update actions to PlayerController. These look almost the same as create and save.

    def edit(Player player) {
        respond player
    }

    @Transactional
    def update(Player player) {
        if (player == null) {
            render status: HttpStatus.NOT_FOUND
            return
        }

        if (player.hasErrors()) {
            respond player.errors, view: 'edit'
            return
        }

        player.save flush: true

        request.withFormat {
            form multipartForm { redirect player }
            '*' { respond player, status: HttpStatus.OK }
        }
    }

The edit action starts with a Player command object; Grails fetches the existing Player, and those properties are the initial values of the form in edit.gsp. The update action is the same as save, except that errors re-render edit rather than create, and the HTTP status for success is OK rather than CREATED.

On the assumption that a player’s win and loss counts are managed elsewhere, wins and losses have already been removed from edit.gsp. The Fields plugin is used with an explicit property list instead of f:all:

grails-app/views/player/edit.gsp
<g:form resource="${this.player}" method="PUT">
    <g:hiddenField name="version" value="${this.player?.version}" />
    <fieldset class="form">
        <f:field bean="player" property="name" />
        <f:field bean="player" property="game" />
        <f:field bean="player" property="region" />
    </fieldset>
    <fieldset class="buttons">
        <input class="save" type="submit" value="${message(code: 'default.button.update.label', default: 'Update')}" />
    </fieldset>
</g:form>

While the application is running, open a player from the list. You can see the win/loss count on the show page, but Edit does not offer those fields.

That is not sufficient. Removing inputs from the form does not prevent those fields from being changed. A crafted request can still submit them.

curl --header "Accept: application/json" "http://localhost:8080/player/show/1.json"
{"id":1, "game":"Pandemic", "losses":30, "name":"Alexis Barnett", "region":"EAST", "wins":96}
curl --request POST \
  --header "Accept: application/json" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data "id=1&wins=2&losses=194" \
  "http://localhost:8080/player/update.json"
curl --header "Accept: application/json" "http://localhost:8080/player/show/1.json"

After the POST, Alexis Barnett has two wins and 194 losses instead of 96 and 30. A form POST hits the form branch of request.withFormat and redirects; GET the show URL afterwards to see the stored values.

One way to fix this would be to stop using a Player command object and bind fields by hand. Command objects do not have to be domain classes, though: use any class. Define PlayerInfo in PlayerController.groovy below the controller. Its property names match the edit form’s inputs, so only those fields bind.

grails-app/controllers/demo/PlayerController.groovy
class PlayerInfo {
    String name
    String game
    String region
}

Change update to use PlayerInfo as the command object. Load the Player, then copy the command object’s properties onto it:

    @Transactional
    def update(PlayerInfo info) {
        Player player = Player.get(params.id)
        if (player == null) {
            render status: HttpStatus.NOT_FOUND
            return
        }

        player.properties = info.properties
        player.save flush: true

        if (player.hasErrors()) {
            respond player.errors, view: 'edit'
            return
        }

        request.withFormat {
            form multipartForm { redirect player }
            '*' { respond player, status: HttpStatus.OK }
        }
    }

Because PlayerInfo does not contain wins or losses, those values are ignored even if they are posted.

curl --header "Accept: application/json" "http://localhost:8080/player/show/4.json"
{"id":4, "name":"Catherine Newton", "game":"Scythe", "region":"WEST", "wins":66, "losses":40}
curl --request POST \
  --header "Accept: application/json" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data "id=4&name=June+Smith&game=Chess&region=NORTH&wins=0&losses=10" \
  "http://localhost:8080/player/update.json"

curl --header "Accept: application/json" "http://localhost:8080/player/show/4.json"
{"id":4, "name":"June Smith", "game":"Chess", "region":"NORTH", "wins":66, "losses":40}

name, game, and region updated; wins and losses did not. The PlayerInfo command object restricted which fields are accepted.

Create a unit test for that behaviour:

src/test/groovy/demo/PlayerControllerSpec.groovy
package demo

import grails.testing.gorm.DomainUnitTest
import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification

class PlayerControllerSpec extends Specification implements ControllerUnitTest<PlayerController>, DomainUnitTest<Player> {

    def 'test update'() {
        when:
        def player = new Player(name: 'Sergio', game: 'XCOM: Enemy Unkown', region: 'Spain', wins: 3, losses: 2)
        player.save()
        params.id = player.id
        params.name = 'Sergio del Amo'
        params.game = 'XCOM 2'
        params.region = 'USA'
        params.wins = 4
        controller.update() (1)

        then: 'respond model has no errors'
        !model.player.hasErrors()

        and: 'player properties have been updated correctly'
        model.player.name == 'Sergio del Amo'
        model.player.game == 'XCOM 2'
        model.player.region == 'USA'

        and: 'non existing properties in the command object have not been modified'
        model.player.wins == 3
        model.player.losses == 2
    }
}
1 Call the no-argument update() and put values in params. Databinding fills the command object. That is what happens at runtime.

The current solution is still lacking. Because PlayerInfo is the command object, validation does not run until player.save. It is better to validate earlier, as when Player itself was the command object.

When you define PlayerInfo in the same file as the controller, Grails makes it Validateable. Add a constraints block:

grails-app/controllers/demo/PlayerController.groovy
class PlayerInfo {
    String name
    String game
    String region

    static constraints = {
        name blank: false
        game blank: false
        region nullable: true
    }
}

Create a unit test for those constraints:

src/test/groovy/demo/PlayerInfoSpec.groovy
package demo

import spock.lang.Specification

class PlayerInfoSpec extends Specification {

    def 'test PlayerInfo.region can be null'() {
        expect:
        new PlayerInfo(region: null).validate(['region'])
    }

    def 'test PlayerInfo.name cannot be blank'() {
        when:
        def playerInfo = new PlayerInfo(name: '')

        then:
        !playerInfo.validate(['name'])
        playerInfo.errors['name'].code == 'blank'

        when:
        playerInfo = new PlayerInfo(name: null)

        then:
        !playerInfo.validate(['name'])
        playerInfo.errors['name'].code == 'nullable'
    }

    def 'test PlayerInfo.game cannot be blank'() {
        when:
        def playerInfo = new PlayerInfo(game: '')

        then:
        !playerInfo.validate(['game'])
        playerInfo.errors['game'].code == 'blank'

        when:
        playerInfo = new PlayerInfo(game: null)

        then:
        !playerInfo.validate(['game'])
        playerInfo.errors['game'].code == 'nullable'
    }
}

Revise update once more so command-object errors are checked before the domain instance is loaded and saved. Copying onto Player still runs domain constraints, including unique: true on name, so inspect the save result and return those errors instead of reporting success. Because update is @Transactional and the Player is already dirty, call transactionStatus.setRollbackOnly() so the failed instance is not flushed when the action returns. respond info.errors, view: 'edit' returns the command-object error payload for API clients. For an HTML form post it also re-renders edit.gsp, but with a playerInfo model rather than player, so the scaffold form does not redisplay those errors. This beginner sample does not add an error-model bridge:

grails-app/controllers/demo/PlayerController.groovy
@Transactional
def update(PlayerInfo info) {
    if (info.hasErrors()) {
        respond info.errors, view: 'edit'
        return
    }
    Player player = Player.get(params.id)
    if (player == null) {
        render status: HttpStatus.NOT_FOUND
        return
    }

    player.properties = info.properties
    if (!player.save(flush: true)) {
        transactionStatus.setRollbackOnly()
        respond player.errors, view: 'edit'
        return
    }

    request.withFormat {
        form multipartForm { redirect player }
        '*' { respond player, status: HttpStatus.OK }
    }
}

Add a unit test that a duplicate name is reported instead of treated as a successful update:

src/test/groovy/demo/PlayerControllerSpec.groovy
def 'test update reports a duplicate name'() {
    given:
    new Player(name: 'Taken', game: 'Chess', region: 'EAST', wins: 1, losses: 0).save(flush: true)
    def player = new Player(name: 'Free', game: 'Go', region: 'WEST', wins: 2, losses: 1).save(flush: true)

    when:
    params.id = player.id
    params.name = 'Taken'
    params.game = 'Go'
    params.region = 'WEST'
    controller.update()

    then:
    view == 'edit'
    model.player.hasErrors()
    model.player.errors['name']?.code == 'unique'
}

The companion functional spec’s second test posts a crafted update after a real save and asserts wins / losses stay unchanged while game and region update. Add this method (and the private helpers) to the spec from the previous chapter:

src/integration-test/groovy/demo/PlayerControllerFuncSpec.groovy
    void 'test update ignores wins and losses'() {
        when: 'a player is created with known wins and losses'
        HttpResponse<String> createdResp = postForm('/player/save.json', 'name=Mass+Assignment&game=Chess&region=NORTH&wins=10&losses=5')
        def id = idFrom(createdResp)

        then:
        createdResp.statusCode() in [201, 302]
        id != null

        when: 'the saved player is fetched'
        Map created = getJson("/player/show/${id}.json")

        then:
        created.wins == 10
        created.losses == 5

        when: 'a crafted request tries to change wins and losses'
        HttpResponse<String> updateResp = postForm('/player/update.json', "id=${id}&name=Mass+Assignment&game=Go&region=EAST&wins=2&losses=194")
        Map after = getJson("/player/show/${id}.json")

        then: 'editable fields change, wins and losses do not'
        updateResp.statusCode() in [200, 302]
        after.name == 'Mass Assignment'
        after.game == 'Go'
        after.region == 'EAST'
        after.wins == 10
        after.losses == 5
    }

    private HttpResponse<String> postForm(String path, String body) {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:${serverPort}${path}"))
                .header('Accept', 'application/json')
                .header('Content-Type', 'application/x-www-form-urlencoded')
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .build()
        client.send(request, HttpResponse.BodyHandlers.ofString())
    }

    private Map getJson(String path) {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:${serverPort}${path}"))
                .header('Accept', 'application/json')
                .GET()
                .build()
        HttpResponse<String> resp = client.send(request, HttpResponse.BodyHandlers.ofString())
        assert resp.statusCode() == 200
        new JsonSlurper().parseText(resp.body()) as Map
    }

    private static Serializable idFrom(HttpResponse<String> resp) {
        if (resp.statusCode() == 201 && resp.body()) {
            return new JsonSlurper().parseText(resp.body()).id
        }
        resp.headers().firstValue('Location').orElse(null)?.split('/')?.last()
    }

Command object classes can be defined elsewhere (for example under src/). They need not live in the same file as the controller. That is useful for sharing a command object across controllers.

If the command object is in the same source file as the controller, Grails automatically makes it Validateable. If you define it elsewhere and need constraints, implement Validateable:

class PlayerInfo implements grails.validation.Validateable {
    String name
    String game
    String region

    static constraints = {
        name blank: false
        game blank: false
        region nullable: true
    }
}

4 Running the Application

From complete/ (or your finished initial/):

./gradlew bootRun

Open http://localhost:8080/player. List, create, and edit players. Confirm wins/losses cannot be changed via the edit form or a crafted update request.

./gradlew test
./gradlew integrationTest

The integration spec covers save validation (422 on a type-mismatched losses value) and the mass-assignment case (wins / losses ignored on update).

5 Do you need help with Grails?

Help with Apache Grails

Apache Grails is supported by an active community of contributors and the Apache Software Foundation. If you need help working through a guide, want to discuss the framework, or have run into something that looks like a bug, the channels below are the right place to start.

For Grails plugins, see the matching project on the apache org or the plugin’s own GitHub repository.