Skip to content

Spring Boot · Web MVC

Validation and JSON Serialization in Spring Boot, Explained

5 min read Updated 2026-06-26 Share:

Practice Validation & Serialization interview questions

The two ends of every request

Every API call has the same two boundaries: untrusted JSON comes in and must be validated, and your objects go out and must be serialized. Spring Boot wires Bean Validation and Jackson to handle both, but the defaults only get you so far. Knowing the annotations — and the traps — is what separates a leaky, inconsistent API from a tight one.

@Valid vs @Validated

@Valid is the standard Bean Validation annotation; @Validated is Spring's superset that adds validation groups and method-parameter validation.

@PostMapping("/orders")
Order create(@Valid @RequestBody CreateOrderRequest body) { ... }   // validate the body

@Validated                                  // class-level: enable method validation
@RestController
class SearchController {
    @GetMapping("/search")
    List<Item> search(@RequestParam @NotBlank String q,
                      @RequestParam @Min(1) @Max(100) int page) { ... }
}

Use @Valid to cascade into a request body or nested object — the common case. Reach for @Validated when you need groups, or to validate loose @RequestParam/@PathVariable constraints (which @Valid alone won't do).

The constraint toolbox

JSR-380 ships annotations for nullability, size, range, and format. Add spring-boot-starter-validation and stack them on DTO fields:

record CreateUserRequest(
    @NotBlank String name,
    @Email String email,
    @Min(18) @Max(120) int age,
    @Size(min = 8, max = 64) String password,
    @NotNull @Past LocalDate birthDate) {}

@NotNull, @NotEmpty, @NotBlank, @Size, @Min/@Max, @Email, @Pattern, @Past/@Future cover most needs. When none fit a domain rule, write a custom constraint: an annotation plus a ConstraintValidator, which — being a Spring bean — can inject a repository for DB-backed checks.

Validation groups and nested objects

Two things trip people up. First, cascading is opt-in: add @Valid at every level you want validated, including inside collection generics:

record OrderRequest(
    @Valid @NotNull Address shippingAddress,            // cascade into Address
    @Valid @Size(min = 1) List<@Valid LineItem> items) {}  // validate each element

Second, when the same DTO needs different rules per operation, use groups with @Validated(Group.class) rather than splitting into separate DTOs:

record UserRequest(
    @Null(groups = OnCreate.class) @NotNull(groups = OnUpdate.class) Long id,
    @NotBlank(groups = {OnCreate.class, OnUpdate.class}) String name) {}

@PostMapping User create(@Validated(OnCreate.class) @RequestBody UserRequest r) { ... }
@PutMapping  User update(@Validated(OnUpdate.class) @RequestBody UserRequest r) { ... }

What you get when validation fails

A failing @Valid @RequestBody throws MethodArgumentNotValidException; a failing @Validated parameter throws ConstraintViolationException. Handle both in your @RestControllerAdvice and return all field errors at once:

@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<ApiError> onBody(MethodArgumentNotValidException ex) {
    var fields = ex.getBindingResult().getFieldErrors().stream()
        .map(f -> f.getField() + ": " + f.getDefaultMessage()).toList();
    return ResponseEntity.badRequest().body(new ApiError("VALIDATION", fields));
}

Jackson does the JSON

On the way out, Boot's auto-configured ObjectMapper (via Jackson) serializes your return value. Shape individual fields with annotations:

@JsonInclude(JsonInclude.Include.NON_NULL)        // drop null fields
class UserDto {
    @JsonProperty("user_name") private String userName;     // rename
    @JsonIgnore private String passwordHash;                // hide entirely
    @JsonProperty(access = Access.WRITE_ONLY) private String password;  // input-only
}

@JsonProperty renames, @JsonIgnore excludes, @JsonInclude(NON_NULL) omits nulls, and WRITE_ONLY/READ_ONLY make a field one-directional — exactly what you want for passwords or server-set ids.

Dates and naming

Boot registers the JavaTimeModule, so java.time types serialize as ISO-8601 strings. Override per field with @JsonFormat, or globally with properties:

@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "UTC")
private LocalDateTime startsAt;
spring.jackson.property-naming-strategy=SNAKE_CASE   # userName ↔ user_name, both directions
spring.jackson.time-zone=UTC

Prefer java.time over the legacy java.util.Date, and set a global naming strategy when your whole API uses snake_case.

DTOs, not entities

The single most important serialization rule: never serialize JPA entities at the boundary.

record UserDto(Long id, String name, String email) {
    static UserDto from(User u) { return new UserDto(u.getId(), u.getName(), u.getEmail()); }
}

Returning entities leaks your schema, serializes sensitive fields, triggers LazyInitializationException on lazy associations outside a transaction, and welds your API to the database. A DTO lets you control the shape, hide fields, validate input separately, and version the API independently. Records make DTOs nearly free — Jackson binds them through the constructor with no annotations.

Customize Jackson without breaking it

When you need global tweaks, use spring.jackson.* or a Jackson2ObjectMapperBuilderCustomizer — not your own ObjectMapper bean:

@Bean
Jackson2ObjectMapperBuilderCustomizer customizer() {
    return builder -> builder
        .failOnUnknownProperties(false)
        .serializationInclusion(JsonInclude.Include.NON_NULL);
}

Defining your own ObjectMapper replaces Boot's and silently drops the modules it registered (Java time, parameter names) unless you re-add them — a classic bug. The customizer layers your changes on top of the working default.

Recap

Validate input with @Valid (and @Validated for groups and method params), reach for the built-in constraints before writing a custom ConstraintValidator, and remember cascading is opt-in. On the way out, let Jackson serialize DTOs — never entities — shaping fields with @JsonProperty, @JsonIgnore, and @JsonInclude, formatting dates with @JsonFormat, and customizing the mapper through the builder customizer rather than replacing it. Tight validation in, intentional JSON out: that's a well-behaved API boundary.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel