Since beginning of time mankind has been looking for a way to separate right from wrong. Where the primeval man judged righteousness by the contributions of the tribe, the current day programmer judges right by the wishes of the customer. For many years the average programmer wrote a bunch of logic to check if the boundaries defined by the client where uphold. As time went on and programming languages involved, metadata could be added to enrich functions, methods, classes and the like.

Of course for Java, these metadata are called annotations. Very soon they were used for a lot of things. Surpressing warnings, managing transactions, building XML/JSON structures and injecting dependencies. And, as you might have guessed by now, validating objects by a set of specific rules. One of the most commonly used frameworks would be the Jakarta Bean Validation framework. But what if I told you the provided annotations of that framework could be very easily expanded.

You asked for a case, we brought you a case

To dive into this subject, let’s use following case to clear things up. A group of historians asked us to simulate the days of yore. The year is 55.000 BC and the world is inhabited by the Neanderthal species. As these people are different from us, research found out all Neanderthals follow these rules[1]:

  1. A Neanderthal has one or two legs.

  2. A Neanderthal must have a name.

  3. Most of the Neanderthals are part of a tribe, but some Neanderthals do live on their own.

  4. As the Neanderthal language consists mosly of growls:

    1. The name should start with one 'g' or 'ch' at least one vowel should follow and it should end with one 'r', 'k', or 'p'.

    2. The tribal name should start with one r', 'k', or 'p' at least one vowel should follow and it should end with one 'g' or 'ch'.

  5. The hair of a Neanderthal can grow till maximum of 1 meter.

Create civilizations that are indistinguishable from reality

This simulation could be written in whatever programming language, so Java will do the job just fine. The Bean Validation framework gives us a handful of annotations to conform to these rules:

public class Neanderthal {

  @Min(1)
  @Max(2)
  public int legs;

  @NotEmpty
  @Pattern(regexp="(g|ch)[aeiou]+(r|k|p)")
  public String name;

  @Pattern(regexp="(r|k|p)[aeiou]+(g|ch)")
  public String tribalName;

  @Max(100)
  public int hairLength;

}

This works quite well, but those regexes tend to be quite unreadable. As this one is relatively simple, this could be good enough. But what if a scientist discovers the Neanderthal language is more complicated than we think right now. It would be much easier to understand if we could just define the annotations on the name as a bunch of rules:

@NotEmpty
@StartsWith({"g", "ch"})
@Contains({"a", "e", "i", "o", "u"})
@EndsWith({"r", "k", "p"})
public String name;

That looks already a lot better to me. If you read this code aloud, even those hairy Neanderthal you are creating right now would understand what’s going on. There is a drawback though, those annotations simply don’t exist. Maybe we could create them ourselves? And if so, wouldn’t it be absolutely awesome if we should go one step further and make a @NeanderthalName annotation which consists of above annotations.

Just smile, just slay and just show

Luckily enough, the Bean Validation framework makes it quite easy to expand its constraints. By defining a custom annotation and validator ourselves, the framework can call the validator and check if the field is valid. This sounds a little complicated, so let’s create a custom @Contains class annotation as example:

@Retention(RUNTIME)
@Constraint(validatedBy = ContainsValidator.class)
public @interface Contains {

  String message() default "Value does not consist of any required arguments";

  Class<?>[] groups() default { };

  Class<? extends Payload>[] payload() default { };

  String[] value();

}

When we examine the code above, some things stand out. As the validation is done once we create and update objects, we need to use the @Retention(RUNTIME) annotation to indicate the validator is used at runtime. Then by defining the @Constraint annotation, we link the annotation to a validator the framework is going to call. The properties message, groups and payload are required. The message will be returned once the validation fails, the groups can be used to specify validation groups and the payload can be used to assign custom payload objects to a constraint[2]. The value property will be the value the programmer defined as annotation argument.

Never find fault, always validate

