Data Binding
|
This section documents the current Vaadin release line — Vaadin 24 LTS / 25.x, Java 17+, Spring Boot 3 / Jakarta EE 10 — as published at the official Vaadin documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. Flow (server-side Java) is the authoring style used throughout, with Hilla / React shown where it differs; Vaadin 7 and the pre-Flow architecture appear only as migration contrast. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vaadin ships major releases roughly twice a year and its ecosystem iterates. This section’s bibliography lists the reference material consulted while preparing these pages. |
Binder<T> is Vaadin Flow’s bridge between a form’s input components and a typed business object: it loads
values from the object into the fields, runs conversion and validation as the user types, and writes the
values back only when they are valid. This page covers the API defined in
Binding Data to Forms. For the fields
themselves see Input Components, and for assembling a full editor see
Forms, CRUD and Master-Detail.
The Binder
A Binder<T> is created for one bean type and shared by every field in the form. The quickest way to wire
fields is bindInstanceFields(this),
which matches each HasValue member variable to a bean property of the same name by reflection.
public class Person {
private String fullName;
private String email;
private int age;
private Address address = new Address();
// getters and setters
}
public class PersonForm extends FormLayout {
private final TextField fullName = new TextField("Full name");
private final EmailField email = new EmailField("Email");
private final IntegerField age = new IntegerField("Age");
private final Binder<Person> binder = new Binder<>(Person.class);
public PersonForm() {
add(fullName, email, age);
binder.bindInstanceFields(this); // fullName -> "fullName", email -> "email", age -> "age"
}
}
bindInstanceFields skips fields that are already bound, so bindings that need a converter or a validator are
declared manually first and the call then fills in the rest. A member variable with no matching property, and
no manual binding, makes the call throw.
Manual bindings with forField
forField(field) starts a fluent binding: chain asRequired, withConverter and withValidator, then
close with bind(getter, setter). Passing null as the setter makes the binding read-only.
binder.forField(fullName)
.asRequired("Full name is required")
.withValidator(name -> name.length() >= 2, "At least two characters")
.bind(Person::getFullName, Person::setFullName);
binder.forField(email)
.bind(Person::getEmail, Person::setEmail);
bind also accepts a property name (binder.bind(fullName, "fullName")), but the method-reference form is
checked at compile time and is the recommended style for anything past a trivial form.
Converters
A field’s value type often differs from the property type — a TextField produces String, the bean stores
int. withConverter sits between them, converting presentation to model as the user types and back again on
load. Vaadin ships common converters such as StringToIntegerConverter;
Validating & Converting User
Input describes the full set.
TextField ageField = new TextField("Age");
binder.forField(ageField)
.withConverter(new StringToIntegerConverter("Enter a whole number"))
.withValidator(new IntegerRangeValidator("Age must be 0-150", 0, 150))
.bind(Person::getAge, Person::setAge);
Write a custom Converter<PRESENTATION, MODEL> with Converter.from, returning a Result so a failed
conversion becomes a field error instead of an exception.
Converter<String, LocalDate> isoDate = Converter.from(
text -> {
try {
return Result.ok(LocalDate.parse(text));
} catch (DateTimeParseException e) {
return Result.error("Use the format yyyy-MM-dd");
}
},
LocalDate::toString);
Converters and validators run in declaration order on the way in, and in reverse on the way out, so place a validator before the converter to check the raw text and after it to check the converted value.
Validators and BindingValidationStatus
withValidator adds a rule to a single binding, either as a (value, message) pair or a full Validator<T>
returning ValidationResult.ok() / ValidationResult.error(…). Keep the Binding reference to validate or
inspect that one field on demand.
Binder.Binding<Person, String> emailBinding = binder.forField(email)
.withValidator(new EmailValidator("Not a valid email address"))
.bind(Person::getEmail, Person::setEmail);
BindingValidationStatus<String> status = emailBinding.validate();
if (status.isError()) {
status.getMessage().ifPresent(Notification::show);
}
BindingValidationStatus is the per-field result — status (OK, ERROR, UNRESOLVED), message, and the
originating Result. The whole-form equivalent is BinderValidationStatus, returned by binder.validate(),
which exposes getFieldValidationStatuses() and getBeanValidationErrors(). Route messages to a specific
component with withStatusLabel(label), or take full control with withValidationStatusHandler(handler).
Cross-field validation
A rule that spans two fields can live on one binding (revalidate the partner when the other field changes) or
on the binder itself as a bean-level validator that runs during writeBean.
DatePicker start = new DatePicker("Start");
DatePicker end = new DatePicker("End");
Binder.Binding<Trip, LocalDate> endBinding = binder.forField(end)
.withValidator(d -> d == null || start.getValue() == null
|| !d.isBefore(start.getValue()), "End cannot be before start")
.bind(Trip::getEnd, Trip::setEnd);
start.addValueChangeListener(e -> endBinding.validate()); // re-run the rule when start moves
// bean-level: evaluated on writeBean / writeBeanIfValid, after every field-level rule passes
binder.withValidator(trip -> trip.getStart() == null || trip.getEnd() == null
|| !trip.getEnd().isBefore(trip.getStart()),
"End cannot be before start");
BeanValidationBinder and Jakarta Bean Validation
BeanValidationBinder<T> extends Binder<T> with the same API but automatically adds a validator for every
Jakarta Bean Validation (JSR 380) constraint annotation it finds on the bean, and marks @NotNull,
@NotEmpty and @Size(min > 0) fields as required.
public class Person {
@NotNull
@Size(min = 2, max = 60, message = "Full name must be 2-60 characters")
private String fullName;
@NotNull
@Email
private String email;
@Min(0)
private int age;
@Valid // cascade validation into the nested bean
private Address address = new Address();
// getters and setters
}
BeanValidationBinder<Person> binder = new BeanValidationBinder<>(Person.class);
binder.bindInstanceFields(this); // constraint validators are attached to each binding automatically
This needs a Bean Validation provider on the classpath. Spring Boot projects add
spring-boot-starter-validation; otherwise depend on Hibernate Validator directly, as noted in
Binding Beans to Forms.
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
Manually declared converters and validators still apply on top of the constraint-derived ones.
Nested bean properties
Binder reads a dotted path into a nested bean. The string form works directly; the type-safe form supplies
lambdas that walk the object graph.
public class Address {
@NotBlank private String street;
@Size(min = 4, max = 10) private String postalCode;
// getters and setters
}
TextField street = new TextField("Street");
TextField postalCode = new TextField("Postal code");
binder.bind(street, "address.street"); // dot path resolves getAddress().getStreet()
binder.forField(postalCode)
.bind(p -> p.getAddress().getPostalCode(),
(p, v) -> p.getAddress().setPostalCode(v));
Keep the nested bean instance non-null (initialise it in the parent, as Person does above) so the setter
path has something to write to. With @Valid on the nested field, a BeanValidationBinder also enforces the
Address constraints.
Loading and saving: readBean, writeBean, writeBeanIfValid
readBean copies the bean into the fields and resets validation state; passing null clears the form.
writeBean validates every binding plus any bean-level validator and copies the values back, throwing
ValidationException on failure. writeBeanIfValid does the same but returns a boolean instead of
throwing.
binder.readBean(person); // bean -> fields
binder.readBean(null); // clear the form
try {
binder.writeBean(person); // fields -> bean, or throw
repository.save(person);
} catch (ValidationException e) {
e.getValidationErrors().forEach(result -> log.warn(result.getErrorMessage()));
}
if (binder.writeBeanIfValid(person)) {
repository.save(person);
} else {
Notification.show("Fix the highlighted fields");
}
writeBeanAsDraft(person) copies whatever converts cleanly, skipping validation, for saving a
work-in-progress. Java record beans use the immutable pair readRecord / writeRecord instead.
Buffered vs. unbuffered binding
| Mode | Behaviour |
|---|---|
Buffered (default) |
Enter with |
Unbuffered |
Enter with |
// buffered
binder.readBean(person);
saveButton.addClickListener(e -> {
if (binder.writeBeanIfValid(person)) {
repository.save(person);
}
});
// unbuffered
binder.setBean(person); // valid edits flow into `person` immediately
binder.hasChanges() reports whether the fields differ from what was last read (buffered mode); with
setChangeDetectionEnabled(true) it compares against the original values rather than just tracking edits.
See Loading & Saving to Business
Objects.
StatusChangeListener
addStatusChangeListener fires on every validation run, value change, and on readBean / writeBean — the usual place to enable or disable the Save button.
binder.addStatusChangeListener(event -> {
boolean saveable = !event.hasValidationErrors() && binder.hasChanges();
saveButton.setEnabled(saveable);
});
The StatusChangeEvent also reports getBinder() and whether the change came from a write operation, which
lets a listener distinguish a user edit from a programmatic reload.
HasValidator
A component can carry its own validation rule by implementing HasValidator<T>. When such a component is
bound, Binder automatically picks up getDefaultValidator(), so an intrinsic rule — a custom PhoneField
that only accepts digits, say — does not have to be repeated at every binding. The component’s
addValidationStatusChangeListener lets it tell the binder to revalidate when its internal constraints
change.
public class PhoneField extends CustomField<String> implements HasValidator<String> {
private final TextField input = new TextField();
@Override
protected String generateModelValue() {
return input.getValue();
}
@Override
protected void setPresentationValue(String value) {
input.setValue(value == null ? "" : value);
}
@Override
public Validator<String> getDefaultValidator() {
return (value, ctx) -> value == null || value.matches("\\+?\\d{6,15}")
? ValidationResult.ok()
: ValidationResult.error("Enter 6 to 15 digits");
}
}
// no withValidator needed -- the field's own rule is applied
binder.forField(new PhoneField()).bind(Person::getPhone, Person::setPhone);
Many built-in Vaadin fields already implement HasValidator, which is how constraints such as an
EmailField pattern or a DatePicker min/max participate in binder validation without extra code.
The Binder data flow
presentation to model"] conv --> val["Validator chain
field-level, HasValidator, bean-level"] val -->|"error"| status["BindingValidationStatus / BinderValidationStatus
StatusChangeListener fires, messages shown"] status --> fields val -->|"valid"| mode{"binding mode?"} mode -->|"buffered: writeBean() / writeBeanIfValid()"| write["Values copied into the bean on save"] mode -->|"unbuffered: setBean(bean)"| live["Each valid edit written into the bean at once"] write --> bean live --> bean
|
In Hilla / React: forms use the |
See also
-
Input Components — the field components a
Binderdrives and theHasValuecontract. -
Forms, CRUD and Master-Detail — wiring a bound form into a Grid-backed editor.
-
The Grid Component — the
Grideditor, which is bound with the sameBinderAPI. -
Hilla and React Views —
useFormand the generated model on the Hilla side. -
Binding Data to Forms — the official reference.