---
title: 'Validation and Error Handling'
source: 'https://academia.sh/en/courses/enterprise-application-framework/validation-and-error-handling'
course: 'The Enterprise Application Framework'
language: en
updated: '2026-09-13T05:08:57+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows that three of four fields take their validation rule from an annotation, and one carries no rule at all'
    - "Separates, when the rule is broken, that the common error response's envelope comes from the container's default while the field messages come from the annotation itself"
    - 'Measures that no rule is applied at all, and the defect stays silent, when the same invalid object is given directly to the handler outside the container'
---

# Validation and Error Handling

Three of four fields take their validation rule from an annotation, one carries no rule at all. When the rule is broken, the common error response's envelope comes from the object container's default, while the field messages come from the annotation itself; when the same invalid object is given directly to the handler outside the container, no rule is applied at all and the defect stays silent.

The previous lesson measured which method a request reaches, but it never asked whether the
data reaching that method is in the correct form. This lesson takes this up: it measures
whether a field's validation rule is a call or an annotation in the source, and, when the
rule is broken, which fields the common error response gathers and from where.

Validation here is subject to the same three-source question too: is a field's rule written
as an expression in the source, is it only marked with a sign, or does the container pass
over the field without saying anything about it at all? This lesson adds a fourth
possibility — a field having no rule at all — and this adds a measurement different from the
previous two lessons: the container reveals not only where the rules come from, but also
which fields carry no rule at all.

Error handling is also this lesson's subject, in two senses: gathering which rule a request
broke (validation), and giving that gathered result back to the caller in some form (error
handling). Though they look like separate questions, both live inside the same container —
when the container rejects a request, it has to both find which fields are broken and turn
that into a single, consistent response. In this lesson, both jobs merge into the same
`request` method, but their sources stay separate: which rule was broken comes from the
annotation, how the broken rules get gathered together is the container's own decision.

## Three of Four Fields Take Their Rule from an Annotation

- **WD11.** Rule checking is done only at the field level; method parameters, nested
  objects, or collection elements are not validated here.
- **WD12.** A field can have more than one rule, but in this lesson every field carries at
  most one; how rules order one another is a separate measurement's subject.

`RegistrationRequest` carries four fields: `name`, `quantity`, `category`, `code`. The first
three carry, respectively, the `@RequiredField`, `@PositiveValue`, and `@Options`
annotations; the fourth, `code`, carries no annotation at all. There is no call anywhere in
the source that validates these four fields — the `validate` method does not know which
fields are subject to which rule, it only goes through every field one by one, through
reflection, and checks which annotation sits on it. Since no annotation is found for the
`code` field, this field is never validated — any value requested for it is accepted.

