Java: Validasi dengan Gaya Jakarta

Tue. Jun 23rd, 2026 04:50 PM6 mins read
Java: Validasi dengan Gaya Jakarta
Source: Gemini Nano Banana Pro - validation

Jika biasanya validasi input dilakukan menggunakan beberapa boilerplate if, maka di Java ada opsi agar validasi input dilakukan lewat deklarasi anotasi saja. Kita bisa menggunakan Jakarta Validation API. Ini memudahkan development karena cukup dengan anotasi doang semuanya bisa teratasi tanpa perlu repot-repot bikin logic validasi. Dengan anotasi jadi lebih simple dibaca rulesnya.

Jakarta Validation API

Sebelumnya ini bernama Java Validation API. Sejak Oracle menyerahkan Java Enterprise ke komunitas open source, semua trade mark “Java” Enterprise berganti jadi “Jakarta”, termasuk Jakarta Validation API karena terkait lisensi. Nama Jakarta dipilih karena Jakarta adalah ibukota dari asal-usul nama “Java” dan beberapa projek open source sebelumnya di Apache juga menggunakan Jakarta, jadi biar konsisten aja.

Setup

Jakarta Validation API adalah interfacenya, implementasinya yang paling populer adalah Hibernate Validator. Kita tinggal tambahkan satu dependensi di sini:

pom.xml
Copy
<dependency>
	<groupId>org.hibernate.validator</groupId>
	<artifactId>hibernate-validator</artifactId>
	<version>8.0.3.Final</version>
</dependency>

Contoh Kasus

Misalkan kita ingin validasi input registrasi user dengan rules seperti ini:

  1. Username ga boleh kosong;
  2. Tanggal lahir ga boleh kosong dan harus di waktu lampau;
  3. Email harus sesuai format email dan ga boleh kosong;
  4. Saldo ga boleh negatif;
  5. Password ga boleh kosong, minimal 8 karakter, maksimal 64 karakter, harus ada huruf, dan harus ada angka;
  6. Username dan password ga boleh sama;
  7. Daftar alamat ga boleh kosong;
  8. Nama jalan di alamat ga boleh kosong;
  9. Nama kota di alamat ga boleh kosong;

Codenya kurang lebih seperti berikut:

Record UserAddress
Copy
public record UserAddress(@NotBlank String street,
                          @NotBlank String city){
}
Record UserRequest
Copy
@Builder
public record UserRequest(@NotBlank String username,
                          @NotNull @Past LocalDate birthdate,
                          @NotBlank @Email String email,
                          @NotNull @PositiveOrZero(message = "must not be negative") BigDecimal balance,
                          @NotBlank @Size(min = 8, max = 64) @Pattern(regexp = ".*[a-zA-Z].*") @Pattern(regexp = ".*\\d.*") String password,
                          @Valid @NotEmpty List<UserAddress> addresses){

    @AssertTrue(message = "username must not equals password")
    boolean isUsernameEqualsPassword() {
        return !username.equals(password);
    }
}

Kita tinggal deklarasikan anotasinya di tiap-tiap properti. @NotBlank untuk data yang ga boleh kosong, @NotNull untuk data yang ga boleh null, @Past untuk data tanggal yang harus di masa lampau, @Email untuk data email dengan format email yang benar, @PositiveOrZero untuk data angka dengan nilai harus positif atau 0, @Size untuk data dengan ukuran tertentu, @Pattern untuk validasi menggunakan regexp, @NotEmpty untuk data Collection yang ga boleh kosong, dan berbagai anotasi lainnya yang bisa dieksplorasi. By default tiap anotasi udah ada default messagenya, tapi kita juga bisa custom menggunakan parameter message seperti pada validasi saldo di atas kita ingin pesannya must not be negative bukan must be positive or zero. Untuk validasi value objek di dalamnya seperti daftar alamat di atas, kita harus menambahkan anotasi @Valid di propertinya seperti pada objek daftar alamat di atas agar ikut divalidasi. Misalkan kita ingin bikin validasi input yang ga ada di anotasi, seperti username ga boleh sama dengan password, maka kita bisa bikin method boolean dengan anotasi @AssertTrue beserta isi message errornya dan logic validasi yang harus bernilai true terkait input tersebut. Untuk penggunaannya, ada beberapa pendekatan untuk melakukannya.

