I’ve been burned by JSR 380 enough times to appreciate a validation framework that doesn’t drag in a whole ecosystem of dependencies. Solon’s validation system lives in solon-security-validation — a single plugin that gives you 20+ annotations, entity validation, and custom validators, all without pulling in javax.validation or jakarta.validation.
Let me walk through what I’ve found useful.
The Two-Phase Validation Model
Solon separates validation into two phases:
- Context validation (pre-injection) — runs before method parameters are resolved. Annotations on the method itself check headers, cookies, IP, or request-level constraints.
- Parameter validation (post-injection) — runs after parameters are resolved. Annotations on individual parameters or entity fields.
This maps to two different implementation mechanisms:
- Context validation uses
@Addition(Filter.class)under the hood - Parameter validation uses
@Around(Interceptor.class)
You don’t need to think about this most of the time, but it explains why some annotations go on the method and others on the parameter.
Getting Started: @valid on a Controller
Add @Valid to your controller class (or its base class), then annotate the parameters:
@Valid
@Controller
public class UserController {
@Mapping("/user/add")
public void addUser(
@NotNull String name,
@Email String email,
@Pattern("^https?://") String avatarUrl,
@Length(min = 6, max = 20) String password) {
// All parameters validated before this runs
}
}
Enter fullscreen mode Exit fullscreen mode
That’s it. No need to configure a ValidatorFactory or declare a bean. @Valid on the class enables validation for all handler methods.
@Validated for Entity (Nested) Validation
When your parameter is a DTO with its own validation rules, use @Validated:
@Valid
@Controller
public class UserController {
@Mapping("/user/register")
public void register(@Validated RegisterRequest req) {
// req fields are validated
}
}
@Data
public class RegisterRequest {
@NotNull
@Length(min = 2, max = 50)
private String name;
@Email
@NotNull
private String email;
@Validated // Nested entity validation
@NotNull
@Size(min = 1)
private List<Order> orderList;
}
Enter fullscreen mode Exit fullscreen mode
Group Validation
For update vs create scenarios, use groups:
public interface UpdateGroup {}
@Data
public class User {
@NotNull(groups = UpdateGroup.class) // Only required on update
private Long id;
@NotNull
private String name;
}
// Controller
@Valid
@Controller
public class UserController {
@Mapping("/user/update")
public void update(@Validated(UpdateGroup.class) User user) {
// Only validates fields in UpdateGroup
}
}
Enter fullscreen mode Exit fullscreen mode
The 20+ Annotations in One Table
Solon’s validation plugin ships with a comprehensive set of annotations. Here’s the full list:
Annotation Scope Purpose@Valid
Controller class
Enable validation
@Validated
Parameter/field
Validate entity fields
@NotNull
Method/param/field
Not null
@Null
Method/param/field
Is null
@NotBlank
Method/param/field
Not blank string
@NotEmpty
Method/param/field
Not empty string
@NotZero
Method/param/field
Not zero
@Min(value)
Parameter/field
>= value
@Max(value)
Parameter/field
<= value
@DecimalMin(value)
Parameter/field
>= decimal value
@DecimalMax(value)
Parameter/field
<= decimal value
@Length(min, max)
Parameter/field
String length range
@Size
Parameter/field
Collection size range
@Email
Parameter/field
Email format
@Pattern(value)
Parameter/field
Regex match
@Date
Parameter/field
Date format
@Numeric
Parameter/field
Numeric format
@Logined
Controller/method
User is logged in
@NoRepeatSubmit
Controller/method
No duplicate submission
@Whitelist
Controller/method
IP in whitelist
@NotBlacklist
Controller/method
IP not in blacklist
A few of these (@Logined, @NoRepeatSubmit, @Whitelist, @NotBlacklist) are “business-aware” — they need you to implement a checker interface. More on that below.
Validation on Non-Web Components
This surprised me: Solon’s validation also works on plain @Component classes, not just controllers:
@Valid
@Component
public class UserService {
public void addUser(@NotNull String name, @Email String email) {
// Validation works here too
}
}
Enter fullscreen mode Exit fullscreen mode
Manual Validation with ValidUtils
You can also validate programmatically anywhere:
User user = new User();
user.setName(null);
ValidUtils.validateEntity(user); // throws ValidatorException if invalid
Enter fullscreen mode Exit fullscreen mode
Handling Validation Errors
Catch ValidatorException in a filter:
@Component
public class ValidationFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
try {
chain.doFilter(ctx);
} catch (ValidatorException e) {
ctx.render(Result.failure(e.getCode(), e.getMessage()));
}
}
}
Enter fullscreen mode Exit fullscreen mode
By default, validation stops at the first error. If you want to collect all errors, set:
solon.validation.validateAll: true
Enter fullscreen mode Exit fullscreen mode
Business-Aware Validators (The Four Checkers)
Four annotations need you to provide a checker implementation. This is where Solon’s design shines — the framework handles the interception, you just plug in the business logic.
@NoRepeatSubmit
@Component
public class NoRepeatSubmitCheckerImpl implements NoRepeatSubmitChecker {
@Override
public boolean check(NoRepeatSubmit anno, Context ctx,
String submitHash, int limitSeconds) {
return LockUtils.tryLock(Solon.cfg().appName(), submitHash, limitSeconds);
}
}
Enter fullscreen mode Exit fullscreen mode
@Whitelist
@Component
public class WhitelistCheckerImpl implements WhitelistChecker {
@Override
public boolean check(Whitelist anno, Context ctx) {
String ip = ctx.realIp();
return CloudClient.list().inListOfIp("whitelist", ip);
}
}
Enter fullscreen mode Exit fullscreen mode
@NotBlacklist
@Component
public class NotBlacklistCheckerImpl implements NotBlacklistChecker {
@Override
public boolean check(NotBlacklist anno, Context ctx) {
String ip = ctx.realIp();
return !CloudClient.list().inListOfIp("blacklist", ip);
}
}
Enter fullscreen mode Exit fullscreen mode
@Logined
@Component
public class LoginedCheckerImpl implements LoginedChecker {
@Override
public boolean check(Logined anno, Context ctx, String userKeyName) {
return ctx.sessionAsLong("userId") > 0;
}
}
Enter fullscreen mode Exit fullscreen mode
Custom Validators: Creating Your Own Annotation
The framework is designed to be extended. Here’s a complete example of creating a custom @Phone validator:
Step 1: Define the annotation
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
String message() default "Invalid phone number";
Class<?>[] groups() default {};
}
Enter fullscreen mode Exit fullscreen mode
Step 2: Implement the validator
public class PhoneValidator implements Validator<Phone> {
private static final Pattern PHONE_PATTERN =
Pattern.compile("^1[3-9]\\d{9}$");
@Override
public String message(Phone anno) {
return anno.message();
}
@Override
public Class<?>[] groups(Phone anno) {
return anno.groups();
}
@Override
public Result validateOfValue(Phone anno, Object val, StringBuilder tmp) {
if (val == null) return Result.succeed();
if (val instanceof String == false) return Result.failure();
if (PHONE_PATTERN.matcher((String) val).matches()) {
return Result.succeed();
}
return Result.failure();
}
@Override
public Result validateOfContext(Context ctx, Phone anno,
String name, StringBuilder tmp) {
String val = ctx.param(name);
if (val == null) return Result.succeed();
if (PHONE_PATTERN.matcher(val).matches()) {
return Result.succeed();
}
return Result.failure(name);
}
}
Enter fullscreen mode Exit fullscreen mode
Step 3: Register it
@Configuration
public class ValidationConfig {
@Bean
public void registerValidators() {
ValidatorManager.register(Phone.class, new PhoneValidator());
}
}
Enter fullscreen mode Exit fullscreen mode
Step 4: Use it
@Valid
@Controller
public class UserController {
@Mapping("/user/phone")
public void setPhone(@Phone String phone) {
// Validated
}
}
Enter fullscreen mode Exit fullscreen mode
What I’d Change
Two things:
-
No built-in
@Phoneor@URL— common ones you’d expect. Easy to write yourself, but would be nice as pre-built options. -
The
@Validatedannotation name — it does something different from both JSR 380’s@Validand Spring’s@Validated. In Solon,@Validatedis specifically for triggering entity field validation on a parameter. It’s fine once you know, but the naming overlap with JSR 380 caused me a moment of confusion.
Bottom Line
Solon’s validation is a complete, self-contained system. You get 20+ annotations out of the box, entity validation, group validation, and a clean extension point for custom validators — all without a single javax.validation or jakarta.validation dependency.
The business-aware annotations (@Whitelist, @NoRepeatSubmit, @Logined) are the standout feature. They let you enforce security and idempotency constraints at the validation layer rather than scattering checks throughout your handlers.
For a project that wants to keep its dependency graph lean, Solon’s validation is a solid choice.
답글 남기기