Validation belongs in a Model Object

An application should perform data validation in a Model Object, where it can validate both user input, and the data being fetched from a database. This validation should be performed in a class, and not in an .xml configuration file or some similar tool.

The role of a Model Object is to implement business logic, and validation is the single most common (and arguably the most important) kind of business logic. The fundamental reason for creating any class is often stated as bringing together data and closely related operations on that data. This is often the very first idea taught in courses on object programming.

Surprisingly, there are many application frameworks that do not follow this simple guiding principle of lasting value. Instead, they encourage the application programmer to separate data and logic, by doing validation either in a separate class, Java Server Pages, configuration files, and so on. Such styles of validation should usually be avoided, if possible.

If you're using a framework that expects validation to be implemented in a distasteful way, you may find it possible to define your core data-validation in your own way, and then simply call-forward from framework classes to your own classes.

Implementing validation in Model Objects is simple and natural. As usual, any common validations can be factored out into utility classes. In addition, writing test cases is simple, and can be executed stand-alone, without needing any special setup or environment.

Example 1

In the Java programming language, the most natural way of doing data validation seems to be the following:

This is implemented in the example below. Validation is done in the constructor. The policy for communicating problems to the caller uses a checked exception that carries all of the details in its suppressed exceptions (which are part of Throwable). (Checked exceptions are used when a condition is outside the direct control of your program; user input is such a case.)

import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsLast;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
 An example of a fairly robust implementation of a Model object.
 
 @since Java 8  
*/
public final class Telescope implements Comparable<Telescope>{
  
  /** Design of a telescope's optics */
  enum Design {
    NEWTONIAN, REFRACTOR, SCHMIDT_CASSEGRAIN
  }
  
  /**
   Constructor that validates all data.
   
   If one or more problems are found, then a checked Exception is thrown. 
   Furthermore, the details of each problem found will be available 
   via Throwable.getSuppressed(). This lets the caller show all 
   details to the end user. 
   
   <P>WARNING: some frameworks force you into putting such 
   validation-code outside the Model Object. That's distasteful. 
   The fundamental idea of object programming is to UNITE data 
   and the code that acts closely upon it, not to separate them. 

   @param id database identifier. Can be empty, but never null. 
    A new item that has not yet been saved to the database will 
    have an empty id.
   @param name brand name of the scope; must have visible content 
   @param cost in dollars, greater than or equal to 0; nullable
   @param datePurchased, in range 1900-01-01..2099-12-31
   @param aperture in millimeters, in range 0..10000
   @param design optical design of the scope
  */
  public Telescope(
    String id, String name, BigDecimal cost, LocalDate datePurchased, 
    Double aperture, Design design
  ) throws Exception {
    this.id = id; 
    this.name = name;
    this.cost = cost; 
    this.datePurchased = datePurchased;
    this.aperture = aperture;
    this.design = design;
    validate();
  }
  
  /** 
   In the case of new objects which don't yet have an id, this method 
   will return an empty String. 
  */
  public String getId() {
    return id;
  }
  
  public String getName() { 
    return name; 
  }
  
  /** Returns an Optional because the field may be null. */
  public Optional<BigDecimal> getCost() { 
    return Optional.ofNullable(cost); 
  }
  
  public LocalDate getDatePurchased() { 
    return datePurchased; 
  }
  
  public Double getAperture() { 
    return aperture; 
  }
  
  public Design getDesign() { 
    return design; 
  }

  @Override public boolean equals(Object aThat) {
    //unusual: multiple return statements
    if (this == aThat) return true;
    if (!(aThat instanceof Telescope)) return false;
    Telescope that = (Telescope)aThat;
    for(int i = 0; i < this.getSigFields().length; ++i){
      if (!Objects.equals(this.getSigFields()[i], that.getSigFields()[i])){
        return false; 
      }
    }
    return true;
  }  
  
  @Override public int hashCode() {
    return Objects.hash(getSigFields());
  }

  /** Intended for debugging only. */
  @Override public String toString() {
    return toStringUtil(this, getSigFields());
  }
  
  /** 
   Implementing compareTo is only needed if you need to sort collections
   of these objects. 
  */
  @Override public int compareTo(Telescope that) {
    int result = COMPARATOR.compare(this, that);
    //optional: you may want to include this assertion (at least during development)
    //note that assertions are disabled by default
    if (result == 0) {
      assert this.equals(that) : 
        this.getClass().getSimpleName() + ": compareTo inconsistent with equals."
      ;
    }
    return result;
  }
  