AOP

Jika menggunakan framework yang support AOP atau bikin AOP sendiri ini bisa jadi pilihan. Seperti pada Spring Boot ini udah otomatis dihandle asalkan kita menggunakan anotasi @Valid di parameter method di Controller yang mau dieksekusi.

Class Controller
Copy
@RestController
public class UserController{
	@PostMapping("/create")
	public Object create(@Valid @RequestBody UserRequest userRequest, Errors errors){
		if(errors.hasErrors()){
			return errors.getFieldErrors().stream()
					.collect(Collectors.groupingBy(FieldError::getField,
							Collectors.mapping(FieldError::getDefaultMessage, Collectors.toList())));
		}
		//logic execution
		return ResponseEntity.status(HttpStatus.CREATED).body(new Response());
	}
}

Keunggulan AOP

Ini lebih simple diimplementasi. Hanya bermodal anotasi @Valid doang. Sisanya udah dihandle oleh framework.

Kelemahan AOP

Validasi jadi bergantung dengan framework. Tanpa AOP anotasi itu jadi ga ada gunanya. Kalau kita ganti framework jadi repot. Logic aplikasi jadi bergantung ke layer terluar sehingga jadi ga independen lagi. Apalagi kalau modul di layer tersebut juga digunakan oleh modul lain yang belum tentu menggunakan framework yang sama.

Utilities

Pendekatan kedua adalah dengan menggunakan class utilities. Kita harus bikin logic implementasi dari validasi itu sendiri, jadi kita ga bergantung pada framework lagi. Nanti tiap objek input yang mau divalidasi harus divalidasi lewat utilities tersebut. Di sini gw menggunakan anotasi @UtilityClass dari Lombok untuk generate boilerplate terkait class utilities.

Class Validators
Copy
@UtilityClass
public class Validators{
	public static <O> O validateValue(O object){
		if(Objects.isNull(object)){
			throw new ConstraintViolationException("The object to be validated must not be null", null);
		}
		Set<ConstraintViolation<O>> violations = ValidationHolder.validate(object);
		if(!violations.isEmpty()){
			Map<Path, List<String>> violationsByPath = violations.stream()
					.collect(Collectors.groupingBy(ConstraintViolation::getPropertyPath,
							Collectors.mapping(ConstraintViolation::getMessage, Collectors.toList())));
			throw new ConstraintViolationException(violationsByPath.toString(), violations);
		}
		return object;
	}

	@NoArgsConstructor(access = AccessLevel.PACKAGE)
	static class ValidationHolder{
		private static final Validator validator;

		static{
			try(ValidatorFactory validatorFactory = Validation.byDefaultProvider()
					.configure()
					.messageInterpolator(new ParameterMessageInterpolator())
					.buildValidatorFactory()){
				validator = validatorFactory.getValidator();
			}
		}

		static <T> Set<ConstraintViolation<T>> validate(T obj){
			return validator.validate(obj);
		}

	}
}
Contoh Penggunaan
Copy
public static void main(String[] args){
	//successâś…
	Validators.validateValue(UserRequest.builder()
					.email("hello@world.com")
					.birthdate(LocalDate.of(2000, 1, 1))
					.username("fafa")
					.password("fu22fa22")
					.addresses(List.of(new UserAddress("jl. H. Agus Salim", "Padang")))
					.balance(BigDecimal.TEN)
			.build());

	//failed❌
	Validators.validateValue(UserRequest.builder()
			.email("xx")
			.birthdate(LocalDate.now())
			.username("fufu")
			.password("fufu")
			.addresses(List.of(new UserAddress("", "   ")))
			.balance(BigDecimal.valueOf(-1))
			.build());
}

Keunggulan Utilities

Kita tinggal masukin objek yang mau divalidasi ke method utilities langsung beres. Sudah jelas dengan begini kita ga bergantung pada framework AOP. Dengan atau tanpa framework validasi tetap jalan.

Kekurangan Utilities

Tentu saja ga sesederhana menggunakan AOP. Kita perlu develop sendiri logic implementasinya. Kita juga perlu pastikan objek yang mau divalidasi harus melewati utilities ini. Solusi ini juga berlawanan dengan prinsip “Tell, Don't Ask” karena antara data dan logic aplikasi terpisah.