```java
// Container.java — validation rules arise from annotations, the common error response is the container's default
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

public class Container {

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
    @interface RequiredField { String message() default "field cannot be empty"; }

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
    @interface PositiveValue { String message() default "value must be greater than zero"; }

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
    @interface Options { String[] value(); String message() default "unrecognized option"; }

    interface Handler { String run(Object request); }

    List<String> validate(Object object) throws ReflectiveOperationException {
        List<String> errors = new ArrayList<>();
        for (Field f : object.getClass().getDeclaredFields()) {
            f.setAccessible(true);
            Object value = f.get(object);

            RequiredField r = f.getAnnotation(RequiredField.class);
            if (r != null && (value == null || value.toString().isBlank()))
                errors.add(f.getName() + ": " + r.message());

            PositiveValue p = f.getAnnotation(PositiveValue.class);
            if (p != null && value instanceof Integer i && i <= 0)
                errors.add(f.getName() + ": " + p.message());

            Options o = f.getAnnotation(Options.class);
            if (o != null && !Arrays.asList(o.value()).contains(value))
                errors.add(f.getName() + ": " + o.message());
        }
        return errors;
    }

    String request(Object requestObject, Handler handler) throws ReflectiveOperationException {
        List<String> errors = validate(requestObject);
        if (!errors.isEmpty())
            return "error; field-count=" + errors.size() + "; fields=" + errors;
        return handler.run(requestObject);
    }

    static int ruledFieldCount(Class<?> type) {
        int n = 0;
        for (Field f : type.getDeclaredFields())
            if (f.isAnnotationPresent(RequiredField.class) || f.isAnnotationPresent(PositiveValue.class)
                    || f.isAnnotationPresent(Options.class)) n++;
        return n;
    }

    public static void main(String[] args) throws Exception {
        Container container = new Container();
        Handler handler = request -> {
            RegistrationRequest r = (RegistrationRequest) request;
            return "registration-ok: name=" + r.name + ", quantity=" + r.quantity + ", category=" + r.category;
        };

        System.out.println("-- how many rules come from an annotation --");
        int totalFields = RegistrationRequest.class.getDeclaredFields().length;
        int ruledFields = ruledFieldCount(RegistrationRequest.class);
        System.out.println("total fields: " + totalFields);
        System.out.println("fields carrying a rule via annotation: " + ruledFields);

        RegistrationRequest valid = new RegistrationRequest("screw", 12, "electronics", 501);
        System.out.println("-- valid request --");
        System.out.println(container.request(valid, handler));

        RegistrationRequest invalid = new RegistrationRequest("", -3, "furniture", 502);
        System.out.println("-- invalid request: through the container --");
        System.out.println(container.request(invalid, handler));

        System.out.println("-- same invalid object, given directly to the handler (container bypassed) --");
        System.out.println(handler.run(invalid));
        System.out.println("defect type: silent (no exception, invalid data reached the handler)");
    }
}

class RegistrationRequest {
    @Container.RequiredField String name;
    @Container.PositiveValue int quantity;
    @Container.Options(value = {"food", "electronics", "clothing"}) String category;
    long code;

    RegistrationRequest(String name, int quantity, String category, long code) {
        this.name = name;
        this.quantity = quantity;
        this.category = category;
        this.code = code;
    }
}
```

```
-- how many rules come from an annotation --
total fields: 4
fields carrying a rule via annotation: 3
-- valid request --
registration-ok: name=screw, quantity=12, category=electronics
-- invalid request: through the container --
error; field-count=3; fields=[name: field cannot be empty, quantity: value must be greater than zero, category: unrecognized option]
-- same invalid object, given directly to the handler (container bypassed) --
registration-ok: name=, quantity=-3, category=furniture
defect type: silent (no exception, invalid data reached the handler)
```

Three of the four fields carry a rule, one does not. This ratio is itself a measurement:
someone reading the source looks at the `code` field and sees no constraint, and that is a
correct reading — the rule really does not exist, the container never validates it. The
situation is different for the other three fields: there is no `if` anywhere in the source
that says `name` cannot be empty, no comparison that says `quantity` has to be greater than
zero. These rules are written only in the annotations, and without the `validate` method
that reads them, they have no effect at all. The difference between the valid request ending
in `registration-ok` and the invalid request coming back with three separate errors is proof
that these three rules — invisible in any line of the code — really run.

The `code` field carrying no rule can be read either as a shortcoming or as a deliberate
choice. The source text does not distinguish the two — when a field has no annotation on
it, the difference between "this field was deliberately left open" and "we forgot to write
the rule" leaves no trace in the source at all. In the previous two lessons, what was
missing was a behavior (a link, an endpoint); here what is missing is a **constraint**, and
the absence of a constraint never shows itself as an error — only as a silence in which
every value is accepted. Three of four fields carrying a rule does not mean the remaining
one was reviewed with the same care; this is why keeping a deliberate list of fields left
without a rule requires a discipline separate from the rule count itself.

## The Common Error Response: Envelope Default, Content Annotation

- **WD13.** The error response's envelope (the form `"error; field-count=...;
  fields=..."`) is written into the body of the `request` method; no annotation decides
  this form.

The response given to an invalid request has two parts, and both come from a different
source. The envelope — the form that says how many errors the response carries and wraps
them as a list — is a string pattern written into the body of the `Container.request`
method itself; this is from the same family as the default behaviors in the previous
lessons: no field in the source requested it, the container produces this form on its own
for every invalid request. The content is different: the strings `"name: field cannot be
empty"` and `"category: unrecognized option"` come directly from the `message` field of the
`RequiredField` and `Options` annotations. Neither of these two annotations specified a
custom message, so their own default messages were used — but if a developer had written
`@Container.RequiredField(message = "a registration cannot be created without a name")`,
that text would have appeared in the response. The envelope is fixed, the content is in the
annotation's hands.

