or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

api-versioning.mdasync.mdconfiguration.mdcontent-negotiation.mdcontroller-annotations.mdcore-framework.mdcors.mddata-binding.mdexception-handling.mdflash-attributes.mdfunctional-endpoints.mdi18n.mdindex.mdinterceptors.mdmultipart.mdrequest-binding.mdresource-handling.mdresponse-handling.mduri-building.mdview-resolution.md
tile.json

data-binding.mddocs/

Data Binding and Validation

Data binding capabilities for binding request parameters to form objects, type conversion, and validation using JSR-303/JSR-380 Bean Validation along with Spring's validation framework.

Capabilities

@InitBinder

Annotation for methods that initialize WebDataBinder instances for data binding customization, including setting allowed/disallowed fields, registering custom editors, and adding validators.

/**
 * Annotation that identifies methods that initialize the WebDataBinder
 * which will be used for populating command and form object arguments
 * of annotated handler methods.
 *
 * @InitBinder methods support all arguments that @RequestMapping methods support,
 * except for command/form objects and corresponding validation result objects.
 * @InitBinder methods must not have a return value; they are usually declared as void.
 *
 * Typical arguments are WebDataBinder in combination with WebRequest or Locale,
 * allowing to register context-specific editors.
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InitBinder {
    /**
     * The names of command/form attributes and/or request parameters
     * that this init-binder method is supposed to apply to.
     * Default is to apply to all command/form attributes and all request parameters
     * processed by the annotated handler class. Specifying model attribute names or
     * request parameter names here restricts the init-binder method to those specific
     * attributes/parameters.
     */
    String[] value() default {};
}

Usage Example:

@RestControllerAdvice
public class DataBindingAdvice {

    // Global init binder applying to all controllers
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        // Disallow binding of sensitive fields
        binder.setDisallowedFields("id", "createdAt", "updatedAt");

        // Custom validators
        binder.addValidators(new ProductValidator());

        // Custom property editor
        binder.registerCustomEditor(Date.class, new CustomDateEditor(
            new SimpleDateFormat("yyyy-MM-dd"), true));
    }

    // Specific init binder only for "user" model attribute
    @InitBinder("user")
    public void initUserBinder(WebDataBinder binder) {
        binder.setDisallowedFields("role", "permissions");
    }

    // Multiple model attributes
    @InitBinder({"product", "inventory"})
    public void initProductBinder(WebDataBinder binder) {
        binder.setAllowedFields("name", "price", "quantity");
    }
}

@SessionAttributes

Type-level annotation that indicates which model attributes should be stored in the HTTP session between requests, useful for multi-page forms and conversational workflows.

/**
 * Annotation that indicates the session attributes that a specific handler uses.
 *
 * This will typically list the names of model attributes which should be
 * transparently stored in the session or some conversational storage,
 * serving as form-backing beans. Declared at the type level, applying
 * to the model attributes that the annotated handler class operates on.
 *
 * Session attributes as indicated using this annotation correspond to a specific
 * handler's model attributes, getting transparently stored in a conversational session.
 * Those attributes will be removed once the handler indicates completion of its
 * conversational session.
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SessionAttributes {
    /**
     * Alias for names().
     */
    String[] value() default {};

    /**
     * The names of session attributes in the model that should be stored in the
     * session or some conversational storage.
     * Note: This indicates the model attribute names. The session attribute names
     * may or may not match the model attribute names.
     */
    String[] names() default {};

    /**
     * The types of session attributes in the model that should be stored in the
     * session or some conversational storage.
     * All model attributes of these types will be stored in the session,
     * regardless of attribute name.
     */
    Class<?>[] types() default {};
}

Usage Example:

@Controller
@RequestMapping("/wizard")
@SessionAttributes({"wizardForm", "userData"})
public class MultiStepFormController {

    @GetMapping("/step1")
    public String showStep1(Model model) {
        // Create form object and add to model
        WizardForm form = new WizardForm();
        model.addAttribute("wizardForm", form);
        return "wizard/step1";
    }

    @PostMapping("/step1")
    public String processStep1(@ModelAttribute("wizardForm") WizardForm form,
                              BindingResult result) {
        // Form is automatically stored in session
        // Process step 1 data
        form.setStep1Complete(true);
        return "redirect:/wizard/step2";
    }

    @GetMapping("/step2")
    public String showStep2(@ModelAttribute("wizardForm") WizardForm form) {
        // Form is automatically retrieved from session
        if (!form.isStep1Complete()) {
            return "redirect:/wizard/step1";
        }
        return "wizard/step2";
    }

