Validate JSON Requests in Spring Boot 3 Using Filters and JSON Schema
This tutorial demonstrates how to validate incoming JSON payloads in Spring Boot 3 by generating JSON Schemas, creating a custom filter that checks request bodies before they reach business logic, and registering the filter, complete with code examples and test screenshots.
Environment: Spring Boot 3.4.2
1. Introduction
Validating API request data before core business logic helps detect errors early and ensures predictable application behavior. JSON Schema provides a formal way to describe the required structure, types, and constraints of JSON data, allowing validation before converting it to Java objects.
2. Practical Example
2.1 Add Dependencies
<!-- Dependency for generating JSON schema files -->
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
<version>4.38.0</version>
</dependency>
<!-- Dependency for validating JSON data -->
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>1.5.6</version>
</dependency>Explanation: jsonschema-generator can automatically generate JSON Schema files from Java classes, while json-schema-validator validates actual JSON data against those schemas.
2.2 Define Controller Interface
public class User {
private String name;
private String password;
private Integer age;
private String email;
// getters, setters, constructors
}
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping("")
public ResponseEntity<?> create(@RequestBody User user) {
return ResponseEntity.ok(user);
}
}2.3 Generate JSON Schema
The following method creates a JSON Schema for the User class, marks name and email as required, ignores password, and disables additional properties.
public static String genUserSchema() {
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON);
configBuilder.forFields()
.withRequiredCheck(field -> "name".equals(field.getName()))
.withRequiredCheck(field -> "email".equals(field.getName()))
.withIgnoreCheck(field -> "password".equals(field.getName()));
SchemaGeneratorConfig config = configBuilder.build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonSchema = generator.generateSchema(User.class);
ObjectNode schemaObject = (ObjectNode) jsonSchema;
schemaObject.put("additionalProperties", false);
return jsonSchema.toPrettyString();
}Generated JSON Schema:
The schema file is saved to classpath:schemas/user-schema.json:
2.4 Define Filter
public class ValidationJsonFilter extends OncePerRequestFilter {
private final JsonSchema schema;
private final ObjectMapper mapper;
public ValidationJsonFilter(ObjectMapper mapper) {
InputStream is;
try {
is = new ClassPathResource("schemas/user-schema.json").getInputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
this.schema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012).getSchema(is);
this.mapper = mapper;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getServletPath().equals("/users") && "POST".equalsIgnoreCase(request.getMethod())) {
String body = readBody(request);
JsonNode node;
try {
node = mapper.readTree(body);
} catch (JsonProcessingException ex) {
response.setContentType("application/json;charset=utf-8");
String content = mapper.writeValueAsString(Map.of("code", -1, "message", "JSON data error"));
response.getWriter().println(content);
return;
}
Set<ValidationMessage> errors = schema.validate(node);
if (!errors.isEmpty()) {
response.setContentType("application/json;charset=utf-8");
List<String> list = errors.stream().map(ValidationMessage::getMessage).toList();
mapper.writeValue(response.getWriter(), Map.of("code", -1, "message", list));
return;
}
filterChain.doFilter(new CachedBodyRequestWrapper(request, body), response);
return;
}
filterChain.doFilter(request, response);
}
private String readBody(HttpServletRequest request) throws IOException {
return StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
}
}2.5 CachedBodyRequestWrapper
public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {
private final String cachedBody;
public CachedBodyRequestWrapper(HttpServletRequest request, String cachedBody) {
super(request);
this.cachedBody = cachedBody;
}
@Override
public ServletInputStream getInputStream() throws IOException {
byte[] bytes = cachedBody.getBytes();
return new CachedBodyServletInputStream(new ByteArrayInputStream(bytes));
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
private static class CachedBodyServletInputStream extends ServletInputStream {
private final InputStream inputStream;
public CachedBodyServletInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public boolean isFinished() {
try {
return inputStream.available() == 0;
} catch (IOException e) {
return false;
}
}
@Override
public boolean isReady() { return true; }
@Override
public void setReadListener(ReadListener readListener) { throw new UnsupportedOperationException(); }
@Override
public int read() throws IOException { return inputStream.read(); }
}
}2.6 Register Filter
@Bean
FilterRegistrationBean<ValidationJsonFilter> validationJsonFilter(ObjectMapper mapper) {
FilterRegistrationBean<ValidationJsonFilter> reg = new FilterRegistrationBean<>();
reg.setFilter(new ValidationJsonFilter(mapper));
reg.setUrlPatterns(List.of("/*"));
return reg;
}2.7 Test
When no required fields are provided, the filter returns an error indicating missing name and email:
After supplying the required fields, the request succeeds:
Adding extra fields such as password or unknown properties triggers validation errors:
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