  // PRIVATE 

  /** 
   Database identifier (not a business identifier).
   New items, that don't yet have an id assigned, have 
   this field as an empty String (not null).
  */
  private final String id;
  
  private final String name;
  
  /**
   This is the only nullable field in this class. 
   Nullable data is very common!
   Not a floating point type, since it's money. 
  */
  private final BigDecimal cost; 
  
  /** java.util.Date is obsolete and of low quality; avoid it in new code. */
  private final LocalDate datePurchased; 
  
  private final Double aperture;
  
  private final Design design;
  
  /** 
   The id is left out! The id is analogous to an object's identity, and is 
   treated here as NOT forming part of the object's core state (its data).
   IMPORTANT: equals and hashCode both call this method; they need to 
   talk to the same fields. In addition, compareTo uses the same fields
   too (without calling this method, however).
  */
  private Object[] getSigFields() {
    //optimize: start with items that are most likely to differ
    return new Object[] {
      name, cost, datePurchased, aperture, design
    };
  }

  /** Static: avoid creating this object every time a comparison is made.*/
  private static Comparator<Telescope> COMPARATOR = getComparator();
  
  /**
   Note the 'thenComparing' chain: when comparing, the implementation goes 
   to the next level of comparison ONLY IF the previous level has 
   returned '0' (equal).
   
   <P>Note that Telescope::getCost can't be used below, since it returns 
   an Optional, and the Comparator methods don't play well with Optional.
   Choice: either use a lambda expression to reference the data directly, 
   or define a private method (getPlainCost()) to return the nullable object.
  */
  private static Comparator<Telescope> getComparator(){
    //use the same fields used by the equals method, if at all possible!
    Comparator<Telescope> result = 
      comparing(Telescope::getName)
      .thenComparing(Telescope::getPlainCost, nullsLast(naturalOrder()))
      /*.thenComparing(t -> t.cost, nullsLast(naturalOrder()))*/ //alternative form
      .thenComparing(Telescope::getDatePurchased)
      .thenComparing(Telescope::getAperture)
      .thenComparing(Telescope::getDesign)
    ;
    return result;
  }
  
  /** 
   Created only so that a method reference expression (instead a lambda expression) 
   can be used in the getComparator() method above. 
   Not needed if you want to use a lambda expression!
  */
  private BigDecimal getPlainCost() { 
    return cost; 
  }
 
  /**
   Validate all of the data passed to the constructor.
   Throws an Exception if one or more problems are found.
   (You may want to define a new Exception type  
   to represent this very specific kind of error.)
   
   All of the constraints are defined by the constructor's javadoc (above).
   You shouldn't repeat those constraints here.
  */
  private void validate() throws Exception {
    List<String> errors = new ArrayList<>();
    ensureNotNull(id, "Id is null", errors);
    
    if (!hasContent(name)) {
      errors.add("Name has no content.");
    }
    
    if (cost != null) { 
      if (cost.compareTo(BigDecimal.ZERO) < 0) {
        errors.add("Cost cannot be negative.");
      }
    }
    
    boolean passes = ensureNotNull(datePurchased, "Date purchased is null.", errors);
    if (passes) {
      LocalDate START = LocalDate.of(1900, 1, 1);
      LocalDate END = LocalDate.of(2099, 12, 31);
      if (datePurchased.isBefore(START) || datePurchased.isAfter(END)) {
        errors.add("Date purchased is outside the normal range: " + START + ".." + END);
      }
    }
    
    passes = ensureNotNull(aperture, "Aperture is null", errors);
    if (passes) {
      Double START = 0.0;
      Double END = 10000.0;
      if (aperture.compareTo(START) < 0 || aperture.compareTo(END) > 0) {
        errors.add("Aperture is outside the normal range: " + START + ".." + END) ;
      }
    }
    
    ensureNotNull(design, "Design is null", errors);
    
    if (!errors.isEmpty()) {
      Exception ex = new Exception();
      for(String error: errors) {
        ex.addSuppressed(new Exception(error));
      }
      throw ex;
    }
  }
  
  /** Returns true only if the field passes the test, and is NOT null. */
  private boolean ensureNotNull(Object field, String errorMsg, List<String> errors) {
    boolean result = true;
    if (field == null) {
      errors.add(errorMsg);
      result = false;
    }
    return result;
  }
  
  /** 
   WARNING: This method is extremely common, and should be in a utility class.
   (It really should be in the JDK, as a static method of the String class.)
  */
  private boolean hasContent(String string) {
    return (string != null && string.trim().length() > 0);
  }
 
