Skip to content
academia.sh

Lesson 09 / 19

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.

Published

In this lesson

  • 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
Contents

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.

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close