Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11

Осторожно: @Size не проверяет на null! Как я пропустил баг

Забыли добавить валидацию на фронтенде и рассчитывали на @Size(min = 1) в Spring? Остерегайтесь! Эта аннотация пропускает null, и ваше "обязательное" поле может прийти пустым. Делюсь реальным кейсом из продакшна, объясняю, почему так происходит, и как правильно использовать @NotBlank, @NotEmpty и @NotNull, чтобы избежать ошибок валидации.

258 открытий2К показов
Осторожно: @Size не проверяет на null! Как я пропустил баг
Разрабатывая веб-приложения на Java Spring, мы часто сталкиваемся с необходимостью валидации входных данных. Одна из самых распространённых задач — убедиться, что обязательное текстовое поле заполнено и соответствует определённым критериям, например, длине. Кажется, что решение очевидно: используем аннотацию @Size. Но здесь нас поджидает одна неприятность.

Недавно я столкнулся с одной проблемой на реальном проекте. У меня есть сайт: бэкенд написан на Spring, а фронтенд — на Angular. Я добавлял новую форму для отправки отзыва пользователя и, в спешке, забыл добавить валидацию на одно из полей формы на фронтенде.

Я хотел, чтобы поле «комментарий» было обязательным и имело длину от 1 до 100 символов. На бэкенде использовал аннотацию @Size(min = 1, max = 100) к обязательным полям, например для поля `comment` в моем DTO имеет такой вид:

			public class FeedbackRequest {
    @Size(min = 1, max = 100, message = "Комментарий должен быть от 1 до 100 символов")
    private String comment;
    // ... другие поля
}

		

Тест с пустой строкой {"comment": ""} был корректно отклонён. Однако, когда я отправил запрос без поля "comment" вообще ( {} ), валидация прошла успешно, и поле «comment» пришло как null. Это была серьёзная ошибка — поле, которое по логике было обязательным, могло быть пропущено. Т.е. могли потеряться важные для меня данные.

Почему так происходит?

Причина кроется в спецификации JSR-303 (Bean Validation). Документация (Javadoc) для аннотации @Size прямо указывает: «null elements are considered valid». Это означает, что если значение поля null, валидатор @Size считает его валидным и не выполняет проверку длины.

Это поведение подтверждается и в исходном коде валидатора. Метод, отвечающий за проверку строки, сначала проверяет значение на null, и если оно null, сразу возвращает true, не переходя к проверке размера. Это стандартное поведение для многих аннотаций валидации, таких как @Min, @Max, @Pattern и т.д. Они проверяют качество значения, но не его наличие.

Предлагаемое решение

Чтобы поле было действительно обязательным (не null и не пустое), необходимо использовать аннотации, которые проверяют наличие значения - это @NotNull, @NotEmpty, @NotBlank . Для строк лучшим выбором является комбинация:

			import jakarta.validation.constraints.NotBlank; // или javax.validation.constraints.NotBlank
import jakarta.validation.constraints.Size;

public class FeedbackRequest {
    @NotBlank(message = "Комментарий обязателен для заполнения")
    @Size(min = 1, max = 100, message = "Комментарий должен быть от 1 до 100 символов")
    private String comment;
}

		

Понимание разницы между этими аннотациями критически важно:

  • @NotNull: Гарантирует, что значение поля не является null. Однако оно не проверяет пустые строки или строки из пробелов и будут считаться валидными.
  • @NotEmpty: Гарантирует, что значение не null и не пустое (для строки длина > 0). Это означает, что "" будет отклонено, но строка из одних пробелов " " пройдёт валидацию, так как её длина больше 0.
  • @NotBlank: Предназначена исключительно для String. Гарантирует, что строка не null, не пустая и после удаления пробелов с начала и конца (trim) имеет длину больше 0. Это означает, что null, "" и " " будут отклонены. Именно это поведение обычно ожидается от "обязательного текстового поля".

Красивое возвращение ошибок

Для того чтобы клиент (в моём случае, Angular-приложение) получал понятные сообщения об ошибках валидации, я настроил глобальный обработчик исключений с помощью @ControllerAdvice. Когда валидация не проходит, Spring выбрасывает MethodArgumentNotValidException. Далее, я его перехватываю и возвращаю структурированный ответ.

			import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.MethodArgumentNotValidException;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class ValidationExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return errors; // Вернёт, например: {"comment": "Комментарий обязателен для заполнения"}
    }
}

		

Angular-приложение может легко обработать этот JSON и отобразить пользователю понятное сообщение об ошибке.

Выводы

Аннотация @Size сама по себе не делает поле обязательным. Она проверяет только длину, если значение не null. Для обязательных строковых полей всегда используйте @NotBlank. Для других типов данных используйте @NotNull.

Не полагайтесь на @Size(min = 1) как на замену проверки на null или пустую строку. Эта ловушка, в которую я сам попался, может привести к консистентным данным в вашей системе и потенциальным уязвимостям, если бэкенд будет работать с неожиданными null значениями. Всегда явно указывайте, что поле является обязательным, с помощью соответствующих аннотаций.

Этот материал — не открытие века, а скорее напоминание как себе, так и коллегам. Даже опытные разработчики иногда могут упустить из виду такие нюансы. Я столкнулся с этой проблемой на проекте, и она стоила мне времени на отладку и тестирование.

Следите за новыми постами
Следите за новыми постами по любимым темам
258 открытий2К показов