  /** 
   There's a lot of variation in how you might want to implement toString().
   Since the output is almost always intended only for debugging, it's 
   usually a matter of taste.
   
   <P>This implementation has the defect that the names of fields aren't included 
   in the output.
   
   <P>Example output for this class:<br>
   <pre>Telescope: [Meade 100.00 2018-09-01 200.0 REFRACTOR]</pre> 
   
   @param fields the fields to be included in the output 
   @param thing the object itself (which provides the class name)
   @return text describing the object, suitable only for logging and debugging
  */
  private String toStringUtil(Object thing, Object[] fields) {
    String result = "";
    for(Object field : fields) {
      result = result + " " + Objects.toString(field); //null-friendly
    }
    return thing.getClass().getSimpleName() + ": ["+ result.trim() + "]";
  }
} 

Example 2

To assist the application programmer with common validations, the WEB4J tool defines a Validator interface, and a Check class which returns some common implementations of that interface. Validation is implemented entirely in code, and never with JSPs or configuration files. Here, a Resto Model Object uses Check to perform all required validation in its constructor, by calling validateState:

package hirondelle.fish.main.resto;

import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.ModelUtil;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.model.Decimal;
import static hirondelle.web4j.model.Decimal.ZERO;
import hirondelle.web4j.model.Check;
import hirondelle.web4j.model.Validator;
import static hirondelle.web4j.util.Consts.FAILS;

/** Model Object for a Restaurant. */
public final class Resto {

  /**
   Full constructor.
    
   @param aId underlying database internal identifier (optional) 1..50 characters
   @param aName of the restaurant (required), 2..50 characters
   @param aLocation street address of the restaurant (optional), 2..50 characters
   @param aPrice of the fish and chips meal (optional) $0.00..$100.00
   @param aComment on the restaurant in general (optional) 2..50 characters
  */
  public Resto(
    Id aId, SafeText aName, SafeText aLocation, Decimal aPrice, SafeText aComment
  ) throws ModelCtorException {
    fId = aId;
    fName = aName;
    fLocation = aLocation;
    fPrice = aPrice;
    fComment = aComment;
    validateState();
  }
  
  public Id getId() { return fId; }
  public SafeText getName() {  return fName; }
  public SafeText getLocation() {  return fLocation;  }
  public Decimal getPrice() { return fPrice; }
  public SafeText getComment() {  return fComment; }

  @Override public String toString(){
    return ModelUtil.toStringFor(this);
  }
  
  @Override public  boolean equals(Object aThat){
    Boolean result = ModelUtil.quickEquals(this, aThat);
    if (result ==  null) {
      Resto that = (Resto) aThat;
      result = ModelUtil.equalsFor(
        this.getSignificantFields(), that.getSignificantFields()
      );
    }
    return result;
  }
  
  @Override public int hashCode(){
    if (fHashCode == 0){
      fHashCode = ModelUtil.hashCodeFor(getSignificantFields());
    }
    return fHashCode;
  }
  
  // PRIVATE 
  private final Id fId;
  private final SafeText fName;
  private final SafeText fLocation;
  private final Decimal fPrice;
  private final SafeText fComment;
  private int fHashCode;
  
  private static final Decimal HUNDRED = Decimal.from("100");

  private void validateState() throws ModelCtorException {
    ModelCtorException ex = new ModelCtorException();
    if (FAILS == Check.optional(fId, Check.range(1,50))) {
      ex.add("Id is optional, 1..50 chars.");
    }
    if (FAILS == Check.required(fName, Check.range(2,50))) {
      ex.add("Restaurant Name is required, 2..50 chars.");
    }
    if (FAILS == Check.optional(fLocation, Check.range(2,50))) {
      ex.add("Location is optional, 2..50 chars.");
    }
    Validator[] priceChecks = {Check.range(ZERO, HUNDRED), Check.numDecimalsAlways(2)};
    if (FAILS == Check.optional(fPrice, priceChecks)) {
      ex.add("Price is optional, 0.00 to 100.00.");
    }
    if (FAILS == Check.optional(fComment, Check.range(2,50))) {
      ex.add("Comment is optional, 2..50 chars.");
    }
    if ( ! ex.isEmpty() ) throw ex;
  }
  
  private Object[] getSignificantFields(){
    return new Object[] {fName, fLocation, fPrice, fComment};
  }
}
 

See Also :
JSPs should contain only presentation logic
Model Objects