← Kursa Dön
📄 Text · 18 min

Bean Validation API — Jakarta Validation

Bir bankanın ATM'sini düşünün. Para çekmek istiyorsunuz. ATM, işlemi gerçekleştirmeden önce bir dizi kontrol yapar: Kart geçerli mi? PIN doğru mu? Bakiye yeterli mi? Çekilecek miktar limitlerin içinde mi? Bu kontrollerin hiçbiri para çekme işleminin kendisi değildir — ama işlem yapılmadan önce zorunlu güvenlik adımlarıdır.

Yazılımda validation (doğrulama) tam olarak budur. Kullanıcıdan gelen veriyi işlemeden önce kontrol etmek: email formatı doğru mu, yaş negatif mi, isim boş mu? Bu kontroller olmadan veritabanına bozuk veri girer, güvenlik açıkları oluşur ve uygulamanız beklenmedik şekillerde çöker.

Bean Validation API, Java dünyasında bu doğrulama işlemini standart, deklaratif ve tekrar kullanılabilir bir şekilde yapmanızı sağlar. Annotation'ları sınıf alanlarına koyarsınız — gerisini framework halleder.


Bean Validation API Nedir?

Bean Validation, Java'da nesne doğrulamanın spesifikasyonudur (standart tanımı). Tarihçesi:

  • JSR 303 (2009): İlk Bean Validation standardı

  • JSR 349 (2013): Bean Validation 1.1

  • JSR 380 (2017): Bean Validation 2.0

  • Jakarta Bean Validation 3.0 (2020+): Jakarta EE altında devam

Bu bir spesifikasyondur — yani kuralları tanımlar ama kendi başına çalışmaz. Hibernate Validator, bu spesifikasyonun referans implementasyonudur (Hibernate ORM ile karıştırmayın — tamamen ayrı bir proje).

Spring Boot Entegrasyonu

<!-- pom.xml — tek dependency yeterli -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Bu starter şunları içerir:

  • Jakarta Bean Validation API (jakarta.validation-api)

  • Hibernate Validator (implementasyon)

  • Expression Language (hata mesajlarında expression desteği)

Neden Ayrı Bir Standart?

"Neden her yerde if-else ile kontrol etmiyoruz?" diye sorabilirsiniz:

// ❌ Imperative (zorunlu) validation — her yerde tekrar
public User createUser(String name, String email, int age) {
    if (name == null || name.isBlank()) {
        throw new IllegalArgumentException("İsim boş olamaz");
    }
    if (email == null || !email.contains("@")) {
        throw new IllegalArgumentException("Email geçersiz");
    }
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("Yaş 0-150 arasında olmalı");
    }
    // Aynı kontroller update, import, batch işlemlerinde de tekrar...
}

// ✅ Declarative (bildirimsel) validation — bir kez yaz, her yerde kullan
public class CreateUserRequest {
    @NotBlank(message = "İsim boş olamaz")
    private String name;

    @Email(message = "Email geçersiz")
    @NotBlank
    private String email;

    @Min(0) @Max(150)
    private int age;
}

Deklaratif yaklaşımın avantajları:

  • DRY: Kurallar bir kez tanımlanır, her yerde geçerlidir

  • Okunabilirlik: Kurallar alanın hemen yanında, kodu okumak kolay

  • Bakım: Kural değişikliği tek yerde yapılır

  • Framework entegrasyonu: Spring, otomatik olarak doğrulama yapıp hata mesajları döner


Null Kontrol Annotation'ları — @NotNull, @NotEmpty, @NotBlank

Bu üç annotation en sık karıştırılan ve en sık kullanılan annotation'lardır. Aralarındaki farkı kesin olarak bilmek kritiktir:

public class UserRequest {
    @NotNull(message = "ID boş olamaz")
    private Long id;

    @NotEmpty(message = "Roller boş olamaz")
    private List<String> roles;

    @NotBlank(message = "İsim boş olamaz")
    private String name;
}

Detaylı Karşılaştırma

Değer@NotNull@NotEmpty@NotBlank
null❌ INVALID❌ INVALID❌ INVALID
"" (boş string)✅ valid❌ INVALID❌ INVALID
" " (sadece boşluk)✅ valid✅ valid❌ INVALID
"John"✅ valid✅ valid✅ valid
[] (boş liste)✅ valid❌ INVALIDN/A

Hangi Durumda Hangisi?

public class OrderRequest {

