6.18 Handling Form Data

In order to make data binding model customizations consistent between form data and JSON, Micronaut uses Jackson to implement binding data from form submissions.

The advantage of this approach is that the same Jackson annotations used for customizing JSON binding can be used for form submissions too.

What this means in practice is that in order to bind regular form data the only change required to the previous JSON binding code is updating the MediaType consumed:

Binding Form Data to POJOs

  1. @Controller("/people")
  2. public class PersonController {
  3. Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>();
  4. @Post
  5. public HttpResponse<Person> save(@Body Person person) {
  6. inMemoryDatastore.put(person.getFirstName(), person);
  7. return HttpResponse.created(person);
  8. }
  9. }

Binding Form Data to POJOs

  1. @Controller("/people")
  2. class PersonController {
  3. ConcurrentHashMap<String, Person> inMemoryDatastore = [:]
  4. @Post
  5. HttpResponse<Person> save(@Body Person person) {
  6. inMemoryDatastore.put(person.getFirstName(), person)
  7. HttpResponse.created(person)
  8. }
  9. }

Binding Form Data to POJOs

  1. @Controller("/people")
  2. class PersonController {
  3. internal var inMemoryDatastore: MutableMap<String, Person> = ConcurrentHashMap()
  4. @Post
  5. fun save(@Body person: Person): HttpResponse<Person> {
  6. inMemoryDatastore[person.firstName] = person
  7. return HttpResponse.created(person)
  8. }
  9. }
To avoid denial of service attacks, collection types and arrays created during binding are limited by the setting jackson.arraySizeThreshold in application.yml

Alternatively, instead of using a POJO you can bind form data directly to method parameters (which works with JSON too!):

Binding Form Data to Parameters

  1. @Controller("/people")
  2. public class PersonController {
  3. Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>();
  4. @Post("/saveWithArgs")
  5. public HttpResponse<Person> save(String firstName, String lastName, Optional<Integer> age) {
  6. Person p = new Person(firstName, lastName);
  7. age.ifPresent(p::setAge);
  8. inMemoryDatastore.put(p.getFirstName(), p);
  9. return HttpResponse.created(p);
  10. }
  11. }

Binding Form Data to Parameters

  1. @Controller("/people")
  2. class PersonController {
  3. ConcurrentHashMap<String, Person> inMemoryDatastore = [:]
  4. @Post("/saveWithArgs")
  5. HttpResponse<Person> save(String firstName, String lastName, Optional<Integer> age) {
  6. Person p = new Person(firstName, lastName)
  7. age.ifPresent({ a -> p.setAge(a)})
  8. inMemoryDatastore.put(p.getFirstName(), p)
  9. HttpResponse.created(p)
  10. }
  11. }

Binding Form Data to Parameters

  1. @Controller("/people")
  2. class PersonController {
  3. internal var inMemoryDatastore: MutableMap<String, Person> = ConcurrentHashMap()
  4. @Post("/saveWithArgs")
  5. fun save(firstName: String, lastName: String, age: Optional<Int>): HttpResponse<Person> {
  6. val p = Person(firstName, lastName)
  7. age.ifPresent { p.age = it }
  8. inMemoryDatastore[p.firstName] = p
  9. return HttpResponse.created(p)
  10. }
  11. }

As you can see from the example above, this approach allows you to use features such as support for Optional types and restrict the parameters to be bound (When using POJOs you must be careful to use Jackson annotations to exclude properties that should not be bound).