Teki

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:

ValueDescription
fieldThe field name or nested field path
errorsOne or more validation messages for that field
typeA stable rule type such as validation.error.required
messageA human-readable message generated by the rule

Nested object errors use dotted paths:

customer.email
shippingAddress.postalCode

Array element errors include the zero-based index in square brackets:

tags[2]
items[0].sku

Collecting 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} betragen
Teki.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:

ConstantKey
TekiErrors.REQUIREDformat.required
TekiErrors.EMAILformat.email
TekiErrors.URLformat.url
TekiErrors.IP_ADDRESSformat.ip
TekiErrors.UUIDformat.uuid
TekiErrors.CREDIT_CARDformat.creditcard
TekiErrors.ALPHA_NUMERICformat.alphanum
TekiErrors.PATTERNformat.pattern
TekiErrors.STRINGtype.string
TekiErrors.NUMBERtype.number
TekiErrors.BOOLEANtype.boolean
TekiErrors.ARRAYtype.array
TekiErrors.NOT_BLANKstring.not_blank
TekiErrors.ONE_OFstring.one_of
TekiErrors.POSITIVEnumber.positive
TekiErrors.POSITIVE_OR_ZEROnumber.positive_or_zero
TekiErrors.NEGATIVEnumber.negative
TekiErrors.NEGATIVE_OR_ZEROnumber.negative_or_zero
TekiErrors.MINsize.min
TekiErrors.MAXsize.max
TekiErrors.LENGTHsize.length
TekiErrors.BETWEENbetween
TekiErrors.PASTtemporal.past
TekiErrors.PAST_OR_PRESENTtemporal.past_or_present
TekiErrors.FUTUREtemporal.future
TekiErrors.FUTURE_OR_PRESENTtemporal.future_or_present
TekiErrors.BEFOREtemporal.before
TekiErrors.AFTERtemporal.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
});

On this page