Prevent Mass Assignment in Grails with Command Objects
Use Grails command objects as an allow-list for editable fields to prevent mass assignment and keep web input validation out of your domain model.
15 Jun 2026, 13:41 UTC

Stop Letting Request Parameters Rewrite Your Domain Model
Imagine a profile edit screen where a user can change their display name and email. A common shortcut in a Grails controller is to bind the incoming request straight to a persisted domain instance:
def update(Long id) {
def user = User.get(id)
user.properties = params
user.save(flush: true)
redirect(action: 'show', id: user.id)
}
This works until a request includes an extra parameter such as role=ADMIN or version=999. Because user.properties = params assigns every matching setter it finds, fields you never meant to expose can be overwritten. This is often called mass assignment.
The useful takeaway: use a Grails command object as an explicit allow-list for the fields a web request may change. Grails binds request data to the command object, validates it, and only then do you copy the approved values into your domain class.
What a Command Object Is
A command object is a plain Groovy class declared as a parameter to a controller action. It is not a GORM domain class: it has no database mapping, no static mapping block, and no persistent identity. Grails automatically populates its fields from HTTP request parameters and runs declared constraints before the action body executes. If validation fails, the action can inspect command.errors and return the form instead of proceeding.
Worked Example: Profile Update Command
Suppose your User domain class stores username, email, bio, role, and internal audit fields. The profile form should only touch the first three.
package com.example
class ProfileUpdateCommand {
String username
String email
String bio
static constraints = {
username blank: false, size: 3..50
email email: true, blank: false
bio maxSize: 500
}
}
Place this file under grails-app/domain/com/example/ or src/main/groovy/com/example/ depending on your project layout; command objects do not require GORM mapping.
In the controller, declare the command object as the action parameter. Grails instantiates and binds it before the method runs:
package com.example
class UserController {
def userService
def update(ProfileUpdateCommand command, Long id) {
if (command.hasErrors()) {
def user = User.get(id)
return view: 'profile', model: [user: user, command: command]
}
def user = User.get(id)
if (!user) {
return redirect(action: 'notFound')
}
user.username = command.username
user.email = command.email
user.bio = command.bio
try {
user.save(flush: true)
} catch (Exception e) {
flash.error = 'Profile could not be saved.'
return view: 'profile', model: [user: user, command: command]
}
flash.message = 'Profile updated.'
redirect(action: 'show', id: user.id)
}
}
Only the three whitelisted fields are assigned to the domain instance. A request parameter such as role=ADMIN is ignored because ProfileUpdateCommand has no role property.
Rendering Validation Errors
In the GSP view, read errors directly from the command object and display them next to the corresponding input:
<div class='fieldErrors'>
<ul>
<li>${command.errors.username}</li>
<li>${command.errors.email}</li>
</ul>
</div>
Because validation happens before the action body, invalid submissions never reach user.save().
Trade-offs and Limitations
- For a trivial one-field update, a command object adds more files than direct binding.
- You must manually copy command fields to the domain instance; forgetting a field means silent data loss from the user's perspective.
- Deeply nested command objects can bind differently across Grails versions, so confirm behavior against your version's reference guide.
- Command objects are not persistent entities; do not add GORM mappings or expect them to be saved directly.
How to Verify the Behavior
- Start the Grails application in your local development environment.
- Open the profile edit form and submit it with an invalid email address or an empty username.
- Confirm the controller action does not call
user.save()and the page re-renders with errors fromcommand.errors. - Submit a valid request that also includes an unexpected parameter such as
role=ADMIN. - Reload the user record and confirm the
rolecolumn was unchanged.
Run these checks from the application directory with normal file-write permissions for grails-app/controllers and grails-app/domain. A risk is that an outdated command object can drift from the domain class, so review both when adding new editable fields.
Command objects trade a small amount of boilerplate for explicit control over what HTTP input can touch your database. For profile updates, checkout forms, or any screen where the request shape differs from the persisted model, that trade is usually worth it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.