SupportWriter space ↗
← Back to the journal
Vaadin

Forms and data binding in Vaadin with Binder

Connect Vaadin form fields to Java objects with Binder: required fields, validators, converters, Bean Validation, buffered editing, and save buttons that react to form state.

DPutu Adi Guna Permana · 08 Oct 2026 · 5 min read

Why a binder?

Almost every business application has forms: customers, products, orders, settings. Without help, form code quickly becomes repetitive: copy each value from the object into a field, read each field back, convert text to numbers and dates, validate everything, and show error messages next to the right field.

Vaadin's Binder does all of that. It connects fields to properties of a Java object, converts and validates values, and shows errors directly on the fields. The examples target Vaadin 24 or later.

We will work with this simple bean:

public class Customer {
    private Long id;
    private String name;
    private String email;
    private Integer age;
    private LocalDate memberSince;
    private boolean newsletter;

    // getters and setters omitted
}

A first binding

Create the fields and a Binder for the bean type, then bind each field explicitly:

public class CustomerForm extends FormLayout {

    private final TextField name = new TextField("Name");
    private final EmailField email = new EmailField("Email");
    private final IntegerField age = new IntegerField("Age");
    private final DatePicker memberSince = new DatePicker("Member since");
    private final Checkbox newsletter = new Checkbox("Subscribe to newsletter");

    private final Binder<Customer> binder = new Binder<>(Customer.class);

    public CustomerForm() {
        binder.forField(name)
                .asRequired("Name is required")
                .withValidator(value -> value.length() <= 80, "Name is too long")
                .bind(Customer::getName, Customer::setName);

        binder.forField(email)
                .asRequired("Email is required")
                .withValidator(new EmailValidator("Please enter a valid email address"))
                .bind(Customer::getEmail, Customer::setEmail);

        binder.forField(age)
                .withValidator(value -> value == null || value >= 17, "Customers must be at least 17")
                .bind(Customer::getAge, Customer::setAge);

        binder.forField(memberSince)
                .withValidator(date -> date == null || !date.isAfter(LocalDate.now()), "Date cannot be in the future")
                .bind(Customer::getMemberSince, Customer::setMemberSince);

        binder.bind(newsletter, Customer::isNewsletter, Customer::setNewsletter);

        add(name, email, age, memberSince, newsletter);
    }
}

Each forField(...) chain reads naturally: this field is required, must pass these validators, and maps to this getter and setter. Method references keep everything type-safe, so renaming a property in the bean becomes a compile error instead of a silent bug.

Converters

Sometimes the field type differs from the property type. A TextField always produces a String, but the property might be a BigDecimal or an Integer. Add a converter:

var discount = new TextField("Discount (%)");

binder.forField(discount)
        .withConverter(new StringToBigDecimalConverter("Please enter a number"))
        .withValidator(value -> value == null || value.compareTo(BigDecimal.valueOf(50)) <= 0, "Maximum discount is 50%")
        .bind(Customer::getDiscount, Customer::setDiscount);

Order matters: validators placed before withConverter check the raw text, while validators placed after it check the converted value. Vaadin includes converters for integers, longs, doubles, BigDecimal, dates, and more. For numeric input, specialized fields such as IntegerField, NumberField, and BigDecimalField avoid the converter entirely.

Buffered vs. unbuffered editing

Binder supports two ways of working with the bean.

Buffered (readBean and writeBeanIfValid): field changes stay in the form until you explicitly write them. This is ideal for forms with Save and Cancel buttons.

public void edit(Customer customer) {
    this.customer = customer;
    binder.readBean(customer);
}

private void save() {
    if (binder.writeBeanIfValid(customer)) {
        customerService.save(customer);
        Notification.show("Customer saved");
    } else {
        Notification.show("Please fix the errors and try again");
    }
}

private void cancel() {
    binder.readBean(customer); // discard edits
}

If you want details about what failed, use writeBean(customer), which throws a ValidationException containing every validation error.

Unbuffered (setBean): every valid change is written to the bean immediately. This suits live settings panels where there is no Save button.

binder.setBean(settings);

A common bug is mixing the two styles. Pick one per form.

Reacting to form state

Enable the Save button only when the form is valid and something has changed:

binder.addStatusChangeListener(event ->
        save.setEnabled(binder.isValid() && binder.hasChanges()));

This gives users immediate feedback and prevents submitting invalid data.

Cross-field validation

Some rules involve more than one field, for example "end date must be after start date". Add a bean-level validator:

binder.withValidator(
        booking -> booking.getEnd() == null || booking.getStart() == null || booking.getEnd().isAfter(booking.getStart()),
        "End date must be after start date");

Bean-level validators run when the bean is written. Show their messages in a status label with binder.setStatusLabel(statusLabel), because they do not belong to a single field.

Using Bean Validation annotations

If your domain classes already use Jakarta Bean Validation, reuse those rules with BeanValidationBinder:

public class Customer {
    @NotBlank
    @Size(max = 80)
    private String name;

    @NotBlank
    @Email
    private String email;

    @Min(17)
    private Integer age;

    // ...
}

Combine it with automatic binding by field name:

private final TextField name = new TextField("Name");
private final EmailField email = new EmailField("Email");
private final IntegerField age = new IntegerField("Age");

private final BeanValidationBinder<Customer> binder = new BeanValidationBinder<>(Customer.class);

public CustomerForm() {
    binder.bindInstanceFields(this);
    add(name, email, age);
}

bindInstanceFields matches Java field names (name, email, age) to bean properties with the same names. Fields annotated with @NotNull or @NotBlank are also marked as required in the UI automatically. Use @PropertyId("emailAddress") on a field when the names differ.

This approach keeps validation rules in one place, shared by the UI, the REST API, and JPA.

Putting it together: an editor in a dialog

A typical pattern opens the form in a dialog from a grid:

private void openEditor(Customer customer) {
    var form = new CustomerForm();
    form.edit(customer);

    var dialog = new Dialog(form);
    dialog.setHeaderTitle(customer.getId() == null ? "New customer" : "Edit customer");

    var save = new Button("Save", e -> {
        if (form.save()) {
            dialog.close();
            grid.getDataProvider().refreshAll();
        }
    });
    save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);

    dialog.getFooter().add(new Button("Cancel", e -> dialog.close()), save);
    dialog.open();
}

Here form.save() returns the result of writeBeanIfValid after calling the service, so the dialog only closes when saving succeeded.

Tips

  • Write user-facing error messages, not technical ones: "Please enter a valid email address" rather than "Pattern mismatch".
  • Validate again in the service layer. The UI improves the experience, but services might be called from other places too.
  • Use setRequiredIndicatorVisible(true) or asRequired so users see which fields are mandatory before they make mistakes.

Exercise

Build a product form with name (required, max 100 characters), price (a BigDecimalField, greater than zero), stock (an IntegerField, zero or more), and discountUntil (a DatePicker that must be in the future). Use buffered editing, enable Save only when the form is valid and changed, and add a bean-level rule that a discount date requires a price above 10.

← Explore more notes