Skip to content

Validation

Jan Bernitt edited this page Jan 30, 2024 · 6 revisions

Tip

This page is written so that later sections build upon earlier ones. For inexperienced users it is recommended to read it start to finish.

Features

Most of the validations specified by the JSON schema are supported. However, since annotations are used to add validation to a property method some of the validations work slightly different than specified so that each targets the annotated method's value. For example required is specified on each required property instead of specifying a list of required properties on their parent. This is also to avoid string name references to be required that are not safe to refactor.

Validations include:

Target JSON schema @Validation attribute Description
(any) type type() annotated property must have on of the JSON node types (with INTEGER being a distinct type)
(any) enum/const oneOfValues() + enumeration() annotated property's value must be one of a given set of JSON values or the given enum constant names
(any) required required() or use @Required annotated property must be present (unconditionally), that means defined and non-null
(any) dependentRequired dependentRequired() annotated property must be present (conditionally). Conditions are modeled using named groups and roles in this group (details below)
STRING minLength minLength() annotated text property must have a minimum length, this implicitly makes it required unless required explicitly states NO
STRING maxLength maxLength() annotated text property must not be longer than the maximum length (inclusive)
STRING pattern pattern() annotated text property must match the given regex pattern
NUMBER minimum minimum() annotated number property must be larger than or equal to the minimum value
NUMBER maximum maximum() annotated number property must be less than or equal to the maximum value
NUMBER exclusiveMinimum exclusiveMinimum() annotated number property must be larger than the minimum value
NUMBER exclusiveMaximum exclusiveMaximum() annotated number property must be less than the maximum value
NUMBER multipleOf multipleOf() annotated number property must dividable by the given number without rest
ARRAY minItems minItems() annotated array property must have at least the number of elements, this implicitly makes it required unless required explicitly states NO
ARRAY maxItems maxItems() annotated array property must have at most the number of elements
ARRAY uniqueItems uniqueItems() annotated array property must not have any duplicates (same "normalized" JSON)
OBJECT minProperties minProperties() annotated object property must have at least the number of members, this implicitly makes it required unless required explicitly states NO
OBJECT maxProperties maxProperties() annotated object property must have at most the number of members

Note

If a validation for a target type is used but that type does not apply based on the return type analysis or the type(s) declared via type() the validation has no effect as it does not apply to any valid value for the property.

Usage

The primary way to add validation to a property is to annotate the method with @Validation and use one or more of the annotation attributes to add validations.

interface User extends JsonObject {

  @Validation(pattern = "[a-z]+", minLength=8, maxLength=20)
  default String username() {
    return getString("username").string();
  }
}

Alternatively to @Validation meta-annotations can be used. These are annotations that themselves are annotated with @Validation. @Required is an example of such a meta-annotation. Users can easily define their own.

For example,

@Target( ElementType.METHOD )
@Retention( RetentionPolicy.RUNTIME )
@Validation( required = NO )
public @interface Optional {}

Implicitly Required

An annotation like @Optional can make sense since minLength, minItems and minProperties all make a property implicitly required. That means they are required as long as their required evaluates to AUTO (the default). With an @Optional annotation like above user then can explicitly declare it non-required.

All properties that use primitive return types are also considered implicitly required. But they might use a default so they are actually not required, for example:

interface User extends JsonObject {

  @Optional
  default boolean expired() {
    return getBoolean("expired").booleanValue(false);
  }
}

Precedence

@Validation restrictions can be declared in several places.

Validation that are always given for a specific Java type can be annotated on that type.

@Validation(type=STRING, pattern="[a-zA-Z0-9]{11}")
record ID(String id) {}

interface User extends JsonObject {

  default ID getId() {
   return getString("id").parsed(ID::new);
  }
}

Or the annotation is placed on the property method or its return return type

interface User extends JsonObject {

  @Required
  default List<@Validation(maxLength=20) String> names() {
    return getArray("names").stringValues();
  }
}

Here names is required and each name has a maxLength of 20.

Important

When multiple annotations are affecting a property the restrictions declared on the method and return type override the restrictions declared on Java types. However, independent restrictions are merged. For example, using the ID type limits type to STRING and requires a certain pattern to match. If a property using ID is now annotated @Required the required=YES restriction is added on top of the restriction from the ID Java type. If on the other hand ID would also include required=YES and the method gets annotated @Optional the required=NO from the @Optional annotation overrides the required=YES declared for ID.

Item Validation

For complex JSON types ARRAY and OBJECT the items can be restricted independently from the container. Either by annotating the container with @Items or by using return type annotations.

interface User extends JsonObject {

  @Items(@Validation(maxLength=20))
  default List<String> names() { return getArray("names").stringValues(); }
}

is identical to

interface User extends JsonObject {

  default List<@Validation(maxLength=20) String> names() { return getArray("names").stringValues(); }
}

The benefit of using the latter is that add validations to multiple levels

interface User extends JsonObject {

  default List<@Validation(minItems=1) List<@Validation(uniqueItems=YES) Point> points() {
    // ...
  }
}

@Items can also be used to create dedicated complex types, for example:

@Validation(type=ARRAY)
@Items(@Validation(type=INTEGER))
interface IntList extends JsonList<JsonInteger> { }

Custom Validators

If no combination of the provided validations is sufficient users can declare their own using @Validator. The annotation refers to a record class that implements the @Validation.Validator interface.

record DateValidator() implements Validation.Validator {
  public void validate( JsonMixed value, Consumer<Error> addError ) {
    if (!value.isString()) return;
    try { 
      LocalDate.parse(value.string());
    } catch (Exception ex) {
      addError.accept(Error.of(Rule.CUSTOM, value, "not a valid date: %s", value.string());
    }
  }
}

Important

Custom validators should always assume they are called with any type of JSON node depending on what was present in the actual input. It is good practice to ignore types that are not suited for validation according to the value's semantic. Any type mismatches should be caught by using @Validation(type=).

To use a custom validator simply annotate the property with @Validator

interface User extends JsonObject {
  
  @Validator( DateValidator.class )
  default String dateOfBirth() {
    return getString("dateOfBirth").string();
  }
}

Users can equally define their own validator meta-annotations

@Target( ElementType.METHOD )
@Retention( RetentionPolicy.RUNTIME )
@Validator( DateValidator.class )
public @interface Date {}

to reuse the same validation without having to link to the specific validator in all usage sites

interface User extends JsonObject {
  
  @Date
  default String dateOfBirth() {
    return getString("dateOfBirth").string();
  }
}

Dependent Required

Clone this wiki locally