    // Sayısal alanlar → @NotNull
    // (Sayılar boş string olamaz, sadece null olabilir)
    @NotNull(message = "Fiyat zorunludur")
    private BigDecimal price;

    @NotNull(message = "Miktar zorunludur")
    private Integer quantity;

    // String alanlar → @NotBlank (en güvenli)
    // (null, "", "   " hepsini reddeder)
    @NotBlank(message = "Ürün adı zorunludur")
    private String productName;

    @NotBlank(message = "Açıklama zorunludur")
    private String description;

    // Koleksiyonlar → @NotEmpty
    // (null ve boş listeyi reddeder)
    @NotEmpty(message = "En az bir ürün seçilmeli")
    private List<OrderItemDTO> items;

    // Boolean → @NotNull
    @NotNull(message = "Onay durumu belirtilmeli")
    private Boolean confirmed;

    // Enum → @NotNull
    @NotNull(message = "Sipariş tipi seçilmeli")
    private OrderType type;

    // Tarih → @NotNull
    @NotNull(message = "Tarih zorunludur")
    private LocalDate deliveryDate;
}

⚠️ Dikkat: @NotBlank sadece String için çalışır. Sayısal alana @NotBlank koyarsanız doğrulama beklenmedik davranabilir. Sayılar için @NotNull kullanın.


Boyut ve Uzunluk Kısıtlamaları — @Size

@Size, String, Collection, Map ve Array'ler için minimum/maksimum eleman sayısını kontrol eder:

public class ProfileRequest {
    @Size(min = 2, max = 50, message = "İsim {min}-{max} karakter olmalı")
    private String name;

    @Size(min = 10, max = 500, message = "Biyografi {min}-{max} karakter olmalı")
    private String bio;

    @Size(min = 1, max = 5, message = "En az {min}, en fazla {max} hobi seçilebilir")
    private List<String> hobbies;

    @Size(max = 10, message = "En fazla {max} tag eklenebilir")
    private Set<String> tags;
}

@Size vs @Length vs @Column(length)

AnnotationKaynakÇalışma ZamanıKullanım
@SizeBean ValidationRuntime (validation)DTO alanları
@LengthHibernate ValidatorRuntime (validation)Hibernate-spesifik
@Column(length=50)JPADDL generationEntity sınıfları

💡 İpucu: DTO'larda @Size kullanın. Entity'lerde @Column(length) kullanın. @Length Hibernate-spesifiktir ve taşınabilirlik açısından önerilmez.


Sayısal Kısıtlamalar

@Min, @Max — Tam Sayı Sınırları

public class ProductRequest {
    @Min(value = 0, message = "Fiyat negatif olamaz")
    @Max(value = 1_000_000, message = "Fiyat {value}'ı aşamaz")
    private BigDecimal price;

    @Min(value = 1, message = "Minimum 1 adet sipariş edilebilir")
    @Max(value = 1000, message = "Maksimum 1000 adet")
    private Integer quantity;
}

@DecimalMin, @DecimalMax — Ondalıklı Sınırlar

public class InterestRateRequest {
    @DecimalMin(value = "0.01", inclusive = true, message = "Faiz oranı en az %0.01 olmalı")
    @DecimalMax(value = "99.99", inclusive = true, message = "Faiz oranı en fazla %99.99 olabilir")
    private BigDecimal interestRate;

    @DecimalMin(value = "0.00", inclusive = false, message = "Tutar 0'dan büyük olmalı")
    // inclusive = false → 0.00 geçersiz, 0.01 geçerli
    private BigDecimal amount;
}

@Positive, @Negative ve Varyantları

public class AccountRequest {
    @Positive(message = "Tutar pozitif olmalı")
    // > 0 (0 dahil değil)
    private BigDecimal depositAmount;

    @PositiveOrZero(message = "Bakiye negatif olamaz")
    // >= 0
    private BigDecimal balance;

    @Negative(message = "İndirim negatif bir değer olmalı")
    // < 0
    private BigDecimal discount;

    @NegativeOrZero
    // <= 0
    private BigDecimal penalty;
}

@Digits — Basamak Kontrolü

public class InvoiceRequest {
    @Digits(integer = 8, fraction = 2, message = "En fazla 8 tam, 2 ondalık basamak")
    // Geçerli: 12345678.99
    // Geçersiz: 123456789.99 (9 tam basamak)
    // Geçersiz: 1234.999 (3 ondalık basamak)
    private BigDecimal totalAmount;
}

String Pattern Doğrulama — @Email, @Pattern