    @PostMapping("/step2")
    public String processStep2(@ModelAttribute("wizardForm") WizardForm form,
                              BindingResult result,
                              SessionStatus status) {
        // Process final step
        form.setStep2Complete(true);

        // Save to database
        wizardService.save(form);

        // Clear session attributes
        status.setComplete();

        return "redirect:/wizard/complete";
    }
}

// Using types instead of names
@Controller
@RequestMapping("/shopping")
@SessionAttributes(types = {ShoppingCart.class, UserPreferences.class})
public class ShoppingController {
    // All model attributes of type ShoppingCart or UserPreferences
    // will be automatically stored in session
}

@Validated

Variant of JSR-303's @Valid to be used on type level for method validation, or on method parameters to apply validation groups.

/**
 * Variant of JSR-303's @Valid, supporting the specification of validation groups.
 * Designed for convenient use with Spring's JSR-303 support but not JSR-303 specific.
 *
 * Can be used for class-level method validation (triggering validation of the arguments
 * for any methods with @Valid parameters) or method-level parameter validation.
 */
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
    /**
     * Specify one or more validation groups to apply to the validation step
     * kicked off by this annotation.
     * JSR-303 defines validation groups as custom annotations which an application declares
     * for the sole purpose of using them as type-safe group arguments, as implemented in
     * SpringValidatorAdapter.
     *
     * Other SmartValidator implementations may support class arguments in other ways.
     */
    Class<?>[] value() default {};
}

Usage Example:

@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {

    @PostMapping
    public ResponseEntity<User> createUser(
            @RequestBody @Valid User user) {
        User created = userService.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }

    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable Long id,
            @RequestBody @Validated(UpdateGroup.class) User user) {
        User updated = userService.update(id, user);
        return ResponseEntity.ok(updated);
    }

    @PostMapping("/admin")
    public ResponseEntity<User> createAdminUser(
            @RequestBody @Validated({Default.class, AdminGroup.class}) User user) {
        User created = userService.saveAdmin(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }
}

// Domain model with validation groups
public class User {
    @NotNull(groups = UpdateGroup.class)
    private Long id;

    @NotBlank
    @Size(min = 3, max = 50)
    private String username;

    @NotBlank
    @Email
    private String email;

    @NotNull
    @Size(min = 8, message = "Password must be at least 8 characters")
    private String password;

    @NotNull(groups = AdminGroup.class)
    private Role role;
}

// Validation groups
public interface UpdateGroup {}
public interface AdminGroup {}

@Valid

Standard JSR-303/JSR-380 annotation for marking a method parameter or field for validation.

/**
 * Marks a property, method parameter or method return type for validation cascading.
 * Constraints defined on the object and its properties are be validated when the
 * property, method parameter or method return type is validated.
 *
 * This is a standard JSR-303/JSR-380 annotation.
 */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Valid {
}

Errors

Interface for data binding errors. Allows storing and evaluating information about data binding errors.

/**
 * Stores and exposes information about data-binding and validation
 * errors for a specific object.
 *
 * Field names can be properties of the target object (e.g. "name"
 * when binding to a customer object), or nested fields in case of
 * subobjects (e.g. "address.street"). Supports subtree navigation
 * via setNestedPath(String): e.g. an AddressValidator validates
 * "address", not "address.street".
 */
public interface Errors {
    /**
     * The separator between path elements in a nested path,
     * for example in "customer.name" or "customer.address.street".
     */
    String NESTED_PATH_SEPARATOR = ".";

    /**
     * Return the name of the bound root object.
     *
     * @return the name of the bound object
     */
    String getObjectName();

    /**
     * Set the nested path of this Errors object.
     * Can be used to navigate to subobjects before validating.
     *
     * @param nestedPath nested path within this object
     */
    void setNestedPath(String nestedPath) {}

    /**
     * Return the current nested path of this Errors object.
     *
     * @return the current nested path
     */
    String getNestedPath() {}

    /**
     * Push the given sub path onto the nested path stack.
     *
     * @param subPath the sub path to push
     */
    void pushNestedPath(String subPath) {}

    /**
     * Pop the former nested path from the nested path stack.
     */
    void popNestedPath() throws IllegalStateException {}

    /**
     * Register a global error for the entire target object.
     *
     * @param errorCode error code, interpretable as a message key
     */
    void reject(String errorCode) {}

