Errors
Handle validation results with the throwing validate() or the non-throwing check() API.
Teki provides two ways to run validation: validate(...) throws on failure, check(...) returns a result object.
validate
validate(...) throws ValidationException when one or more rules fail.
import dev.ditsche.teki.error.ValidationException;
try {
schema.validate(request);
} catch (ValidationException exception) {
exception.getErrors().forEach(error -> {
System.out.println(error.getField());
error.getErrors().forEach(info -> {
System.out.println(info.getType());
System.out.println(info.getMessage());
});
});
}check
check(...) never throws. It returns a ValidationOutcome that carries either the valid object or the accumulated errors.
import dev.ditsche.teki.ValidationOutcome;
ValidationOutcome<SignupRequest> outcome = schema.check(request);
if (outcome.isValid()) {
SignupRequest validated = outcome.getValue();
} else {
outcome.getErrors().forEach(error -> {
System.out.println(error.getField());
});
}Use orElseThrow() to convert back to the throwing path when needed:
SignupRequest validated = schema.check(request).orElseThrow();check(...) accepts the same abort-early flag as validate(...):
ValidationOutcome<SignupRequest> outcome = schema.check(request, true);Error shape
Each validation error contains:
| Value | Description |
|---|---|
field | The field name or nested field path |
errors | One or more validation messages for that field |
type | A stable rule type such as validation.error.required |
message | A human-readable message generated by the rule |
Nested object errors use dotted paths:
customer.email
shippingAddress.postalCodeArray element errors include the zero-based index in square brackets:
tags[2]
items[0].skuCollecting errors
By default, Teki validates every configured field and throws one exception containing all failures.
schema.validate(request);Use abort-early mode to stop on the first failure:
schema.validate(request, true);Abort-early mode is useful for fast request rejection. Full collection is usually better for forms and API clients because it lets callers fix all invalid fields at once.
Custom messages
Use TekiMessages to override built-in English messages without writing a full resolver.
Templates support {field} for the field name and named placeholders for rule params
(e.g. {min}, {max}, {length}, {allowed}).
import dev.ditsche.teki.TekiMessages;
import dev.ditsche.teki.TekiErrors;
// Apply to a single schema
Teki schema = Teki.fromRules(
string("username").required().between(2, 50)
).messages(
TekiMessages.defaults()
.override(TekiErrors.BETWEEN, "Must be between {min} and {max} characters")
.override(TekiErrors.REQUIRED, "{field} is required")
);Set messages globally to apply them across all schemas:
Teki.setGlobalMessages(
TekiMessages.defaults()
.override(TekiErrors.BETWEEN, "Must be between {min} and {max}")
.override(TekiErrors.REQUIRED, "{field} is required")
);Loading from a properties file
Place a .properties file on the classpath and pass its path to fromProperties. Each key
is a stable error type (see TekiErrors); each value is the template string.
# i18n/messages_de.properties
format.required = Das Feld "{field}" ist erforderlich
format.email = Das Feld "{field}" muss eine gültige E-Mail sein
between = Muss zwischen {min} und {max} liegen
size.min = Das Feld "{field}" muss mindestens {min} betragenTeki.setGlobalMessages(
TekiMessages.defaults()
.fromProperties("i18n/messages_de.properties")
);fromProperties merges into the existing templates — keys present in the file override the
defaults; keys not in the file keep their current values.
Error type keys
All built-in error type keys are available as constants on TekiErrors:
| Constant | Key |
|---|---|
TekiErrors.REQUIRED | format.required |
TekiErrors.EMAIL | format.email |
TekiErrors.URL | format.url |
TekiErrors.IP_ADDRESS | format.ip |
TekiErrors.UUID | format.uuid |
TekiErrors.CREDIT_CARD | format.creditcard |
TekiErrors.ALPHA_NUMERIC | format.alphanum |
TekiErrors.PATTERN | format.pattern |
TekiErrors.STRING | type.string |
TekiErrors.NUMBER | type.number |
TekiErrors.BOOLEAN | type.boolean |
TekiErrors.ARRAY | type.array |
TekiErrors.NOT_BLANK | string.not_blank |
TekiErrors.ONE_OF | string.one_of |
TekiErrors.POSITIVE | number.positive |
TekiErrors.POSITIVE_OR_ZERO | number.positive_or_zero |
TekiErrors.NEGATIVE | number.negative |
TekiErrors.NEGATIVE_OR_ZERO | number.negative_or_zero |
TekiErrors.MIN | size.min |
TekiErrors.MAX | size.max |
TekiErrors.LENGTH | size.length |
TekiErrors.BETWEEN | between |
TekiErrors.PAST | temporal.past |
TekiErrors.PAST_OR_PRESENT | temporal.past_or_present |
TekiErrors.FUTURE | temporal.future |
TekiErrors.FUTURE_OR_PRESENT | temporal.future_or_present |
TekiErrors.BEFORE | temporal.before |
TekiErrors.AFTER | temporal.after |
Full programmatic control
For cases that can't be expressed as templates — custom rule type keys, dynamic logic — implement
MessageResolver directly. Return null for any type you haven't handled to fall through to
the next resolver in the chain.
import dev.ditsche.teki.MessageResolver;
schema.messages((field, type, params) -> {
if ("my.custom.rule".equals(type)) return "Custom error for " + field;
return null; // fall through to global messages / rule default
});