@Email

public class ContactRequest {
    @Email(message = "Geçerli bir email adresi giriniz")
    private String email;

    // Daha kısıtlayıcı email doğrulama
    @Email(regexp = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$",
           message = "Kurumsal email adresi giriniz")
    private String corporateEmail;
}

⚠️ Dikkat: @Email varsayılan olarak oldukça gevşektir — "a@b" bile geçerli sayılır. Production'da daha kısıtlayıcı regex veya custom validator kullanmanızı öneririm.

@Pattern — Regex ile Doğrulama

public class UserRegistrationRequest {
    @Pattern(regexp = "^[a-zA-Z0-9_]{3,20}$",
             message = "Kullanıcı adı 3-20 karakter, sadece harf, rakam ve alt çizgi")
    private String username;

    @Pattern(regexp = "^\\+?[1-9]\\d{9,14}$",
             message = "Geçerli telefon formatı: +905551234567")
    private String phone;

    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&]).{8,}$",
             message = "Şifre: en az 8 karakter, 1 büyük harf, 1 küçük harf, 1 rakam, 1 özel karakter")
    private String password;

    @Pattern(regexp = "^[1-9]\\d{10}$",
             message = "Geçerli TC Kimlik numarası giriniz")
    private String tcKimlik;

    @Pattern(regexp = "^(TR\\d{2}\\s?)(\\d{4}\\s?){5}\\d{2}$",
             message = "Geçerli IBAN formatı giriniz")
    private String iban;
}

Tarih Doğrulama — @Past, @Future

public class EventRequest {
    @Past(message = "Doğum tarihi geçmişte olmalı")
    private LocalDate birthDate;

    @PastOrPresent(message = "Kayıt tarihi gelecekte olamaz")
    private LocalDateTime registrationDate;

    @Future(message = "Etkinlik tarihi gelecekte olmalı")
    private LocalDate eventDate;

    @FutureOrPresent(message = "Planlanan tarih geçmişte olamaz")
    private LocalDateTime scheduledAt;
}
AnnotationnullGeçmişŞimdiGelecek
@Past
@PastOrPresent
@Future
@FutureOrPresent

💡 İpucu: Tüm tarih annotation'ları null değeri geçerli kabul eder. Tarih alanının zorunlu olmasını istiyorsanız @NotNull ile birlikte kullanın.


Validation Groups — Senaryoya Göre Kurallar

Aynı DTO'yu farklı işlemler için farklı kurallarla doğrulamak isteyebilirsiniz. Örneğin, oluşturma sırasında ID gerekmez ama güncelleme sırasında zorunludur:

Gerçek Dünya Analojisi

Bir otelin check-in ve check-out süreçlerini düşünün:

  • Check-in: İsim, TC, telefon → zorunlu. Oda tercihi → opsiyonel

  • Check-out: Oda numarası → zorunlu. Fatura adresi → zorunlu. TC → gerekmez

Aynı "Misafir Formu" farklı senaryolarda farklı kurallarla doğrulanır.

Implementasyon

// Adım 1: Marker interface'ler tanımla
public interface OnCreate {}
public interface OnUpdate {}
public interface OnPatch {}

// Adım 2: DTO'da groups belirt
public class UserRequest {

    @Null(groups = OnCreate.class, message = "Oluşturmada ID belirtilmemeli")
    @NotNull(groups = OnUpdate.class, message = "Güncelleme için ID zorunlu")
    private Long id;

    @NotBlank(groups = {OnCreate.class, OnUpdate.class})
    private String name;

    @Email
    @NotBlank(groups = OnCreate.class)  // Oluşturmada zorunlu
    // OnUpdate'te belirtilmemiş → güncelleme sırasında doğrulanmaz
    private String email;

    @Size(min = 8, groups = OnCreate.class)
    @NotBlank(groups = OnCreate.class)
    // Güncelleme sırasında şifre gerekmez
    private String password;

    @NotNull(groups = {OnCreate.class, OnUpdate.class})
    private Boolean active;
}

// Adım 3: Controller'da @Validated ile group belirt
@RestController
@RequestMapping("/api/users")
public class UserController {

    @PostMapping
    public ResponseEntity<UserResponse> create(
            @Validated(OnCreate.class) @RequestBody UserRequest request) {
        // Sadece OnCreate grubundaki kurallar çalışır
        // → id: null olmalı, name: zorunlu, email: zorunlu, password: zorunlu
    }

