Appearance
Validations
Squizy provides automatic entity validation powered by Jakarta Bean Validation (Hibernate Validator). Validation constraints are declared directly on your entity fields using standard annotations, and Squizy enforces them at the right time — applying different rules during creation and updates through validation groups.
Supported Constraints
Squizy supports the full set of standard Jakarta validation constraints:
| Constraint | Applies to | Description |
|---|---|---|
@NotNull | Any type | Field must not be null |
@NotBlank | String | Must not be null, empty, or only whitespace |
@NotEmpty | String, Collections | Must not be null or empty |
@Size(min, max) | String, Collections | Length/size must be within range |
@Min(value) | Numeric | Minimum value (inclusive) |
@Max(value) | Numeric | Maximum value (inclusive) |
@DecimalMin(value) | Numeric | Minimum decimal value |
@DecimalMax(value) | Numeric | Maximum decimal value |
@Positive | Numeric | Must be strictly positive |
@PositiveOrZero | Numeric | Must be zero or positive |
@Negative | Numeric | Must be strictly negative |
@NegativeOrZero | Numeric | Must be zero or negative |
@Email | String | Must be a valid email address |
@Past | Temporal | Must be a date/time in the past |
@PastOrPresent | Temporal | Must be in the past or present |
@Future | Temporal | Must be a date/time in the future |
@FutureOrPresent | Temporal | Must be in the future or present |
Example
java
@SquizyEntity
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotBlank
@Size(max = 100)
private String name;
@NotNull
@PositiveOrZero
private BigDecimal price;
@Email
private String contactEmail;
@Size(min = 1)
@OneToMany(cascade = CascadeType.ALL)
private List<@Valid ProductVariant> variants;
// ...
}Squizy validates the entity automatically when it is created via POST or updated via PATCH. No additional controller configuration is needed — BaseSquizyController handles it.
Validation Groups
Not all validations should run at every stage. For example, you may want to require a field during creation but allow it to remain unchanged during a patch update. Squizy uses validation groups to differentiate between these phases.
Available Groups
Squizy defines two validation group marker interfaces:
| Group | Interface | When it runs |
|---|---|---|
| Create | CreateValidationGroup | During POST (entity creation) |
| Update | UpdateValidationGroup | During PATCH (entity update) |
The standard Default group always runs in both phases.
How It Works
When Squizy processes a request:
POST(create): validates with groupsDefault+CreateValidationGroupPATCH(update): validates with groupsDefault+UpdateValidationGroup
This means:
- Constraints without a
groupsattribute belong toDefaultand run in both phases. - Constraints assigned to
CreateValidationGrouprun only during creation. - Constraints assigned to
UpdateValidationGrouprun only during updates.
Example with Validation Groups
java
@SquizyEntity
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotBlank // Default group → runs on create AND update
private String name;
@NotNull(groups = CreateValidationGroup.class) // Only required on creation
private String department;
@Size(min = 8, groups = UpdateValidationGroup.class) // Only enforced on update
private String password;
// ...
}In this example:
nameis always required (both create and update).departmentis required only when creating — a patch that doesn't touchdepartmentwon't fail.passwordmust be at least 8 characters, but only when explicitly updated.
Combining Groups
You can assign a constraint to multiple groups:
java
@NotNull(groups = {CreateValidationGroup.class, UpdateValidationGroup.class})
private String code;This is functionally equivalent to using Default group (no groups attribute), but gives you explicit control if you want to exclude it from the Default group for other tooling purposes.
Nested Entity Validation
Use @Valid on relationship fields to cascade validation to nested entities:
java
@Valid
@OneToOne(cascade = CascadeType.ALL)
private Address address;
@OneToMany(cascade = CascadeType.ALL)
private List<@Valid OrderLine> orderLines;When the parent entity is validated, all nested entities annotated with @Valid are validated too, including their own constraint annotations.
Custom Validators
You can create custom validation constraints using the standard Jakarta ConstraintValidator mechanism. Custom validators work at both field and class level.
Class-Level Custom Validator
java
@Documented
@Constraint(validatedBy = UniqueNameValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueName {
String message() default "Name combination must be unique";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}java
public class UniqueNameValidator implements ConstraintValidator<UniqueName, Employee> {
@Override
public boolean isValid(Employee employee, ConstraintValidatorContext context) {
// Custom validation logic
boolean valid = !employee.getFirstName().equals(employee.getLastName());
if (!valid) {
context.buildConstraintViolationWithTemplate("{uniqueName.message}")
.addPropertyNode("firstName")
.addConstraintViolation();
}
return valid;
}
}java
@UniqueName
@SquizyEntity
@Entity
public class Employee {
// ...
}Custom validators can use validation groups the same way as standard constraints.
Automatic Required Field Detection
Squizy automatically determines whether a field is required based on its validation annotations and JPA metadata:
- Fields with
@NotNull,@NotBlank, or@NotEmptyare marked as required. - Non-optional JPA
@Columnfields (withnullable = false) are also marked as required. - For collections,
@Size(min = 1)or higher marks the field as required.
This information is exposed through the entity descriptor API and used by the frontend to show required indicators in forms.
Descriptor Integration
Squizy automatically extracts validation metadata from your entity annotations and exposes it through the entity descriptor. Each property in the descriptor includes a validations array with:
name: the validation rule identifier (e.g.,standard.notBlank,standard.size)messageTemplate: the i18n message key for the errormessageParameters: constraint parameters (e.g.,min,maxvalues)groups: which validation groups this rule applies to
This allows the frontend to perform client-side validation matching the server rules, and to display localized error messages.
Error Responses
When validation fails, Squizy returns an HTTP 400 Bad Request with a structured ValidationErrorResponse:
json
{
"squizyErrorCode": "VALIDATION_ERROR",
"message": "Invalid request data.",
"violations": [
{
"entity": "Product",
"property": "name",
"messageTemplate": "{global.validation.rules.standard.notBlank.message}",
"messageParameters": {}
},
{
"entity": "Product",
"property": "price",
"messageTemplate": "{global.validation.rules.standard.min.message}",
"messageParameters": {
"value": 0
}
}
]
}Each violation includes:
| Field | Description |
|---|---|
entity | The entity name (as registered in JPA) |
property | The property path that failed validation (see Field-Level vs Entity-Level Errors) |
messageTemplate | An i18n key that can be resolved to a localized message |
messageParameters | Parameters from the constraint (e.g., min, max, value) for message interpolation |
TIP
The messageTemplate uses a consistent format ({global.validation.rules.<name>.message}) that can be used by the frontend for localized error display. Standard validations are prefixed with standard. and custom validations with custom..
Field-Level vs Entity-Level Errors
The property field in each violation determines whether the error is associated with a specific field or with the entity as a whole.
Field-Level Errors
When a constraint is declared on a field (e.g., @NotBlank, @Size), the property contains the path to the invalid field. This allows the frontend to highlight the specific field in a form.
json
{
"squizyErrorCode": "VALIDATION_ERROR",
"message": "Invalid request data.",
"violations": [
{
"entity": "Product",
"property": "name",
"messageTemplate": "{global.validation.rules.standard.notBlank.message}",
"messageParameters": {}
}
]
}For nested entities validated with @Valid, the property path includes the full path to the nested field:
json
{
"entity": "Product",
"property": "variants[0].size",
"messageTemplate": "{global.validation.rules.standard.notBlank.message}",
"messageParameters": {}
}Entity-Level Errors
When a constraint is declared at the class level (e.g., a custom validator annotated on the entity class), the error may apply to the entity as a whole rather than a specific field.
If the custom validator does not add a property node in its violation, the property field will be empty. This signals an entity-level error that cannot be tied to a single field:
json
{
"squizyErrorCode": "VALIDATION_ERROR",
"message": "Invalid request data.",
"violations": [
{
"entity": "Employee",
"property": "",
"messageTemplate": "Name combination must be unique",
"messageParameters": {}
}
]
}However, class-level validators can target a specific field by calling addPropertyNode() in the validator implementation (see Custom Validators). In that case, even though the annotation is at the class level, the violation will have a non-empty property and behave like a field-level error:
json
{
"entity": "Employee",
"property": "firstName",
"messageTemplate": "{uniqueName.message}",
"messageParameters": {}
}TIP
To determine the error type in the frontend:
propertyis non-empty → field-level error. Highlight the specific field.propertyis empty → entity-level error. Display as a general form error.