A Java validation library for runtime parameter validation with zero dependencies.
Fluent, opinionated API for validating Java Records. Method chains align nicely when formatted with Google Java Format.
Maven:
<dependency>
<groupId>io.github.ag-libs.validcheck</groupId>
<artifactId>validcheck</artifactId>
<version>0.11.0</version>
</dependency>Gradle:
implementation 'io.github.ag-libs.validcheck:validcheck:0.11.0'Use ValidCheck in record compact constructors to validate constructor parameters.
Fail-Fast - Throws on first validation failure:
public record User(String username, String email, int age) {
public User {
ValidCheck.require()
.notNull(username, "username")
.hasLength(username, 3, 20, "username")
.matches(email, "(?i)^[\\w._%+-]+@[\\w.-]+\\.[A-Z]{2,}$", "email")
.inRange(age, 13, 120, "age");
}
}Error messages do not include actual values, making them safe for logs and API responses.
Batch - Collects all errors before throwing:
public record UserRegistration(String username, String email, String password) {
public UserRegistration {
ValidCheck.check()
.notNull(username, "username")
.hasLength(username, 3, 20, "username")
.matches(email, "(?i)^[\\w._%+-]+@[\\w.-]+\\.[A-Z]{2,}$", "email")
.hasLength(password, 8, 100, "password")
.matches(password, ".*[A-Z].*", "password")
.validate();
}
}ValidCheck supports two validation strategies:
Fail-Fast - Throws on first validation failure:
ValidCheck.require()
.notNull(value, "field")
.isPositive(number, "count");Batch - Collects all errors before throwing:
ValidCheck.check()
.notNull(value, "field")
.isPositive(number, "count")
.validate();// Null checks
.notNull(value, "field")
.isNull(value, "field")
.notEmpty(text, "text")
.notBlank(text, "text")
// Range validation
.inRange(number, min, max, "field")
.isPositive(number, "field") // > 0 (excludes zero)
.isNegative(number, "field") // < 0 (excludes zero)
.isNonNegative(number, "field") // >= 0 (includes zero)
.isNonPositive(number, "field") // <= 0 (includes zero)
.min(number, minValue, "field") // >= minValue (single bound)
.max(number, maxValue, "field") // <= maxValue (single bound)
// String validation
.hasLength(text, min, max, "field")
.matches(text, pattern, "field")
// Collection validation
.hasSize(collection, min, max, "field")
.notEmpty(collection, "field")
// Assertions
.assertTrue(condition, "message")
.assertFalse(condition, "message")Each validation method has three overloads:
- Named:
.notNull(value, "fieldName") - Message supplier:
.notNull(value, () -> "custom message") - Parameter-less:
.notNull(value)- uses "parameter" as field name
Validation methods return the validator instance for chaining:
ValidCheck.require()
.notNull(user, "user")
.notNull(user.getName(), "name")
.hasLength(user.getName(), 1, 50, "name")
.matches(user.getEmail(), EMAIL_PATTERN, "email")
.isPositive(user.getAge(), "age");Use when() to apply validations conditionally:
ValidCheck.check()
.notNull(user, "user")
.when(user != null && user.isAdmin(),
v -> v.hasLength(user.getUsername(), 10, 30, "admin username"))
.validate();Use nullOr* methods to validate values that may be null. These methods pass if the value
is null or satisfies the validation:
public record UserProfile(
String username, // Required
String bio, // Optional - can be null
Integer age, // Optional - can be null
List<String> skills // Optional - can be null
) {
public UserProfile {
ValidCheck.check()
// Required fields
.notEmpty(username, "username")
// Optional fields - null is allowed, but if present must be valid
.nullOrNotBlank(bio, "bio") // null OR not blank
.nullOrHasLength(bio, 10, 500, "bio") // null OR 10-500 chars
.nullOrInRange(age, 13, 120, "age") // null OR 13-120
.nullOrNotEmpty(skills, "skills") // null OR not empty
.nullOrHasSize(skills, 1, 10, "skills") // null OR 1-10 items
.validate();
}
}Available conditional methods:
nullOrNotEmpty()- String, Collection, Map variantsnullOrNotBlank()- String validationnullOrHasLength()- String length validationnullOrHasSize()- Collection size validationnullOrInRange()- Numeric range validationnullOrIsPositive()/nullOrIsNegative()- Sign validationnullOrIsNonNegative()/nullOrIsNonPositive()- Sign validation (includes zero)nullOrMatches()- Pattern matchingnullOrMin()/nullOrMax()- Single-bound validation
Message suppliers are evaluated only when validation fails:
ValidCheck.require()
.assertTrue(isValid(data),
() -> "Validation failed for complex data: " + data.toString());Use message suppliers to override default error messages:
ValidCheck.require()
.notNull(value, () -> "Custom field cannot be null")
.hasLength(text, 5, 20, () -> "Custom field must be 5-20 characters");Use include() to combine multiple validation contexts:
BatchValidator userValidator = ValidCheck.check()
.notNull(username, "username");
BatchValidator emailValidator = ValidCheck.check()
.matches(email, EMAIL_PATTERN, "email");
ValidCheck.check()
.include(userValidator)
.include(emailValidator)
.validate();Validation methods can be called without a field name. The error message will use "parameter" as the field name:
ValidCheck.require()
.notNull(value) // "parameter must not be null"
.isPositive(number) // "parameter must be positive"
.hasLength(text, 5, 20); // "parameter must have length between 5 and 20"All validation failures throw ValidationException (or FastValidationException if configured)
which contains structured error information.
try {
ValidCheck.require().isPositive(-5, "age");
} catch (ValidationException e) {
System.out.println(e.getMessage());
// "'age' must be positive"
List<ValidationError> errors = e.getErrors();
// [ValidationError{field="age", message="must be positive"}]
}try {
ValidCheck.check()
.notNull(null, "username")
.isPositive(-1, "age")
.validate();
} catch (ValidationException e) {
System.out.println(e.getMessage());
// "'username' must not be null; 'age' must be positive"
List<ValidationError> errors = e.getErrors();
// [ValidationError{field="username", message="must not be null"},
// ValidationError{field="age", message="must be positive"}]
}ValidationError provides structured access to field names and error messages:
try {
ValidCheck.check()
.notNull(null, "username")
.isPositive(-1, "age")
.validate();
} catch (ValidationException e) {
// Access structured error information
for (ValidationError error : e.getErrors()) {
String field = error.field(); // "username", "age"
String message = error.message(); // "must not be null", ...
String formatted = error.toString(); // "'username' must not be null"
}
// Group errors by field for API responses
Map<String, List<String>> errorsByField = e.getErrors().stream()
.filter(err -> err.field() != null)
.collect(Collectors.groupingBy(
ValidationError::field,
Collectors.mapping(ValidationError::message, Collectors.toList())
));
// {"username": ["must not be null"],
// "age": ["must be positive"]}
}ValidCheck allows you to throw custom exception types instead of the default
ValidationException. Pass an exception factory function to requireWith() or checkWith():
// Fail-fast with IllegalArgumentException
ValidCheck.requireWith(errors -> new IllegalArgumentException(ValidationError.join(errors)))
.notNull(null, "value")
.isPositive(-1, "count"); // throws IllegalArgumentException
// Batch validation with custom exception and formatting
ValidCheck.checkWith(errors -> {
String message = errors.stream()
.map(e -> e.field() + ": " + e.message())
.collect(Collectors.joining("\n- ",
"Validation failed:\n- ", ""));
return new MyCustomException(message);
})
.notNull(null, "username")
.isPositive(-1, "age")
.validate(); // throws MyCustomExceptionThe exception factory receives a List<ValidationError> with structured error information:
field()- the field name or nullmessage()- the error message without field nametoString()- formatted as "'field' message"join(errors)- convenience method to join all errors with "; " separator
This approach is useful when:
- Integrating with frameworks expecting specific exceptions (Spring's
IllegalArgumentException, Jakarta Bean Validation) - Building REST APIs that need custom error response formats
- Adding correlation IDs or context to exceptions
For rare high-throughput scenarios where stack traces are not needed, use FastValidationException:
// Fail-fast without stack traces (better performance)
ValidCheck.requireWith(FastValidationException::new)
.notNull(apiKey, "apiKey")
.hasLength(apiKey, 32, 64, "apiKey");
// Batch validation without stack traces
ValidCheck.checkWith(FastValidationException::new)
.notNull(username, "username")
.isPositive(age, "age")
.validate();FastValidationException skips stack trace generation, improving performance in:
- High-frequency API request validation
- Performance-critical validation paths
Complete examples available in the examples module:
- User Registration - Record validation with batch processing, conditional validation for optional fields, and single-bound validation examples
- Java 11+
- Zero dependencies
This project uses AI assistance in development. See AI.md for details.
Licensed under the Apache License, Version 2.0. See LICENSE for details.