Skip to main content

Validation in Entities and DTOs

Validation plays a critical role in ensuring the consistency and integrity of the data flowing through the system. In our project, we use Hibernate Validator.

Where Validation Happens

We apply validation at two key levels:

  • Backend-level validation: Ensures that incoming data respects constraints before it's processed or persisted. This protects the system from invalid input early in the request lifecycle.
  • Database-level constraints: Declared using annotations like @Column(length = ...), these enforce limits at the database schema level as a final safety net.

The combination of these two levels provides a double layer of protection.

How we implement it

These annotations ensure that fields are not only constrained by the database schema but are also validated before reaching the database, improving error reporting and reducing unnecessary transactions.

Moreover, one of the benefits of using SOKit is that validation annotations present in the entity are automatically propagated to the generated DTOs.

This means your input DTOs (CompanySubmitDto, CompanyApproveDto, etc.) will carry over the same validation logic.

CompanyEntity.java
package com.dev.registration.company.data;

import jakarta.persistence.*;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import com.strategyobject.sokit.extensions.dm.api.annotations.Dto;
import com.strategyobject.sokit.extensions.document.api.entities.DocumentEntity;

@Entity
@Table(name = "TBL_COMPANY")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Dto(variant = "View", altVariants = "Entry")
@Dto(variant = "Searchable", altVariants = "Entry")
@Dto(variant = "Submit", altVariants = "Entry")
@Dto(variant = "Approve", altVariants = "Entry", includeAll = false)
@Dto(variant = "Reject", altVariants = "Entry", includeAll = false)
public class CompanyEntity extends DocumentEntity implements Serializable {

@NotEmpty
@Size(max = 100)
@Column(name = "COMPANY_NAME", length = 100)
private String companyName;

@NotEmpty
@Size(max = 5)
@Column(name = "LEGAL_FORM", length = 5)
private String legalForm; // enum?

@NotNull
@Column(name = "ESTABLISHED_DATE")
private LocalDate establishedDate;

@NotNull
@Positive
@Column(name = "CAPITAL")
private BigDecimal capital;

@NotNull
@Positive
@Column(name = "EMPLOYEES")
@Min(1)
private Integer employees = 0;

@Embedded @Valid @NotNull private CompanyType type;

private String creatorSubject;

@NotEmpty
@Size(max = 50)
@Column(name = "PHONE", length = 50)
private String phone;

@NotEmpty
@Size(max = 50)
@Email(message = "email not vaild")
@Column(name = "EMAIL", length = 50)
private String email;

@Size(max = 11)
@Column(name = "VAT_NUMBER", length = 11)
@Dto.Include({"Approve"})
private String vatNumber;

@Size(max = 4000)
@Column(name = "remark", length = 4000)
@Dto.Include({"Approve", "Reject"})
private String remark;

@Embedded @Valid @NotNull private CompanyAddress address;

@NotEmpty
@Column(name = "website", length = 150)
private String website;

@Valid
@NotNull
@Dto.Exclude({"Searchable"})
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "parent")
@Size(min = 1)
private Set<@Valid @NotNull ShareholderEntity> shareholders;

@Override
public int hashCode() {
return super.hashCode();
}

@Override
public boolean equals(Object o) {
return super.equals(o);
}
}

Here you can find which validation we applied to the other remaining classes:

ShareholderEntity.java
package com.dev.registration.company.data;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.io.Serializable;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import com.strategyobject.sokit.extensions.core.exceptions.dto.SubElement;
import com.strategyobject.sokit.extensions.dm.api.annotations.Dto;

@SuppressWarnings({"MoreThanOneIdInspection", "unused"})
@Entity
@Table(name = "TBL_SHAREHOLDER")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Dto(variant = "Entry")
public class ShareholderEntity extends SubElement implements Serializable {

@NotEmpty
@Size(max = 255)
@Id
@Column(name = "SHAREHOLDER_ID", nullable = false, updatable = false)
private String id;

@Id
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "DOCUMENT_ID", referencedColumnName = "DOCUMENT_ID")
private CompanyEntity parent;

@NotEmpty
@Size(max = 50)
@Column(name = "FIRST_NAME", length = 50)
private String firstName;

@NotEmpty
@Size(max = 50)
@Column(name = "LAST_NAME", length = 50)
private String lastName;

@NotEmpty
@Size(max = 50)
@Column(name = "PHONE", length = 50)
private String phone;

@NotEmpty
@Size(max = 50)
@Email(message = "email not vaild")
@Column(name = "EMAIL", length = 50)
private String email;

@NotNull
@Positive
@Column(name = "shares")
private Integer shares = 0;

@NotNull
@Min(1)
@Max(100)
@Column(name = "percentage")
private Double percentage;

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
ShareholderEntity that = (ShareholderEntity) o;
return Objects.equals(id, that.id);
}

@Override
public int hashCode() {
return Objects.hashCode(id);
}
}
CompanyType.java
package com.dev.registration.company.data;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import java.io.Serializable;
import lombok.*;

import com.strategyobject.sokit.extensions.dm.api.annotations.Dto;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Embeddable
@Dto(variant = "Entry")
public class CompanyType implements Serializable {
@NotEmpty
@Size(max = 10)
@Column(name = "CODE", length = 10)
private String code;

@NotEmpty
@Size(max = 255)
@Column(name = "DESCRIPTION", length = 255)
private String description;
}
CompanyAddress.java
package com.dev.registration.company.data;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import java.io.Serializable;
import lombok.*;

import com.strategyobject.sokit.extensions.dm.api.annotations.Dto;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Embeddable
@Dto(variant = "Entry")
public class CompanyAddress implements Serializable {
@NotEmpty
@Size(max = 100)
@Column(name = "street", length = 100)
private String street;

@NotEmpty
@Size(max = 50)
@Column(name = "CITY", length = 50)
private String city;

@NotEmpty
@Size(max = 20)
@Column(name = "STATE", length = 20)
private String state;

@NotEmpty
@Size(max = 10)
@Column(name = "ZIP", length = 10)
private String zip;
}