    /**
     * Register a global error for the entire target object.
     *
     * @param errorCode error code, interpretable as a message key
     * @param defaultMessage fallback default message
     */
    void reject(String errorCode, String defaultMessage) {}

    /**
     * Register a global error for the entire target object.
     *
     * @param errorCode error code, interpretable as a message key
     * @param errorArgs error arguments, for argument binding via MessageFormat
     * @param defaultMessage fallback default message
     */
    void reject(String errorCode, Object[] errorArgs, String defaultMessage) {}

    /**
     * Register a field error for the specified field of the current object.
     *
     * @param field the field name (may be null or empty String)
     * @param errorCode error code, interpretable as a message key
     */
    void rejectValue(String field, String errorCode) {}

    /**
     * Register a field error for the specified field of the current object.
     *
     * @param field the field name (may be null or empty String)
     * @param errorCode error code, interpretable as a message key
     * @param defaultMessage fallback default message
     */
    void rejectValue(String field, String errorCode, String defaultMessage) {}

    /**
     * Register a field error for the specified field of the current object.
     *
     * @param field the field name (may be null or empty String)
     * @param errorCode error code, interpretable as a message key
     * @param errorArgs error arguments, for argument binding via MessageFormat
     * @param defaultMessage fallback default message
     */
    void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) {}

    /**
     * Add all errors from the given Errors instance to this Errors instance.
     *
     * @param errors the Errors instance to merge in
     */
    void addAllErrors(Errors errors) {}

    /**
     * Return if there were any errors.
     *
     * @return true if there are any errors
     */
    boolean hasErrors() {}

    /**
     * Return the total number of errors.
     *
     * @return the total number of errors
     */
    int getErrorCount() {}

    /**
     * Get all errors, both global and field ones.
     *
     * @return a list of ObjectError instances
     */
    List<ObjectError> getAllErrors() {}

    /**
     * Are there any global errors?
     *
     * @return true if there are any global errors
     */
    boolean hasGlobalErrors() {}

    /**
     * Return the number of global errors.
     *
     * @return the number of global errors
     */
    int getGlobalErrorCount() {}

    /**
     * Get all global errors.
     *
     * @return a list of ObjectError instances
     */
    List<ObjectError> getGlobalErrors() {}

    /**
     * Get the first global error, if any.
     *
     * @return the global error, or null
     */
    ObjectError getGlobalError() {}

    /**
     * Are there any field errors?
     *
     * @return true if there are any errors associated with a field
     */
    boolean hasFieldErrors() {}

    /**
     * Return the number of errors associated with a field.
     *
     * @return the number of errors associated with a field
     */
    int getFieldErrorCount() {}

    /**
     * Get all errors associated with a field.
     *
     * @return a list of FieldError instances
     */
    List<FieldError> getFieldErrors() {}

    /**
     * Get the first error associated with a field, if any.
     *
     * @return the field-specific error, or null
     */
    FieldError getFieldError() {}

    /**
     * Are there any errors associated with the given field?
     *
     * @param field the field name
     * @return true if there were any errors associated with the given field
     */
    boolean hasFieldErrors(String field) {}

    /**
     * Return the number of errors associated with the given field.
     *
     * @param field the field name
     * @return the number of errors associated with the given field
     */
    int getFieldErrorCount(String field) {}

    /**
     * Get all errors associated with the given field.
     *
     * @param field the field name
     * @return a list of FieldError instances
     */
    List<FieldError> getFieldErrors(String field) {}

    /**
     * Get the first error associated with the given field, if any.
     *
     * @param field the field name
     * @return the field-specific error, or null
     */
    FieldError getFieldError(String field) {}

    /**
     * Return the current value of the given field, either the current
     * bean property value or a rejected update from the last binding.
     *
     * @param field the field name
     * @return the current value of the given field
     */
    Object getFieldValue(String field) {}

    /**
     * Return the type of a given field.
     *
     * @param field the field name
     * @return the type of the field, or null if not determinable
     */
    Class<?> getFieldType(String field) {}
}

BindingResult

Interface that extends Errors with additional capabilities for registration and evaluation of binding errors.

/**
 * General interface that represents binding results. Extends the Errors
 * interface for error registration capabilities, allowing for a Validator
 * to be applied, and adds binding-specific analysis and model building.
 *
 * Serves as result holder for a DataBinder, obtained via the DataBinder.getBindingResult()
 * method. BindingResult implementations can also be used directly, for example to invoke
 * a Validator on it (e.g. as part of a unit test).
 */