    @PutMapping("/{id}")
    public ResponseEntity<UserResponse> update(
            @PathVariable Long id,
            @Validated(OnUpdate.class) @RequestBody UserRequest request) {
        // Sadece OnUpdate grubundaki kurallar çalışır
        // → id: zorunlu, name: zorunlu, email: doğrulanmaz, password: doğrulanmaz
    }
}

⚠️ Dikkat: @Valid annotation'ı groups desteklemez. Groups kullanmak için `@Validated` kullanmanız gerekir. Bu, @Valid ve @Validated arasındaki en önemli farktır.

Default Group

Hiçbir group belirtilmemiş annotation'lar Default grubuna aittir:

public class ProductRequest {
    @NotBlank  // groups belirtilmemiş → Default grubunda
    private String name;

    @NotNull(groups = OnCreate.class)  // Sadece OnCreate'te
    private Long categoryId;
}

// @Validated(OnCreate.class) kullanıldığında:
// → name: DOĞRULANMAZ (Default grubunda, OnCreate'te değil!)
// → categoryId: doğrulanır

// Çözüm: Default grubunu da dahil et
@PostMapping
public ResponseEntity<?> create(
    @Validated({Default.class, OnCreate.class}) @RequestBody ProductRequest request) {
    // Hem Default hem OnCreate kuralları çalışır
}

Validation Messages Özelleştirme

1. Doğrudan Annotation'da

@NotBlank(message = "Kullanıcı adı boş bırakılamaz")
private String username;

2. ValidationMessages.properties ile

# src/main/resources/ValidationMessages.properties
user.name.required=Kullanıcı adı zorunludur
user.email.invalid=Geçerli bir email adresi giriniz
user.password.weak=Şifre en az {min} karakter olmalıdır

# Türkçe
jakarta.validation.constraints.NotBlank.message=Bu alan boş bırakılamaz
jakarta.validation.constraints.Size.message={min} ile {max} karakter arasında olmalı
jakarta.validation.constraints.Email.message=Geçerli bir email adresi giriniz
@NotBlank(message = "{user.name.required}")
private String name;

@Size(min = 8, max = 64, message = "{user.password.weak}")
private String password;
// → "Şifre en az 8 karakter olmalıdır"

3. Parametreli Mesajlar

Bean Validation, mesajlarda annotation parametrelerine otomatik erişim sağlar:

@Size(min = 3, max = 20, message = "Kullanıcı adı {min}-{max} karakter olmalı")
private String username;
// → "Kullanıcı adı 3-20 karakter olmalı"

@DecimalMin(value = "0.01", message = "Minimum değer: {value}")
private BigDecimal price;
// → "Minimum değer: 0.01"

@Digits(integer = 5, fraction = 2, message = "Format: max {integer} tam, {fraction} ondalık")
private BigDecimal amount;
// → "Format: max 5 tam, 2 ondalık"

Tüm Annotation'lar — Cheatsheet

AnnotationUygulanabilir TipAçıklama
@NotNullHerhangi bir tipnull olamaz
@NotEmptyString, Collection, Map, Arraynull ve boş olamaz
@NotBlankSadece Stringnull, boş, sadece boşluk olamaz
@Size(min, max)String, Collection, Map, ArrayBoyut kontrolü
@Min(value)Sayısal tiplerMinimum değer
@Max(value)Sayısal tiplerMaksimum değer
@DecimalMinSayısal tiplerOndalıklı minimum
@DecimalMaxSayısal tiplerOndalıklı maksimum
@PositiveSayısal tipler> 0
@PositiveOrZeroSayısal tipler>= 0
@NegativeSayısal tipler< 0
@NegativeOrZeroSayısal tipler<= 0
@Digits(integer, fraction)Sayısal tiplerBasamak kontrolü
@EmailStringEmail formatı
@Pattern(regexp)StringRegex pattern
@PastTarih tipleriGeçmiş tarih
@PastOrPresentTarih tipleriGeçmiş veya bugün
@FutureTarih tipleriGelecek tarih
@FutureOrPresentTarih tipleriBugün veya gelecek
@AssertTrueBooleantrue olmalı
@AssertFalseBooleanfalse olmalı

Gerçek Dünya Örneği: E-Ticaret Kayıt Formu

public class CustomerRegistrationRequest {

    @NotBlank(message = "Ad zorunludur")
    @Size(min = 2, max = 50, message = "Ad {min}-{max} karakter olmalı")
    private String firstName;