This distinction has a practical consequence: to change the common error response's form
(for instance, putting the field count at the end instead of the start), the container's own
code has to be changed; no annotation affects this. But to change a single field's error
message, there is no need to touch the container at all — the annotation's `message` field
is enough. The two kinds of change sit in two separate places in the source, and one is far
cheaper than the other: changing the message text is editing an annotation parameter,
changing the envelope is changing the container itself.

There is also a reverse side to this distinction: the `validate` method gathers the errors,
but it also decides which rule gets checked first — here this matches the field declaration
order, `name`, then `quantity`, then `category`. This order, too, is the container's own
decision; no annotation says "check me first." The ordering looks inconsequential, because
in this lesson all rules are gathered together and given in a single response — none of
them stops another. But if a container were designed to "stop at the first error," this
order would decide which error reaches the caller and which is never seen at all; the order
would then stop being an implementation detail and become the behavior itself. The "gather
all of them" approach chosen in this lesson does not carry that risk, but the choice belongs
to the container itself — nowhere in the source does it say "gather all errors."

## Validation Does Not Work at All Outside the Container

- **WD14.** The call `handler.run(invalid)` never sees the `Container.request` method;
  `validate` is never called at all.
- **WD15.** The `code` field carries no rule to begin with, so it is never validated in
  any setup; the difference shows up only in the other three fields.

The last measurement gives the same invalid object — empty `name`, negative `quantity`,
unrecognized `category` — directly to the handler without ever putting the container into
play. The result: `registration-ok: name=, quantity=-3, category=furniture`. No exception,
no error message — the handler works with the same fidelity on invalid data, because its own
code contains no validation at all. The validation logic lives only inside the
`Container.request` method, and as long as that method is never called, none of the three
annotations is ever read.

This is another example of the course's recurring bounding rule: the guarantee a container
provides holds only along the path where that container is called. The annotations on the
`RegistrationRequest` class were not removed, they still stand there — but as long as the
`validate` call that would read them is not put into play, the class is no different from an
ordinary data carrier. This produces a somewhat riskier outcome than what was seen in the
previous two lessons, because here invalid data does not merely produce a wrong response —
it produces a response that looks valid, one that says "ok." When the rule is broken, a
request going through the container clearly sees an error; the same data coming from
outside the container continues with an "ok" response without any warning at all.
Validation existing is a guarantee only as long as there is a single entry point that
enforces it; once there is more than one entry point — here, the two paths of going through
the container and being called directly — the guarantee stays dependent only on which path
was used.

This traces to the same root as the bounding measurements in the previous two lessons, but
its consequence is a degree heavier. In the first lesson, a link left outside the container
only lost its order; in the second, a manually called controller only failed to see the
container's registration state — both kept working correctly at the functional level. Here,
though, a call left outside the container processes invalid data **as if it were valid**:
the string `handler` returns carries the same `registration-ok` prefix as the string a
correct request would return. The caller cannot tell the two cases apart by looking at the
response's form — telling them apart requires separately checking whether the `name` field
is empty, which itself amounts to rewriting validation outside the container. In an
application, this kind of second entry point usually shows up as a background job, a batch
import step, or a direct call from another component; whatever form it takes, the container
has no knowledge of its existence and the rule never runs on it at all.

## Summary

- Three of four fields (`name`, `quantity`, `category`) took their validation rule from an
  annotation; the fourth (`code`) carried no rule and was never validated.
- When the invalid request went through the container, it produced a common error
  response; the response's envelope was fixed in the container's own code, while the field
  messages came from the annotations' `message` field.
- Changing an annotation's message required only editing the annotation; changing the
  envelope would have required changing the container's own code.
- When the same invalid object was given directly to the handler outside the container, no
  rule was applied at all, and the handler produced a response meaning "ok"; the defect
  stayed silent.
- Validation's guarantee holds only along the path where the container is called; a call
  outside the container can use the same class with no constraint at all.

## Next Step

This lesson measured whether a request's data is correct, but it never asked where that
data gets written once it is processed. The next lesson takes this up: it measures how the
container supplies the implementation of a repository interface, and how the same caller
can run against two separate implementations without ever being recompiled.