To make our custom annotation usable, the validator listed in the @Constraint annotation must be created as well. The validator is just a class that implements the ConstraintValidator interface. This interface defines two methods, the initialize method which acts like a constructor and the isValid method which holds the actual validation logic. So, let’s implement the ContainsValidator:

public class ContainsValidator implements ConstraintValidator<Contains, String> {

  private String[] conditions;

  @Override
  public void initialize(final Contains constraintAnnotation) {
    this.conditions = constraintAnnotation.value();
  }

  @Override
  public boolean isValid(final String value, final ConstraintValidatorContext context) {
    if (value == null || value.isEmpty()) {
      return true;
    }

    for (String condition : conditions) {
      if (value.contains(condition)) {
        return true;
      }
    }

    return false;
  }

}

For the initialize method, we grab from our annotation the value the programmer defined. So when contains is used as a single argument like @Contains("A"), the value would be ["A"] and when used with multiple arguments like @Contains({"A", "B"}) the value would be ["A", "B"].

Now for the validation method, two things are worth mentioning. The first thing to remember, null and empty values should always be considered valid. Even if one could argue an empty value does not match the condition, you should just use the combination of @NotNull or @NotEmpty with your own defined annotions to achieve this[3]. The second thing, return true when the condition is met. This seems quite obvious, but once your conditions are getting more complicated, accidentally returning the inverse value is done quite easily.

A concept is a brick

The @Contains annotation is hereby finished and usable. As writing the @StartsWith and @EndsWith annotation can be done at exactly the same way, I’ll leave that to the reader as a nice exercise. Once all three finished, we can replace the @Pattern annotations with our own created ones! But instead of doing so, let’s go the extra mile and create the @NeanderthalName annotation. The annotation should represent a composition of other annotations. As we saw before when writing the @Contains annotation, annotations can be placed at interface level when we define a new annotation. So let’s do just that:

@Retention(RUNTIME)
@Constraint(validatedBy = {})
@StartsWith({"g", "ch"})
@Contains({"a", "e", "i", "o", "u"})
@EndsWith({"r", "k", "p"})
@ReportAsSingleViolation
public @interface NeanderthalName {

    String message() default "The name of this neanderthal cannot be pronounced correctly";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

}

The @NeanderthalName could still need some explanation. To let the Bean Validation framework know we want to create a composition of constraints, we explicitly define the @Constraint annotation without a validator. After that, we list all our just constraint annotations one by one. At this point our constraints are no different from any other validation annotation. Say we want the name be no longer than 20 chars, we could just add @Size(max=20). The optional @ReportAsSingleViolation annotion follows next; it makes the framework show the error message of this annotation instead of showing the underlying error messages. Since that we covered all the annotations, do also note the @NeanderthalName annotation does not have a value, because the programmer cannot set an argument for this annotation.

Now you know how to create composition of constraints, you can create the @TribalName yourself. Building upon this knowlegde, you could also create several layered annotations[4]. Building layer upon layer, your constraints can be endless.

Become the architect of your future

And we have done it! We extended the default annotations with our own. Now everything is in place, updating the class’s code is the last thing left to do:

public class Neanderthal {

  @Min(1)
  @Max(2)
  public int legs;

  @NotEmpty
  @NeanderthalName
  public String name;

  @TribalName
  public String tribalName;

  @Max(100)
  public int hairLength;

}

As the code is easily readable now and bugs hence are scarce, the Neanderthal simulation shall turn out to be a success. And once the digital caveman turns upwards to gaze the stars, let’s do the same and ponder about right and wrong…​


1. Just some fiction of mine of course, nothing about these rules has anything to do with science.
2. Going into more details would be beyond the scope of this blog.
3. Not everyone follows this rule though, even some common extension libraries like java-bean-validation-extension do ignore this rule. But in my opinion it is just wrong, because use cases like "I want this field be either null/empty or follow rule x" should also be supported.
4. For example, you could create a @NotStartsWith annotation that’s build upon the @StartsWith(inverse = true) annotation and use that one in you @NeanderthalName annotation.
shadow-left