public interface BindingResult extends Errors {
    /**
     * Prefix for the name of the BindingResult instance in a model,
     * followed by the object name.
     */
    String MODEL_KEY_PREFIX = BindingResult.class.getName() + ".";

    /**
     * Return the wrapped target object, which may be a bean, an object with
     * public fields, a Map - depending on the concrete binding strategy.
     *
     * @return the target object
     */
    Object getTarget() {}

    /**
     * Return a model Map for the obtained state, exposing a BindingResult
     * instance as '{@link #MODEL_KEY_PREFIX MODEL_KEY_PREFIX} + objectName'
     * and the object itself as 'objectName'.
     *
     * @return the model Map
     */
    Map<String, Object> getModel() {}

    /**
     * Extract the raw field value for the given field.
     * Typically used for comparison purposes.
     *
     * @param field the field to check
     * @return the current value of the field in its raw form,
     * or null if not known
     */
    Object getRawFieldValue(String field) {}

    /**
     * Find a custom property editor for the given type and property.
     *
     * @param field the path of the property (name or nested path), or
     * null if looking for an editor for all properties of the given type
     * @param valueType the type of the property (can be null if a property
     * is given but should be specified in any case for consistency checking)
     * @return the registered editor, or null if none
     */
    PropertyEditor findEditor(String field, Class<?> valueType) {}

    /**
     * Return the underlying PropertyEditorRegistry.
     *
     * @return the PropertyEditorRegistry, or null if none available
     */
    PropertyEditorRegistry getPropertyEditorRegistry() {}

    /**
     * Resolve the given error code into message codes.
     *
     * @param errorCode the error code to resolve into message codes
     * @return the resolved message codes
     */
    String[] resolveMessageCodes(String errorCode) {}

    /**
     * Resolve the given error code into message codes for the given field.
     *
     * @param errorCode the error code to resolve into message codes
     * @param field the field to resolve message codes for
     * @return the resolved message codes
     */
    String[] resolveMessageCodes(String errorCode, String field) {}

    /**
     * Add a custom ObjectError or FieldError to the errors list.
     *
     * @param error the custom ObjectError or FieldError
     */
    void addError(ObjectError error) {}

    /**
     * Record the given value for the specified field.
     *
     * @param field the field to record the value for
     * @param type the type of the field
     * @param value the value to record
     */
    default void recordFieldValue(String field, Class<?> type, Object value) {}

    /**
     * Mark the specified disallowed field as suppressed.
     *
     * @param field the field to mark as suppressed
     */
    default void recordSuppressedField(String field) {}

    /**
     * Return the list of fields that were suppressed during the bind process.
     *
     * @return the list of suppressed fields, or an empty array
     */
    default String[] getSuppressedFields() {
        return new String[0];
    }
}

Usage Example:

@Controller
@RequestMapping("/products")
public class ProductFormController {

    @PostMapping
    public String createProduct(@Valid @ModelAttribute("product") Product product,
                               BindingResult bindingResult,
                               Model model) {
        // Check for validation errors
        if (bindingResult.hasErrors()) {
            // Add additional model attributes for form
            model.addAttribute("categories", categoryService.findAll());
            return "product/form";
        }

        // Custom business validation
        if (productService.existsByName(product.getName())) {
            bindingResult.rejectValue("name", "duplicate.product.name",
                "A product with this name already exists");
            model.addAttribute("categories", categoryService.findAll());
            return "product/form";
        }

        productService.save(product);
        return "redirect:/products";
    }

    @PostMapping("/bulk")
    public String createProducts(@Valid @ModelAttribute ProductBulkForm form,
                                BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            // Handle field errors
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
            for (FieldError error : fieldErrors) {
                System.out.println("Field: " + error.getField() +
                                 ", Error: " + error.getDefaultMessage());
            }
            return "product/bulk-form";
        }

        productService.saveAll(form.getProducts());
        return "redirect:/products";
    }
}

// REST controller example with custom error handling
@RestController
@RequestMapping("/api/products")
public class ProductApiController {

    @PostMapping
    public ResponseEntity<?> createProduct(@RequestBody @Valid Product product,
                                          BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            Map<String, String> errors = new HashMap<>();
            bindingResult.getFieldErrors().forEach(error ->
                errors.put(error.getField(), error.getDefaultMessage())
            );
            return ResponseEntity.badRequest().body(errors);
        }

