What Do We Want To Do? 🤔
Apply validation to the objects we receive at the endpoint controllers from clients.
What Is The First Solution That Comes To Mind? 🧠
it’s simple, right? write functions that validate those objects.
Say we have an endpoint to add new Shipments.
Let’s start with the DTO classes (Data Transfer Object)
<span>@RequiredArgsConstructor</span> <span>@Getter</span><span>public</span> <span>class</span> <span>ShipmentComponentDTO</span> <span>{</span><span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span><span>}</span><span>@RequiredArgsConstructor</span> <span>@Getter</span> <span>public</span> <span>class</span> <span>ShipmentComponentDTO</span> <span>{</span> <span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span> <span>}</span>@RequiredArgsConstructor @Getter public class ShipmentComponentDTO { private final String productCode; }
Enter fullscreen mode Exit fullscreen mode
<span>@RequiredArgsConstructor</span> <span>@Getter</span><span>public</span> <span>class</span> <span>ShipmentDTO</span> <span>{</span><span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span><span>private</span> <span>final</span> <span>int</span> <span>count</span><span>;</span><span>private</span> <span>final</span> <span>List</span><span><</span><span>ShipmentComponentDTO</span><span>></span> <span>shipmentComponentDTOs</span><span>;</span><span>}</span><span>@RequiredArgsConstructor</span> <span>@Getter</span> <span>public</span> <span>class</span> <span>ShipmentDTO</span> <span>{</span> <span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span> <span>private</span> <span>final</span> <span>int</span> <span>count</span><span>;</span> <span>private</span> <span>final</span> <span>List</span><span><</span><span>ShipmentComponentDTO</span><span>></span> <span>shipmentComponentDTOs</span><span>;</span> <span>}</span>@RequiredArgsConstructor @Getter public class ShipmentDTO { private final String productCode; private final int count; private final List<ShipmentComponentDTO> shipmentComponentDTOs; }
Enter fullscreen mode Exit fullscreen mode
Now, the controller
<span>@RestController</span><span>public</span> <span>class</span> <span>ShipmentController</span> <span>{</span><span>@PostMapping</span><span>(</span><span>"/shipment/add"</span><span>)</span><span>public</span> <span>ResponseEntity</span><span><</span><span>String</span><span>></span> <span>addShipment</span><span>(</span><span>@RequestBody</span> <span>ShipmentDTO</span> <span>shipmentDTO</span><span>){</span><span>if</span> <span>(</span><span>shipmentDTO</span><span>.</span><span>getProductCode</span><span>()</span> <span>==</span> <span>null</span><span>)</span><span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>"Product code can't be null"</span><span>);</span><span>if</span> <span>(</span><span>shipmentDTO</span><span>.</span><span>getCount</span><span>()</span> <span><=</span> <span>0</span><span>)</span><span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>"Count can't be negative or zero"</span><span>);</span><span>if</span> <span>(!</span><span>allComponentsAreValid</span><span>(</span><span>shipmentDTO</span><span>))</span><span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>"Component product code can't be null"</span><span>);</span><span>return</span> <span>ResponseEntity</span><span>.</span><span>ok</span><span>(</span><span>":)"</span><span>);</span><span>}</span><span>private</span> <span>boolean</span> <span>allComponentsAreValid</span><span>(</span><span>ShipmentDTO</span> <span>shipmentDTO</span><span>)</span> <span>{</span><span>List</span><span><</span><span>ShipmentComponentDTO</span><span>></span> <span>shipmentComponentDTOs</span> <span>=</span> <span>shipmentDTO</span><span>.</span><span>getShipmentComponentDTOs</span><span>();</span><span>return</span> <span>shipmentComponentDTOs</span> <span>!=</span> <span>null</span> <span>&&</span><span>shipmentComponentDTOs</span><span>.</span><span>size</span><span>()</span> <span>></span> <span>0</span> <span>&&</span><span>shipmentComponentDTOs</span><span>.</span><span>stream</span><span>().</span><span>allMatch</span><span>(</span><span>shipmentComponentDTO</span> <span>-></span> <span>shipmentComponentDTO</span><span>.</span><span>getProductCode</span><span>()</span> <span>!=</span> <span>null</span><span>);</span><span>}</span><span>}</span><span>@RestController</span> <span>public</span> <span>class</span> <span>ShipmentController</span> <span>{</span> <span>@PostMapping</span><span>(</span><span>"/shipment/add"</span><span>)</span> <span>public</span> <span>ResponseEntity</span><span><</span><span>String</span><span>></span> <span>addShipment</span><span>(</span><span>@RequestBody</span> <span>ShipmentDTO</span> <span>shipmentDTO</span><span>){</span> <span>if</span> <span>(</span><span>shipmentDTO</span><span>.</span><span>getProductCode</span><span>()</span> <span>==</span> <span>null</span><span>)</span> <span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>"Product code can't be null"</span><span>);</span> <span>if</span> <span>(</span><span>shipmentDTO</span><span>.</span><span>getCount</span><span>()</span> <span><=</span> <span>0</span><span>)</span> <span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>"Count can't be negative or zero"</span><span>);</span> <span>if</span> <span>(!</span><span>allComponentsAreValid</span><span>(</span><span>shipmentDTO</span><span>))</span> <span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>"Component product code can't be null"</span><span>);</span> <span>return</span> <span>ResponseEntity</span><span>.</span><span>ok</span><span>(</span><span>":)"</span><span>);</span> <span>}</span> <span>private</span> <span>boolean</span> <span>allComponentsAreValid</span><span>(</span><span>ShipmentDTO</span> <span>shipmentDTO</span><span>)</span> <span>{</span> <span>List</span><span><</span><span>ShipmentComponentDTO</span><span>></span> <span>shipmentComponentDTOs</span> <span>=</span> <span>shipmentDTO</span><span>.</span><span>getShipmentComponentDTOs</span><span>();</span> <span>return</span> <span>shipmentComponentDTOs</span> <span>!=</span> <span>null</span> <span>&&</span> <span>shipmentComponentDTOs</span><span>.</span><span>size</span><span>()</span> <span>></span> <span>0</span> <span>&&</span> <span>shipmentComponentDTOs</span><span>.</span> <span>stream</span><span>().</span> <span>allMatch</span><span>(</span><span>shipmentComponentDTO</span> <span>-></span> <span>shipmentComponentDTO</span><span>.</span><span>getProductCode</span><span>()</span> <span>!=</span> <span>null</span><span>);</span> <span>}</span> <span>}</span>@RestController public class ShipmentController { @PostMapping("/shipment/add") public ResponseEntity<String> addShipment(@RequestBody ShipmentDTO shipmentDTO){ if (shipmentDTO.getProductCode() == null) return ResponseEntity.badRequest().body("Product code can't be null"); if (shipmentDTO.getCount() <= 0) return ResponseEntity.badRequest().body("Count can't be negative or zero"); if (!allComponentsAreValid(shipmentDTO)) return ResponseEntity.badRequest().body("Component product code can't be null"); return ResponseEntity.ok(":)"); } private boolean allComponentsAreValid(ShipmentDTO shipmentDTO) { List<ShipmentComponentDTO> shipmentComponentDTOs = shipmentDTO.getShipmentComponentDTOs(); return shipmentComponentDTOs != null && shipmentComponentDTOs.size() > 0 && shipmentComponentDTOs. stream(). allMatch(shipmentComponentDTO -> shipmentComponentDTO.getProductCode() != null); } }
Enter fullscreen mode Exit fullscreen mode
Notice all the manual tedious work we had to do.🤕
Can’t anyone help us make this task more fun?
Please meet the Javax validator.🥳
- It provides a set of annotations that are used with class fields.
- They act as constraints.
- this field should not be null
- this list should have a minimum size of 5
- and many more…
How to do it?
For the object we want to validate
- We annotate the fields we want to validate with annotations from
javax.validation.constraints
-
Then, we annotate the parameter of the controller where we want to validate.
-
We then have to catch the error thrown by the validator, and return a proper response to the user.
Let’s code it 🦾
Here are the DTO objects
<span>@RequiredArgsConstructor</span> <span>@Getter</span><span>public</span> <span>class</span> <span>ShipmentComponentDTO</span> <span>{</span><span>@NotBlank</span><span>(</span><span>message</span> <span>=</span> <span>"Component product code can't be null"</span><span>)</span><span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span><span>// required by the javax validation code </span><span>public</span> <span>ShipmentComponentDTO</span><span>()</span> <span>{</span><span>this</span><span>.</span><span>productCode</span> <span>=</span> <span>""</span><span>;</span><span>}</span><span>}</span><span>@RequiredArgsConstructor</span> <span>@Getter</span> <span>public</span> <span>class</span> <span>ShipmentComponentDTO</span> <span>{</span> <span>@NotBlank</span><span>(</span><span>message</span> <span>=</span> <span>"Component product code can't be null"</span><span>)</span> <span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span> <span>// required by the javax validation code </span> <span>public</span> <span>ShipmentComponentDTO</span><span>()</span> <span>{</span> <span>this</span><span>.</span><span>productCode</span> <span>=</span> <span>""</span><span>;</span> <span>}</span> <span>}</span>@RequiredArgsConstructor @Getter public class ShipmentComponentDTO { @NotBlank(message = "Component product code can't be null") private final String productCode; // required by the javax validation code public ShipmentComponentDTO() { this.productCode = ""; } }
Enter fullscreen mode Exit fullscreen mode
<span>@RequiredArgsConstructor</span> <span>@Getter</span><span>public</span> <span>class</span> <span>ShipmentDTO</span> <span>{</span><span>@NotBlank</span><span>(</span><span>message</span> <span>=</span> <span>"Product code can't be null"</span><span>)</span><span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span><span>@Min</span><span>(</span><span>1</span><span>)</span><span>private</span> <span>final</span> <span>int</span> <span>count</span><span>;</span><span>@Size</span><span>(</span><span>min</span> <span>=</span> <span>1</span><span>,</span> <span>message</span> <span>=</span> <span>"Component product code can't be empty"</span><span>)</span><span>@NotNull</span><span>(</span><span>message</span> <span>=</span> <span>"Component product code can't be null"</span><span>)</span><span>@Valid</span><span>private</span> <span>final</span> <span>List</span><span><</span><span>ShipmentComponentDTO</span><span>></span> <span>shipmentComponentDTOs</span><span>;</span><span>}</span><span>@RequiredArgsConstructor</span> <span>@Getter</span> <span>public</span> <span>class</span> <span>ShipmentDTO</span> <span>{</span> <span>@NotBlank</span><span>(</span><span>message</span> <span>=</span> <span>"Product code can't be null"</span><span>)</span> <span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span> <span>@Min</span><span>(</span><span>1</span><span>)</span> <span>private</span> <span>final</span> <span>int</span> <span>count</span><span>;</span> <span>@Size</span><span>(</span><span>min</span> <span>=</span> <span>1</span><span>,</span> <span>message</span> <span>=</span> <span>"Component product code can't be empty"</span><span>)</span> <span>@NotNull</span><span>(</span><span>message</span> <span>=</span> <span>"Component product code can't be null"</span><span>)</span> <span>@Valid</span> <span>private</span> <span>final</span> <span>List</span><span><</span><span>ShipmentComponentDTO</span><span>></span> <span>shipmentComponentDTOs</span><span>;</span> <span>}</span>@RequiredArgsConstructor @Getter public class ShipmentDTO { @NotBlank(message = "Product code can't be null") private final String productCode; @Min(1) private final int count; @Size(min = 1, message = "Component product code can't be empty") @NotNull(message = "Component product code can't be null") @Valid private final List<ShipmentComponentDTO> shipmentComponentDTOs; }
Enter fullscreen mode Exit fullscreen mode
Ok, what were those annotations?
- NotBlank
- This field can’t be null or empty string
- Min
- this integer should have a min value we specify
- Size
- this array’s size should be within the boundaries we specify
- NotNull
- This field must not be null
- Valid
- Validate the fields in this object
Now, here is the controller
<span>@RestController</span><span>public</span> <span>class</span> <span>ShipmentController_Validation</span> <span>{</span><span>@PostMapping</span><span>(</span><span>"validation/shipment/add"</span><span>)</span><span>public</span> <span>ResponseEntity</span><span><</span><span>String</span><span>></span> <span>addShipment</span><span>(</span><span>@RequestBody</span> <span>@Validated</span> <span>ShipmentDTO</span> <span>shipmentDTO</span><span>){</span><span>return</span> <span>ResponseEntity</span><span>.</span><span>ok</span><span>(</span><span>":)"</span><span>);</span><span>}</span><span>}</span><span>@RestController</span> <span>public</span> <span>class</span> <span>ShipmentController_Validation</span> <span>{</span> <span>@PostMapping</span><span>(</span><span>"validation/shipment/add"</span><span>)</span> <span>public</span> <span>ResponseEntity</span><span><</span><span>String</span><span>></span> <span>addShipment</span><span>(</span><span>@RequestBody</span> <span>@Validated</span> <span>ShipmentDTO</span> <span>shipmentDTO</span><span>){</span> <span>return</span> <span>ResponseEntity</span><span>.</span><span>ok</span><span>(</span><span>":)"</span><span>);</span> <span>}</span> <span>}</span>@RestController public class ShipmentController_Validation { @PostMapping("validation/shipment/add") public ResponseEntity<String> addShipment(@RequestBody @Validated ShipmentDTO shipmentDTO){ return ResponseEntity.ok(":)"); } }
Enter fullscreen mode Exit fullscreen mode
What was that @Validated annotation?🤨
- it tells Spring to run the validation mechanism on this object according to its validation annotations(NotBlank, etc..)
Finally, let’s catch the exception thrown by the validator and generate a proper response
<span>@Order</span><span>(</span><span>Ordered</span><span>.</span><span>HIGHEST_PRECEDENCE</span><span>)</span><span>@ControllerAdvice</span><span>public</span> <span>class</span> <span>ValidationAdvice</span><span>{</span><span>@ResponseStatus</span><span>(</span><span>BAD_REQUEST</span><span>)</span><span>@ResponseBody</span><span>@ExceptionHandler</span><span>(</span><span>MethodArgumentNotValidException</span><span>.</span><span>class</span><span>)</span><span>public</span> <span>ResponseEntity</span><span><?></span> <span>methodArgumentNotValidException</span><span>(</span><span>MethodArgumentNotValidException</span> <span>ex</span><span>)</span> <span>{</span><span>BindingResult</span> <span>result</span> <span>=</span> <span>ex</span><span>.</span><span>getBindingResult</span><span>();</span><span>List</span><span><</span><span>FieldError</span><span>></span> <span>fieldErrors</span> <span>=</span> <span>result</span><span>.</span><span>getFieldErrors</span><span>();</span><span>String</span> <span>errorMessage</span> <span>=</span> <span>fieldErrors</span><span>.</span><span>get</span><span>(</span><span>0</span><span>).</span><span>getDefaultMessage</span><span>();</span><span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>errorMessage</span><span>);</span><span>}</span><span>}</span><span>@Order</span><span>(</span><span>Ordered</span><span>.</span><span>HIGHEST_PRECEDENCE</span><span>)</span> <span>@ControllerAdvice</span> <span>public</span> <span>class</span> <span>ValidationAdvice</span><span>{</span> <span>@ResponseStatus</span><span>(</span><span>BAD_REQUEST</span><span>)</span> <span>@ResponseBody</span> <span>@ExceptionHandler</span><span>(</span><span>MethodArgumentNotValidException</span><span>.</span><span>class</span><span>)</span> <span>public</span> <span>ResponseEntity</span><span><?></span> <span>methodArgumentNotValidException</span><span>(</span><span>MethodArgumentNotValidException</span> <span>ex</span><span>)</span> <span>{</span> <span>BindingResult</span> <span>result</span> <span>=</span> <span>ex</span><span>.</span><span>getBindingResult</span><span>();</span> <span>List</span><span><</span><span>FieldError</span><span>></span> <span>fieldErrors</span> <span>=</span> <span>result</span><span>.</span><span>getFieldErrors</span><span>();</span> <span>String</span> <span>errorMessage</span> <span>=</span> <span>fieldErrors</span><span>.</span><span>get</span><span>(</span><span>0</span><span>).</span><span>getDefaultMessage</span><span>();</span> <span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>errorMessage</span><span>);</span> <span>}</span> <span>}</span>@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class ValidationAdvice{ @ResponseStatus(BAD_REQUEST) @ResponseBody @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<?> methodArgumentNotValidException(MethodArgumentNotValidException ex) { BindingResult result = ex.getBindingResult(); List<FieldError> fieldErrors = result.getFieldErrors(); String errorMessage = fieldErrors.get(0).getDefaultMessage(); return ResponseEntity.badRequest().body(errorMessage); } }
Enter fullscreen mode Exit fullscreen mode
In conclusion️
- Imagine having an API with many endpoints that we need to validate. Which method would be better?
- The Validator.
暂无评论内容