Skip to content

Formal Validation with @Validation

Jan Bernitt edited this page May 20, 2026 · 14 revisions

This page is mainly meant as a overview or presentation around the introduction of formal input validation using @Validation as introduced by #23754.

Formal Input Validation

  • context-less (static context)
  • no state
  • no data related validation (just the input itself)
  • no semantic validation

Typical examples

  • property is required, e.g. name
  • number must be be positive, e.g. page
  • text must match a pattern, e.g. a UID
  • set of properties must be defined together, e.g. cc+co => coc

What is @Validation?

@Validation is a declarative Java-API for (a subset of) JSON-schema-validation

πŸ₯³ we can extract JSON-schema conform JSON and include it in our OpenAPI documentation to inform clients about expectations that is both human and machine readable

It follows: If input is formally wrong, the caller made an avoidable mistake => BadRequestException

What extend of JSON-schema-validation is supported?

@Validation supports most of the fundamental validations 1:1 as far as they make sense in the context of mapping JSON to Java

The Rules include

  • type (JSON node type)
  • enum (JSON equals from a list of options)
  • string: minLength, maxLength, pattern
  • number: minimum, exclusiveMinimum, maximum, exclusiveMaximum, multipleOf
  • array: minItems, maxItems, uniqueItems
  • object: minProperties, maxProperties, required, dependentRequired

Custom validations can be added using @Validator implementing @Validation.Validator

Where does it apply?

  • In #23754 validation from @Validation is first added to URL request parameter objects
  • Opt-in: by using records that implement UrlParams
  • can easily be extended to request bodies (as validation is JSON based)

Examlple MinMaxDataElementParams

record MinMaxDataElementParams(
    List<String> fields, List<String> filters, @Collapsed PagedParams paged) 
    implements UrlParams {

  public static final MinMaxDataElementParams DEFAULT =
      new MinMaxDataElementParams(List.of(), List.of(), PagedParams.DEFAULT);
}

But: Only supports records to model "objects"

Why @Validation? (and not bean-validation or alike)

  • uniform validation of URL request parameters and request bodies
  • uniform API: @Validation + @Validator
  • full generic support (e.g. List<@Validation(maxLength=5)String>
  • consistent behaviour as validation and type mapping go hand in hand
  • validation occurs on input (JSON) not Java (which might have lost information or failed to translate masking errors)
  • easy to extend and customize: @Validator + @Validation.Validator + meta-annotations
  • low overhead, fail fast/early
  • allows mixing JSON and Java types
  • full control on this critical feature remains in DHIS2 hands

Meta-Annotations

Declaration of meta-annotations:

  @Retention(RUNTIME)
  @Validation(minLength = 1, minItems = 1, minProperties = 1)
  @interface NotEmpty {}

Usage:

record MyParams(@NotEmpty String text) {}

A test shows how to re-create common bean annotations.

πŸ’‘ meta-annotations do support parametrization (see example)

Custom Validators

Use @Validator refering to a Class implementing @Validation.Validator

record NoKeywordsValidator {
    @Override
    public void validate(JsonMixed value, Consumer<Error> addError) {
      if (!value.isString()) return;
      if (List.of("select", "delete").stream().anyMatch(keyword -> value.text().contains(keyword)))
        addError.accept(Error.of(Rule.CUSTOM, value, "Value contains keyword %s", value.text()));
    }
}

Usage:

record QueryParams(
  @Validator(NoKeywordsValidator.class) String query) {}

πŸ’‘ validators do support parametrization (see example)

Validation & Mapping to Java

  • JSON + Java types can be mixed
  • allows "delayed" validation
record ImportParams(
  // header
  UID de,
  UID ou,
  // body/data
  JsonArray elements
) {}

In the service...

record Entry(UID coc, @Validation(minimum=0) Number value) {}

void import(ImportParams params) {
  params.elements.forEach(e -> {
    // validate
    e.validate(Entry.class);
    // map
    Entry entry = e.to(Entry.class));
    // ...
  }
}

πŸ’‘ allows to validate thought the same mechanism while also stream-processing large inputs.

elements and e exist in the original input context, e.g. we can get the index of each e for error handling etcetera

Q&A

  • use of meta-annotations

Clone this wiki locally