    @NotBlank(message = "Soyad zorunludur")
    @Size(min = 2, max = 50, message = "Soyad {min}-{max} karakter olmalı")
    private String lastName;

    @NotBlank(message = "Email zorunludur")
    @Email(message = "Geçerli bir email giriniz")
    private String email;

    @NotBlank(message = "Şifre zorunludur")
    @Size(min = 8, max = 64, message = "Şifre {min}-{max} karakter olmalı")
    @Pattern(
        regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$",
        message = "En az 1 büyük harf, 1 küçük harf ve 1 rakam gerekli"
    )
    private String password;

    @NotBlank(message = "Telefon zorunludur")
    @Pattern(regexp = "^\\+90[0-9]{10}$", message = "Format: +905XXXXXXXXX")
    private String phone;

    @NotNull(message = "Doğum tarihi zorunludur")
    @Past(message = "Doğum tarihi geçmişte olmalı")
    private LocalDate birthDate;

    @NotNull(message = "Sözleşme onayı zorunludur")
    @AssertTrue(message = "Kullanıcı sözleşmesini kabul etmelisiniz")
    private Boolean termsAccepted;

    @Size(max = 5, message = "En fazla 5 adres ekleyebilirsiniz")
    private List<@Valid AddressDTO> addresses;
}

public class AddressDTO {
    @NotBlank(message = "İl zorunludur")
    private String city;

    @NotBlank(message = "İlçe zorunludur")
    private String district;

    @NotBlank(message = "Adres satırı zorunludur")
    @Size(max = 200, message = "Adres en fazla 200 karakter olabilir")
    private String addressLine;

    @Pattern(regexp = "^\\d{5}$", message = "Posta kodu 5 haneli olmalı")
    private String zipCode;
}

Yaygın Hatalar ve Best Practice'ler

1. ❌ Entity'de Validation — DTO'da Kullanın

// ❌ YANLIŞ — Entity'de validation
@Entity
public class User {
    @NotBlank
    private String name;
    // Entity hem JPA hem validation sorumluluğu taşıyor
}

// ✅ DOĞRU — DTO'da validation
public class CreateUserRequest {
    @NotBlank
    private String name;
}

@Entity
public class User {
    @Column(nullable = false, length = 50)
    private String name;
    // Entity sadece persistence ile ilgilenir
}

2. ❌ @NotNull ile @NotBlank Karıştırmak

// ❌ YANLIŞ — String alanında @NotNull
@NotNull
private String name;  // "" (boş string) geçerli → veritabanına boş string girer

// ✅ DOĞRU — String alanında @NotBlank
@NotBlank
private String name;  // null, "", "   " hepsi reddedilir

3. ❌ Hata Mesajı Yazmamak

// ❌ YANLIŞ — varsayılan mesaj anlamsız
@NotBlank
private String name;
// Mesaj: "must not be blank" (İngilizce, son kullanıcıya gösterilemez)

// ✅ DOĞRU — anlamlı Türkçe mesaj
@NotBlank(message = "İsim alanı zorunludur")
private String name;

4. ❌ Primitive Tiplerde @NotNull

// ❌ YANLIŞ — int null olamaz (varsayılan 0)
@NotNull
private int age;  // Hiçbir zaman null olmaz, @NotNull anlamsız

// ✅ DOĞRU — wrapper tip kullanın
@NotNull(message = "Yaş zorunludur")
@Min(value = 0, message = "Yaş negatif olamaz")
private Integer age;  // null olabilir → @NotNull anlamlı

Özet

  • Bean Validation API, Java'da nesne doğrulamanın standardıdır — Hibernate Validator referans implementasyonu

  • `@NotNull` sadece null'ı reddeder, `@NotEmpty` null ve boşu reddeder, `@NotBlank` null, boş ve sadece boşluk olanı reddeder

  • String alanlarında `@NotBlank`, sayısal alanlarda `@NotNull`, koleksiyonlarda `@NotEmpty` kullanın

  • Validation Groups ile aynı DTO'yu farklı senaryolarda farklı kurallarla doğrulayabilirsiniz

  • Tüm annotation'lar null'ı geçerli kabul eder (convention) — zorunlu alanlar için @NotNull veya @NotBlank eklemeyi unutmayın

  • Validation annotation'larını DTO sınıflarında kullanın, entity'lerde değil

  • Her zaman anlamlı, Türkçe hata mesajları yazın — varsayılan İngilizce mesajlar kullanıcıya gösterilemez