-
Notifications
You must be signed in to change notification settings - Fork 4
Validation
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.
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.
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 {}
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);
}
}
@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
.
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> { }
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();
}
}