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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Validate JSON Requests in Spring Boot 3 Using Filters and JSON Schema

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:

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaJSON SchemaSpring Bootfilterjson-validation
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.