This is a utility library for validating JSON HTTP request body for Java web applications.
Request validator allows you to specify one or more rules that a request body must satisfy before
it can be processed further in the controller.
Major Advantages of Request Validator includes:
- You can validate deeply nested data using simple dot notations
- You'll get ALL the violations at a time and not one after the other
- You can provide more than one rule at the same time without littering your code with annotations
- It's a simple plug-and-play that can work with any web framework, just invoke the validate method and you're good to go
<dependency>
<groupId>com.smattme</groupId>
<artifactId>request-validator</artifactId>
<version>0.0.2</version>
</dependency>
The main entry point is the static RequestValidator.validate()
method. This method does not raise an exception, rather it returns a
List<String>
of errors. If the list is empty, it means the request data is valid, otherwise, you should return the errors to your
user in a response format suitable for your app.
Let's say we expect the following JSON request body in our controller:
{
"firstName": "Seun",
"lastName": "Matt",
"email": "seunmatt@example.com",
"dob": "23-10-2022",
"gender": "MALE",
"interests": [
"Java",
"SpringBoot"
],
"preferences": {
"emailNotificationEnabled": true,
"frequency": 4
},
"kyc": {
"idType": "SSN",
"ssn": 123456789034,
"address": "Lagos, Nigeria"
},
"investmentAmount": 1000.50,
"investmentCurrency": "NGN"
}
- We will create a
Map<String, String>
with all the rules for the fields we want to validate: - We will then invoke
RequestValidator.validate()
to get possible list of errors - If the returned errors list is empty then, there's no exception. Otherwise, we will return 401 response with the errors
@RestController
public class UserController {
@PostMapping("/auth/signup")
public ResponseEntity<GenericResponse> signUp(@RequestBody Map<String, Object> request) {
Map<String, String> rules = new HashMap<>();
rules.put("firstName", "required|max:250");
rules.put("lastName", "required|max:250");
rules.put("email", "required|max:250|email");
rules.put("dob", "required||regex:[0-9]{2}-[0-9]{2}-[0-9]{4}");
rules.put("gender", "required|in:MALE,FEMALE");
rules.put("interests", "optional|array");
rules.put("preferences.emailNotificationEnabled", "optional|in:true,false");
rules.put("preferences.frequency", "optional|digit");
rules.put("kyc.idType", "required|in:BVN,SSN");
rules.put("kyc.bvn", "requiredIf:kyc.idType,BVN|length:11");
rules.put("kyc.ssn", "requiredIf:kyc.idType,SSN|length:12");
rules.put("kyc.address", "optional|max:250");
rules.put("investmentAmount", "optional|numeric");
rules.put("investmentCurrency", "requiredWith:investmentAmount|in:USD,NGN");
List<String> errors = RequestValidator.validate(request, rules);
if (!errors.isEmpty()) {
GenericResponse genericResponse = new GenericResponse();
genericResponse.setStatus(false);
genericResponse.setCode(HttpStatus.BAD_REQUEST.value());
genericResponse.setErrors(errors);
genericResponse.setMessage("Missing required parameter(s)");
return ResponseEntity.badRequest().body(genericResponse);
}
//otherwise all is well, process the request
//userService.signUp()
return ResponseEntity.ok(GenericResponse.generic200ResponseObj("Sing up successful"));
}
}
The request body can also be a plain JSON string or a Plain Old Java Object (POJO):
@RestController
public class LoginController {
@PostMapping("/auth/login")
public ResponseEntity<GenericResponse> login(@RequestBody LoginRequest request) {
Map<String, String> rules = new HashMap<>();
rules.put("email", "required|email");
rules.put("password", "required");
List<String> errors = RequestValidator.validate(request, rules);
if (!errors.isEmpty()) {
GenericResponse genericResponse = new GenericResponse();
genericResponse.setStatus(false);
genericResponse.setCode(HttpStatus.BAD_REQUEST.value());
genericResponse.setErrors(errors);
genericResponse.setMessage("Missing required parameter(s)");
return ResponseEntity.badRequest().body(genericResponse);
}
//otherwise all is well, process the request
//loginService.login()
return ResponseEntity.ok(GenericResponse.generic200ResponseObj("Login successful"));
}
}
The RequestValidator.validate()
method uses the JsonPath library to navigate the JSON request body
and retrieve the user provided value. The field's value is then pass through all the specified rules to check for violations if any.
Because the project uses JsonPath, all the json path operators also applies which makes this
library to be even more powerful.
You can specify more than one rule by combining the rules with a pipe |
separator. For example, in the snippet above, the email
field is required
and can't be more than 250 chars and should be a valid email
address. All those rules are combined using the |
as
in required|max:250|email
Please note that the regex
rule requires a double pipe char ||
as a separator and should be the last rule when combined with others.
This is to accommodate the potential presence of |
in the regex pattern.
If the library is used in a Spring Boot (or generally a Spring Framework) environment,
the library will attempt to use the Jackson's ObjectMapper
bean in the Spring's context if any.
If none is found, it will simply instantiate another one.
Below is a list of currently available rules for your use:
Rule | Description |
---|---|
required | This is a very basic rule that'll ensure that the user provides a non-null/non-empty value for a field in the HTTP request body |
optional | This rule is used in combination with other rules. It will only execute other rules if a field is present. If not, it will skip without raising any error e.g. rules.put("preferences.emailNotificationEnabled", "optional|in:true,false"); |
array | This will validate that the request data is a non-empty array of elements. |
digit | This will validate that the request data is an integer with no decimal places |
This will validate that the request data is a valida email | |
in:VALUE1,VALUE2,VALUE3...VALUEX | This rule will validate that the user provided value for its field is among the list of comma-separated values. For example, status => in:PENDING,ACTIVE,INACTIVE will return an error if the user provide other value for status in the request that is not PENDING , ACTIVE or INACTIVE |
length:limit | Where limit is the number of characters expected. This will ensure the request data is a String and has exactly limit number of characters |
max:limit | Where limit is the maximum number of characters expected. This will ensure the request data is a String and has at most limit number of characters |
min:limit | Where limit is the minimum number of characters expected. This will ensure the request data is a String and has at least limit number of characters |
numeric | This rule will ensure the supplied data is a number, with or withouth decimal places |
regex:pattern | Where pattern is the regex pattern to match the user supplied value against. This rule will ensure the supplied data matches the provided regex pattern |
requiredIf:key,value | Where key is a field expected in the request body and value is the user provided value. This rule will ensure that a value is provided for its own field, when other field key == value |
requiredWith:field1,field2 | This rule will validate that a non-null/non-empty value is provided for its field when ALL the comma-separated list of fields in its parameters have non-null/non-empty values. For example, accountName => requiredWith:accountNumber,bankCode will ensure the user also supply accountName field when the user provides an accountNumber and bankCode |
requiredWithAny:field1,field2 | This rule will validate that a non-null/non-empty value is provided for its field when AT LEAST ONE of the comma-separated list of fields in its parameters have non-null/non-empty values. For example, accountName => requiredWithAny:accountNumber,bankCode will ensure the user also supply accountName field when the user provides either an accountNumber OR a bankCode |
requiredWithout:field1,field2 | This rule will validate that a non-null/non-empty value is provided for its field when ALL of the comma-separated list of fields in its parameters is NOT available. For example, fullName => requiredWithout:firstName,lastName will validate that the user provides a fullName when both firstName and lastName is not provided |
requiredWithoutAny:field1,field2 | This rule will validate that a non-null/non-empty value is provided for its field when AT LEAST ONE of the comma-separated list of fields in its parameters is NOT available. For example, fullName => requiredWithoutAny:firstName,lastName will validate that the user provides a fullName when either of firstName or lastName is not provided |
- Create a class that'll implement the
RuleValidator
interface and return aValidationResult
:
public class PrefixRuleValidator implements RuleValidator {
private static final String CUSTOM_PREFIX = "custom_";
@Override
public ValidationResult isValid(Object value, Rule rule) {
//ensure value is prefixed with custom_
return value != null && String.class.isAssignableFrom(value.getClass()) &&
value.toString().startsWith(CUSTOM_PREFIX)
? ValidationResult.success()
: ValidationResult.failed(rule.getKey() + " should start with " + CUSTOM_PREFIX);
}
}
- Create a subclass of the
RequestValidator
class and append your rule to theruleValidatorMap
of the parent class:
public class CustomRequestValidator extends RequestValidator {
static {
ruleValidatorMap.put("customprefix", PrefixRuleValidator.class);
}
public static List<String> validate(Object target, Map<String, String> rules) {
String jsonRequest = convertObjectRequestToJsonString(target);
return validate(jsonRequest, rules, ruleValidatorMap);
}
}
- Use the custom sub-class in your controller instead of the default
RequestValidator
:
@RestController
public class CustomPrefixController {
@PostMapping("/custom")
public ResponseEntity<GenericResponse> formCustomPrefix(@RequestBody Map<String, Object> request) {
Map<String, String> rules = Collections.singletonMap("objectType", "customprefix");
List<String> errors = CustomRequestValidator.validate(request, rules);
if(!errors.isEmpty()) {
GenericResponse genericResponse = new GenericResponse();
genericResponse.setStatus(false);
genericResponse.setCode(HttpStatus.BAD_REQUEST.value());
genericResponse.setErrors(errors);
genericResponse.setMessage("Missing required parameter(s)");
return ResponseEntity.badRequest().body(genericResponse);
}
return ResponseEntity.ok(GenericResponse.generic200ResponseObj("Operation successful"));
}
}
This library only works with JSON request body. Other Content-Types like XML or plain text are not yet supported
Contributions are welcome. Please create a PR that targets the development branch. Also, do let the author know of what you're working on to avoid duplicate effort
Please use the Issues on Github to report any bugs you encountered while using the library or suggest improvements