Validator Interface

Alternatif lainnya adalah menggunakan interface sebagai utilities, jadi tiap objek input yang mau divalidasi harus mengimplementasi interface ini.

Interface ConstraintViolation
Copy
public interface ConstraintValidation{
	default void validate(){
		Validators.validateValue(this);
	}
}
Record UserRequest
Copy
@Builder
public record UserRequest(@NotBlank String username,
                          @NotNull @Past LocalDate birthdate,
                          @NotBlank @Email String email,
                          @NotNull @PositiveOrZero(message = "must not be negative") BigDecimal balance,
                          @NotBlank @Size(min = 8, max = 64) @Pattern(regexp = ".*[a-zA-Z].*")
                          @Pattern(regexp = ".*\\d.*") String password,
                          @Valid @NotEmpty List<UserAddress> addresses) implements ConstraintValidation{

    @AssertTrue(message = "username must not equals password")
    public boolean isUsernameEqualsPassword() {
        return !username.equals(password);
    }
}
Contoh Penggunaan
Copy
public static void main(String[] args){
	//successâś…
	UserRequest goodRequest = UserRequest.builder()
			.email("hello@world.com")
			.birthdate(LocalDate.of(2000, 1, 1))
			.username("fafa")
			.password("fu22fa22")
			.addresses(List.of(new UserAddress("jl. H. Agus Salim", "Padang")))
			.balance(BigDecimal.TEN)
			.build();
	goodRequest.validate();

	//failed❌
	UserRequest badRequest = UserRequest.builder()
			.email("xx")
			.birthdate(LocalDate.now())
			.username("fufu")
			.password("fufu")
			.addresses(List.of(new UserAddress("", "   ")))
			.balance(BigDecimal.valueOf(-1))
			.build();
	badRequest.validate();
}

public void execute(UserRequest nullableRequest){
	//null-checking⌛
	 if(nullableRequest == null){
			throw new InvalidArgumentException("request is null");
	 }
	 nullableRequest.validate();
}

Kita bikin interface dengan default implementation untuk mengeksekusi utilities validator pada method validate().

Keunggulan Interface Validation

Sama seperti pendekatan utilities, ini ga bergantung pada framework AOP sehingga bisa jalan dengan atau tanpa framework AOP. Ini lebih OOP-style karena kita mengeksekusinya lewat method validate() yang ada di masing-masing objek input. Ini sejalan dengan prinsip “Tell, Don't Ask” karena saat menggunakannya kita tinggal perintah objek untuk validasi sendiri.

Kekurangan Interface Validation

Kita juga tetap harus bikin logic implementasinya sendiri, ga kayak solusi dari framework AOP yang udah otomatis. Kalau misalkan objek input ini dibuat dari luar seperti dikirim lewat parameter argument, maka akan ada kemungkinan null value. Jadi kita harus validasi null-checking dulu. Sedangkan kalau pada pendekatan static method kita bisa null-check di utilitiesnya aja.

Verdict

Itulah 3 pendekatan input validation menggunakan Jakarta Validation API. Dengan AOP lebih simple, tapi jadi bergantung pada framework. Dengan utilities lebih fleksibel dengan atau tanpa framework dan ga perlu null-checking saat menggunakannya, tapi perlu develop implementasi sendiri dan logic validasinya jadi terpisah dari objek inputnya. Dengan Interface Validation juga lebih fleksibel dengan atau tanpa framework dan saat menggunakannya kita tinggal perintah lewat objek inputnya, tapi kita juga perlu develop logic implementasinya dan harus null-checking dulu setiap penggunaan. Oh ya, ini scopenya hanya pada validasi input pada logic aplikasi seperti validasi request di layer use case di mana validasi itu hanya berlaku pada use case tertentu doang. Kalau untuk logic bisnis seperti pada layer domain entities di mana validasi bisnis aturannya akan selalu sama, ini tetap perlu validasi sendiri. Misalnya format email di dalam use case apa pun formatnya harus selalu dalam bentuk email yang valid. Untuk validasi seperti ini tetap perlu ada validasi di domain logic bisnisnya seperti dengan bikin Domain Objek Email sendiri.

© 2026 · Ferry Suhandri