Skip to content

Commit 22a0f5a

Browse files
authored
Merge pull request #3508 from kliushnichenko/feat/jooby-hibernate-validator
Feat/jooby hibernate validator
2 parents c4f7ea6 + 4d73e31 commit 22a0f5a

31 files changed

Lines changed: 1429 additions & 3 deletions
Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
== Hibernate Validator
2+
3+
Bean validation via https://hibernate.org/validator/[Hibernate Validator].
4+
5+
=== Usage
6+
7+
1) Add the dependency:
8+
9+
[dependency, artifactId="jooby-hibernate-validator"]
10+
.
11+
12+
2) Install
13+
14+
.Java
15+
[source, java, role="primary"]
16+
----
17+
import io.jooby.hibernate.validator.HibernateValidatorModule;
18+
19+
{
20+
install(new HibernateValidatorModule());
21+
}
22+
----
23+
24+
.Kotlin
25+
[source, kt, role="secondary"]
26+
----
27+
import io.jooby.hibernate.validator.HibernateValidatorModule
28+
29+
{
30+
install(new HibernateValidatorModule())
31+
}
32+
----
33+
34+
3) Usage in MVC routes
35+
36+
.Java
37+
[source,java,role="primary"]
38+
----
39+
import io.jooby.annotation.*;
40+
import jakarta.validation.Valid;
41+
42+
@Path("/mvc")
43+
public class Controller {
44+
45+
@POST("/validate-body")
46+
public void validateBody(@Valid Bean bean) { // <1>
47+
...
48+
}
49+
50+
@POST("/validate-query")
51+
public void validateQuery(@Valid @QueryParam Bean bean) { // <2>
52+
...
53+
}
54+
55+
@POST("/validate-list")
56+
public void validateList(@Valid List<Bean> beans) { // <3>
57+
...
58+
}
59+
60+
@POST("/validate-map")
61+
public void validateMap(@Valid Map<String, Bean> beans) { // <4>
62+
...
63+
}
64+
}
65+
----
66+
67+
.Kotlin
68+
[source, kt, role="secondary"]
69+
----
70+
import io.jooby.annotation.*;
71+
import jakarta.validation.Valid
72+
73+
@Path("/mvc")
74+
class Controller {
75+
76+
@POST("/validate-body")
77+
fun validateBody(@Valid bean: Bean) : Unit { // <1>
78+
...
79+
}
80+
81+
@POST("/validate-query")
82+
fun validateQuery(@Valid @QueryParam bean: Bean) : Unit { // <2>
83+
...
84+
}
85+
86+
@POST("/validate-list")
87+
fun validateList(@Valid beans: List<Bean>) : Unit { // <3>
88+
...
89+
}
90+
91+
@POST("/validate-map")
92+
fun validateMap(@Valid beans: Map<String, Bean>) : Unit { // <4>
93+
...
94+
}
95+
}
96+
----
97+
98+
<1> Validate a bean decoded from the request body
99+
<2> Validate a bean parsed from query parameters. This works the same for `@FormParam` or `@BindParam`
100+
<3> Validate a list of beans. This also applies to arrays `@Valid Bean[] beans`
101+
<4> Validate a map of beans
102+
103+
4) Usage in in script/lambda routes
104+
105+
Jooby doesn't provide fully native bean validation in script/lambda at the moment,
106+
but you can use a helper that we utilize under the hood in MVC routes:
107+
108+
.Java
109+
[source, java, role="primary"]
110+
----
111+
import io.jooby.validation.BeanValidator;
112+
113+
{
114+
post("/validate", ctx -> {
115+
Bean bean = BeanValidator.validate(ctx, ctx.body(Bean.class));
116+
...
117+
});
118+
}
119+
----
120+
121+
.Kotlin
122+
[source, kt, role="secondary"]
123+
----
124+
import io.jooby.validation.BeanValidator
125+
126+
{
127+
post("/validate") {
128+
val bean = BeanValidator.validate(ctx, ctx.body(Bean.class))
129+
...
130+
}
131+
}
132+
----
133+
134+
`BeanValidator.validate()` behaves identically to validation in MVC routes.
135+
It also supports validating list, array, and map of beans
136+
137+
=== Constraint Violations Rendering
138+
139+
`HibernateValidatorModule` provides default built-in error handler that
140+
catches `ConstraintViolationException` and transforms it into the following response:
141+
142+
.JSON:
143+
----
144+
{
145+
"title": "Validation failed",
146+
"status": 422,
147+
"errors": [
148+
{
149+
"field": "firstName",
150+
"messages": [
151+
"must not be empty",
152+
"must not be null"
153+
],
154+
"type": "FIELD"
155+
},
156+
{
157+
"field": null,
158+
"messages": [
159+
"passwords are not the same"
160+
],
161+
"type": "GLOBAL"
162+
}
163+
]
164+
}
165+
----
166+
167+
It is possible to override the `title` and `status` code of the response above:
168+
169+
[source, java]
170+
----
171+
172+
{
173+
install(new JacksonModule());
174+
install(new HibernateValidatorModule()
175+
.statusCode(StatusCode.BAD_REQUEST)
176+
.validationTitle("Incorrect input data")
177+
);
178+
}
179+
----
180+
181+
If the default error handler doesn't fully meet your needs, you can always disable it and provide your own:
182+
183+
[source, java]
184+
----
185+
186+
{
187+
install(new JacksonModule());
188+
install(new HibernateValidatorModule().disableViolationHandler());
189+
190+
error(ConstraintViolationException.class, new MyConstraintViolationHandler());
191+
}
192+
----
193+
194+
=== Manual Validation
195+
196+
The module exposes `Validator` as a service, allowing you to run validation manually at any time.
197+
198+
==== Script/lambda:
199+
200+
[source, java]
201+
----
202+
import jakarta.validation.Validator;
203+
204+
{
205+
post("/validate", ctx -> {
206+
Validator validator = require(Validator.class);
207+
Set<ConstraintViolation<Bean>> violations = validator.validate(ctx.body(Bean.class));
208+
if (!violations.isEmpty()) {
209+
...
210+
}
211+
...
212+
});
213+
}
214+
----
215+
216+
==== MVC routes with dependency injection:
217+
218+
1) Install DI framework at first.
219+
220+
[source, java]
221+
----
222+
import io.jooby.hibernate.validator.HibernateValidatorModule;
223+
224+
{
225+
install(new GuiceModule()); // <1>
226+
install(new HibernateValidatorModule());
227+
}
228+
----
229+
230+
<1> `Guice` is just an example, you can achieve the same with `Avaje` or `Dagger`
231+
232+
2) Inject `Validator` in controller, service etc.
233+
234+
[source, java]
235+
----
236+
import jakarta.validation.Validator;
237+
import jakarta.inject.Inject;
238+
239+
@Path("/mvc")
240+
public class Controller {
241+
242+
private final Validator validator;
243+
244+
@Inject
245+
public Controller(Validator validator) {
246+
this.validator = validator;
247+
}
248+
249+
@POST("/validate")
250+
public void validate(Bean bean) {
251+
Set<ConstraintViolation<Bean>> violations = validator.validate(bean);
252+
...
253+
}
254+
}
255+
----
256+
257+
=== Business rules validation
258+
259+
As you know, `Hibernate Validator` allows you to build fully custom `ConstraintValidator`.
260+
In some scenarios, you may need access not only to the bean but also to services, repositories, or other resources
261+
to perform more complex validations required by business rules.
262+
263+
In this case you need to implement a custom `ConstraintValidatorFactory` that will rely on your DI framework
264+
instantiating your custom `ConstraintValidator`
265+
266+
1) Implement custom `ConstraintValidatorFactory`:
267+
268+
[source, java]
269+
----
270+
public class MyConstraintValidatorFactory implements ConstraintValidatorFactory {
271+
272+
private final Function<Class<?>, ?> require;
273+
private final ConstraintValidatorFactory defaultFactory;
274+
275+
public MyConstraintValidatorFactory(Function<Class<?>, ?> require) {
276+
this.require = require;
277+
try (ValidatorFactory factory = Validation.byDefaultProvider()
278+
.configure().buildValidatorFactory()) {
279+
this.defaultFactory = factory.getConstraintValidatorFactory();
280+
}
281+
}
282+
283+
@Override
284+
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
285+
if (isBuiltIn(key)) {
286+
// use default factory for built-in constraint validators
287+
return defaultFactory.getInstance(key);
288+
} else {
289+
// use DI to instantiate custom constraint validator
290+
return (T) require.apply(key);
291+
}
292+
}
293+
294+
@Override
295+
public void releaseInstance(ConstraintValidator<?, ?> instance) {
296+
if(isBuiltIn(instance.getClass())) {
297+
defaultFactory.releaseInstance(instance);
298+
} else {
299+
// No-op: lifecycle usually handled by DI framework
300+
}
301+
}
302+
303+
private boolean isBuiltIn(Class<?> key) {
304+
return key.getName().startsWith("org.hibernate.validator");
305+
}
306+
}
307+
----
308+
309+
2) Register your custom `ConstraintValidatorFactory`:
310+
311+
[source, java]
312+
----
313+
{
314+
install(new HibernateValidatorModule().doWith(cfg -> {
315+
cfg.constraintValidatorFactory(new MyConstraintValidatorFactory(this::require)); // <1>
316+
}));
317+
}
318+
----
319+
320+
<1> This approach using `require` will work with `Guice` or `Avaje`. For `Dagger`, a bit more effort is required,
321+
but the concept is the same, and the same result can be achieved. Both `Avaje` and `Dagger` require additional
322+
configuration due to their build-time nature.
323+
324+
325+
3) Implement your custom `ConstraintValidator`
326+
327+
[source, java]
328+
----
329+
public class MyCustomValidator implements ConstraintValidator<MyCustomAnnotation, Bean> {
330+
331+
// This is the service you want to inject
332+
private final MyService myService;
333+
334+
@Inject
335+
public MyCustomValidator(MyService myService) {
336+
this.myService = myService;
337+
}
338+
339+
@Override
340+
public boolean isValid(Bean bean, ConstraintValidatorContext context) {
341+
// Use the injected service for validation logic
342+
return myService.isValid(bean);
343+
}
344+
}
345+
----
346+
347+
=== Configuration
348+
Any property defined at `hibernate.validator` will be added automatically:
349+
350+
.application.conf
351+
[source, properties]
352+
----
353+
hibernate.validator.fail_fast = true
354+
----
355+
356+
Or programmatically:
357+
358+
[source, java]
359+
----
360+
import io.jooby.hibernate.validator.HibernateValidatorModule;
361+
362+
{
363+
install(new HibernateValidatorModule().doWith(cfg -> {
364+
cfg.failFast(true);
365+
}));
366+
}
367+
----

docs/asciidoc/modules/modules.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ Available modules are listed next.
2626
* link:/modules/kafka[Kafka]: Kafka module.
2727
* link:/modules/redis[Redis]: Redis module.
2828

29+
=== Validation
30+
* link:/modules/hibernate-validator[Hibernate Validator]: Hibernate Validator module.
31+
2932
=== Development Tools
3033
* link:#hot-reload[Jooby Run]: Run and hot reload your application.
3134
* link:/modules/whoops[Whoops]: Pretty page stacktrace reporter.

modules/jooby-apt/pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@
2626
<scope>test</scope>
2727
</dependency>
2828

29+
<dependency>
30+
<groupId>io.jooby</groupId>
31+
<artifactId>jooby-validation</artifactId>
32+
<version>${jooby.version}</version>
33+
<scope>test</scope>
34+
</dependency>
35+
36+
<dependency>
37+
<groupId>jakarta.validation</groupId>
38+
<artifactId>jakarta.validation-api</artifactId>
39+
<scope>test</scope>
40+
</dependency>
41+
2942
<!-- Test dependencies -->
3043
<dependency>
3144
<groupId>com.google.testing.compile</groupId>

0 commit comments

Comments
 (0)