Skip to content

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:

ConstraintApplies toDescription
@NotNullAny typeField must not be null
@NotBlankStringMust not be null, empty, or only whitespace
@NotEmptyString, CollectionsMust not be null or empty
@Size(min, max)String, CollectionsLength/size must be within range
@Min(value)NumericMinimum value (inclusive)
@Max(value)NumericMaximum value (inclusive)
@DecimalMin(value)NumericMinimum decimal value
@DecimalMax(value)NumericMaximum decimal value
@PositiveNumericMust be strictly positive
@PositiveOrZeroNumericMust be zero or positive
@NegativeNumericMust be strictly negative
@NegativeOrZeroNumericMust be zero or negative
@EmailStringMust be a valid email address
@PastTemporalMust be a date/time in the past
@PastOrPresentTemporalMust be in the past or present
@FutureTemporalMust be a date/time in the future
@FutureOrPresentTemporalMust 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:

GroupInterfaceWhen it runs
CreateCreateValidationGroupDuring POST (entity creation)
UpdateUpdateValidationGroupDuring PATCH (entity update)

The standard Default group always runs in both phases.

How It Works

When Squizy processes a request:

  • POST (create): validates with groups Default + CreateValidationGroup
  • PATCH (update): validates with groups Default + UpdateValidationGroup

This means:

  • Constraints without a groups attribute belong to Default and run in both phases.
  • Constraints assigned to CreateValidationGroup run only during creation.
  • Constraints assigned to UpdateValidationGroup run 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:

  • name is always required (both create and update).
  • department is required only when creating — a patch that doesn't touch department won't fail.
  • password must 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 @NotEmpty are marked as required.
  • Non-optional JPA @Column fields (with nullable = 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 error
  • messageParameters: constraint parameters (e.g., min, max values)
  • 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:

FieldDescription
entityThe entity name (as registered in JPA)
propertyThe property path that failed validation (see Field-Level vs Entity-Level Errors)
messageTemplateAn i18n key that can be resolved to a localized message
messageParametersParameters 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:

  • property is non-empty → field-level error. Highlight the specific field.
  • property is empty → entity-level error. Display as a general form error.