        Product created = productService.save(product);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }
}

WebDataBinder

Special DataBinder for data binding from web request parameters to JavaBeans.

/**
 * Special DataBinder to perform data binding from web request parameters to JavaBeans,
 * including support for multipart files.
 *
 * WARNING: Data binding can lead to security issues by exposing parts of the object graph
 * that are not meant to be exposed. It is therefore recommended to design your JavaBeans
 * specifically for the purpose of data binding, or to manually initialize the binder with
 * allowed/disallowed fields.
 */
public class WebDataBinder extends DataBinder {
    /**
     * Create a new WebDataBinder instance.
     *
     * @param target the target object to bind onto (or null if the binder
     * is just used to convert a plain parameter value)
     */
    public WebDataBinder(Object target) {}

    /**
     * Create a new WebDataBinder instance.
     *
     * @param target the target object to bind onto (or null if the binder
     * is just used to convert a plain parameter value)
     * @param objectName the name of the target object
     */
    public WebDataBinder(Object target, String objectName) {}

    /**
     * Set whether to bind empty MultipartFile parameters.
     * Default is "true".
     *
     * @param bindEmptyMultipartFiles whether to bind empty multipart files
     */
    public void setBindEmptyMultipartFiles(boolean bindEmptyMultipartFiles) {}

    /**
     * Return whether to bind empty MultipartFile parameters.
     *
     * @return whether to bind empty multipart files
     */
    public boolean isBindEmptyMultipartFiles() {}

    /**
     * Bind the given property values to this binder's target.
     *
     * @param propertyValues the property values to bind
     */
    public void bind(PropertyValues propertyValues) {}

    /**
     * Close this DataBinder, which may result in throwing a BindException if it
     * encountered any errors.
     *
     * @return the model Map, containing target object and BindingResult
     * @throws BindException if there were any errors in the bind operation
     */
    public Map<?, ?> close() throws BindException {}
}

@BindParam

Annotation for binding values from web request parameters or path variables to constructor parameters or fields when instantiating @ModelAttribute objects.

/**
 * Annotation to bind values from a web request such as request parameters or
 * path variables to fields of a Java object. Supported on constructor parameters
 * of @ModelAttribute controller method arguments.
 *
 * @since 6.1
 */
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BindParam {
    /**
     * The lookup name to use for the bind value.
     */
    String value() default "";
}

Usage Example:

// Domain model with constructor binding
public class Product {
    private final Long id;
    private final String name;
    private final BigDecimal price;

    // Constructor-based binding with @BindParam
    public Product(
            @BindParam("id") Long id,
            @BindParam("name") String name,
            @BindParam("price") BigDecimal price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    // Getters
    public Long getId() { return id; }
    public String getName() { return name; }
    public BigDecimal getPrice() { return price; }
}

@Controller
@RequestMapping("/products")
public class ProductController {

    // Use @ModelAttribute with constructor binding
    @PostMapping
    public String createProduct(@ModelAttribute Product product) {
        // Product is instantiated via constructor with @BindParam
        productService.save(product);
        return "redirect:/products";
    }

    @PutMapping("/{id}")
    public String updateProduct(
            @PathVariable Long id,
            @ModelAttribute Product product) {
        productService.update(product);
        return "redirect:/products/" + id;
    }
}

// Field-level @BindParam
public class OrderItem {
    @BindParam("product_id")
    private Long productId;

    @BindParam("quantity")
    private int quantity;

    @BindParam("unit_price")
    private BigDecimal unitPrice;

    // No-arg constructor required for field-level binding
    public OrderItem() {}

    // Getters and setters
}

Types

ObjectError

Represents a global error not associated with a specific field.

public class ObjectError {
    public ObjectError(String objectName, String defaultMessage) {}
    public ObjectError(String objectName, String[] codes, Object[] arguments, String defaultMessage) {}
    public String getObjectName() {}
    public String[] getCodes() {}
    public Object[] getArguments() {}
    public String getDefaultMessage() {}
    public String getCode() {}
}

FieldError

Represents a field-specific error.

public class FieldError extends ObjectError {
    public FieldError(String objectName, String field, String defaultMessage) {}
    public FieldError(String objectName, String field, Object rejectedValue,
                     boolean bindingFailure, String[] codes, Object[] arguments,
                     String defaultMessage) {}
    public String getField() {}
    public Object getRejectedValue() {}
    public boolean isBindingFailure() {}
}