Skip to main content

Data Validation

Introduction

Data validation is a critical aspect of the Student Management System that ensures data integrity, security, and business rule compliance. This document covers the comprehensive validation strategy implemented using Jakarta Bean Validation (formerly JSR-303) and custom validation logic.

Validation Architecture

Multi-Layer Validation Strategy

┌─────────────────────────────────────┐
│ Client-Side Validation │
│ (Frontend - Optional) │
├─────────────────────────────────────┤
│ Controller Validation │
│ (Request DTO Validation) │
├─────────────────────────────────────┤
│ Service Validation │
│ (Business Logic Validation) │
├─────────────────────────────────────┤
│ Database Validation │
│ (Constraints & Triggers) │
└─────────────────────────────────────┘

Validation Flow Diagram

[DIAGRAM_PLACEHOLDER_VALIDATION_FLOW]

Jakarta Bean Validation

Core Validation Annotations

AnnotationPurposeUsage
@NotNullNull checkRequired fields
@NotBlankNull, empty, whitespace checkString fields
@NotEmptyNull and empty checkCollections, arrays
@SizeLength constraintsStrings, collections
@Min/@MaxNumeric range validationNumbers
@EmailEmail format validationEmail fields
@PatternRegex pattern matchingCustom formats
@ValidNested object validationComplex objects

Request DTO Validation

Example from ProgramRequest:

@Getter
@Setter
@Builder
public class ProgramRequest {
@NotBlank(message = "Program name is required")
private String programName;
}

Extended Example for Student Registration:

public class StudentRequest {
@NotNull(message = "Student id is required")
private String studentId;

@NotBlank(message = "Full name is required")
private String fullName;

@NotNull(message = "Date of birth is required")
private LocalDate dob;

@NotBlank(message = "Gender is required")
private String gender;

@NotBlank(message = "Intake is required")
private String intake;

@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
@EmailDomain
private String email;

@NotBlank(message = "Phone country is required")
private String phoneCountry;

@NotBlank(message = "Phone number is required")
private String phone;

@NotBlank(message = "Nationality is required")
private String nationality;

@NotNull(message = "Faculty id is required")
private Integer facultyId;

@NotNull(message = "Program id is required")
private Integer programId;

@NotNull(message = "Student status id is required")
private Integer studentStatusId;

private List<AddressRequest> addresses;

private List<DocumentRequest> documents;
}

Controller-Level Validation

Implementation in ProgramController:

@PostMapping("")
public APIResponse addProgram(@RequestBody @Valid ProgramRequest request) {
log.info("Received request to add program: {}", request.getProgramName());

ProgramResponse program = programService.addProgram(request);

return APIResponse.builder()
.status(HttpStatus.CREATED.value())
.message("Success")
.data(program)
.build();
}

Key Points:

  • @Valid annotation triggers validation
  • Validation occurs before method execution
  • Automatic error response generation for validation failures

Custom Validation

Custom Annotation Creation

EmailDomain Annotation:

@Constraint(validatedBy = EmailDomainValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailDomain {
String message() default "Invalid email domain";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

Custom Validator Implementation

EmailDomainValidator:

@Component
public class EmailDomainValidator implements ConstraintValidator<EmailDomain, String> {

private final IEmailDomainRepository emailDomainRepository;

@Autowired
public EmailDomainValidator(IEmailDomainRepository emailDomainRepository) {
this.emailDomainRepository = emailDomainRepository;
}

@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
if (email == null) {
return true; // Let @NotNull handle null validation
}

String domain = email.substring(email.indexOf('@') + 1);

List<org.example.backend.domain.EmailDomain> allowedDomains =
emailDomainRepository.findAll();

return allowedDomains.stream()
.anyMatch(allowedDomain -> domain.equals(allowedDomain.getDomain()));
}
}

Usage in Request DTOs

[CODE_PLACEHOLDER_CUSTOM_VALIDATION_USAGE]

Service Layer Validation

Business Logic Validation

Example from ProgramServiceImpl:

@Override
public ProgramResponse addProgram(ProgramRequest request) {
// Business rule validation
if (programRepository.findByProgramName(request.getProgramName()).isPresent()) {
log.error("Program already exists");
throw new RuntimeException("Program already exists");
}

Program program = ProgramMapper.mapToDomain(request);
program = programRepository.save(program);

log.info("Program saved to database successfully");
return ProgramMapper.mapToResponse(program);
}

Complex Business Validation

@Override
@Transactional
public ClassRegistrationResponse addClassRegistration(ClassRegistrationRequest classRegistrationRequest) {

log.info("add class registration with request: {}", classRegistrationRequest);

ClassRegistration classRegistration = ClassRegistrationMapper.mapFromClassRegistrationRequestToDomain(classRegistrationRequest);

log.info("check if class exists");
Class aClass = classRepository.findById(classRegistrationRequest.getClassId())
.orElseThrow(() -> {
log.error("Class not found");
return new RuntimeException("Class not found");
});

log.info("check if the course of the class is active");
if (!aClass.getCourse().getIsActive()) {
log.error("Course of the class is not active");
throw new RuntimeException("Course of the class is not active");
}

log.info("check if max students reached");
if (aClass.getClassRegistrations().size() >= aClass.getMaxStudents()) {
log.error("Max students reached");
throw new RuntimeException("Max students reached");
}

log.info("check if student exists");
Student student = studentRepository.findById(classRegistrationRequest.getStudentId())
.orElseThrow(() -> {
log.error("Student not found");
return new RuntimeException("Student not found");
});

log.info("set class and student");
classRegistration.setAClass(aClass);
classRegistration.setStudent(student);
classRegistration = classRegistrationRepository.save(classRegistration);

log.info("class registration created successfully");

// Add the registration history
log.info("Add class registration history");
ClassRegistrationHistoryRequest classRegistrationHistoryRequest = ClassRegistrationHistoryMapper.mapFromClassRegistrationDomainToClassRegistrationHistoryRequest(classRegistration);
classRegistrationHistoryRequest.setReason("Class registration created");

classRegistrationHistoryService.addClassRegistrationHistory(classRegistrationHistoryRequest);
log.info("class registration history added successfully");

return ClassRegistrationMapper.mapFromDomainToClassRegistrationResponse(classRegistration);
}

Error Handling and Response

Validation Error Structure

{
"status": 400,
"message": "Validation failed",
"errors": [
{
"field": "programName",
"message": "Program name is required",
"rejectedValue": null
}
]
}

Database-Level Validation

Entity Constraints

Program Entity with Constraints:

@Entity
@Table(name = "programs")
public class Program extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@Column(name = "program_name", nullable = false, unique = true, length = 100)
private String programName;

// Additional constraints
}

Database Migration Constraints

ALTER TABLE addresses
ADD CONSTRAINT addresses_address_type_check
CHECK (address_type IN ('Thường Trú', 'Tạm Trú', 'Nhận Thư'));

Validation Patterns by Entity

Student Validation

public class Student extends Auditable {
@Id
@Column(name = "student_id", length = 10)
private String studentId;

@Column(name = "full_name", nullable = false)
private String fullName;

@Column(name = "dob", nullable = false)
private LocalDate dob;

@Column(name = "gender", nullable = false)
private String gender;

// Additional fields with validation
}

Registration Validation

public class ClassRegistration extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@Column(name = "status", nullable = false)
@Enumerated(EnumType.STRING)
private RegistrationStatus status;
}

Testing Validation Logic

Unit Testing Validators

@ExtendWith(MockitoExtension.class)
public class EmailDomainValidatorTest {

@Test
public void shouldReturnFalseWhenDomainIsNotAllowed() {
List<EmailDomain> allowedDomains = Arrays.asList(
createEmailDomain("gmail.com"),
createEmailDomain("yahoo.com"),
createEmailDomain("hotmail.com")
);

when(emailDomainRepository.findAll()).thenReturn(allowedDomains);

assertFalse(validator.isValid("test@outlook.com", context));
assertFalse(validator.isValid("user@example.com", context));
}
}

Controller Testing with Invalid Data

@Test
public void givenProgramIdNotFound_whenGetProgramById_shouldThrowException() {
when(programRepository.findById(1)).thenReturn(Optional.empty());

RuntimeException exception = assertThrows(RuntimeException.class, () -> {
programService.getProgramById(1);
});

assertThat(exception.getMessage()).isEqualTo("Program not found");
}

Migration and Upgrade Path

Jakarta Migration (JSR-303 to Jakarta)

// Old (JSR-303)
import javax.validation.constraints.NotBlank;

// New (Jakarta)
import jakarta.validation.constraints.NotBlank;

Validation Framework Updates

  • Bean Validation specification changes
  • Custom validator compatibility
  • Message interpolation updates
  • Performance improvements

This comprehensive guide covers all aspects of data validation in the Student Management System. For implementation details and examples, refer to the source code in the respective packages and the accompanying test cases.