DEV Community

David Lastrucci
David Lastrucci

Posted on

Declarative validation: keep bad data out of your database

In the previous article we covered entity mapping and types. But mapping alone does not guarantee data quality. What happens if someone inserts a product with a negative price, or a contact with an empty name?

Trysil provides a set of validation attributes that you place directly on entity fields. The ORM checks them automatically before every Insert and Update — if validation fails, no SQL is executed and you get a clear exception.

Available validation attributes

Attribute Applies to Rule
TRequired String, DateTime, TTNullable, TTLazy Field cannot be empty, zero, or null
TMaxLength(n) String Length must be <= n
TMinLength(n) String Length must be >= n
TMaxValue(n) Integer, Double Value must be <= n
TMinValue(n) Integer, Double Value must be >= n
TGreater(n) Integer, Double Value must be > n
TLess(n) Integer, Double Value must be < n
TRange(min, max) Integer, Double Value must be in [min, max]
TRegex(pattern) String Value must match the regex
TEmail String Valid email format (built-in regex)

All attributes live in Trysil.Validation.Attributes.

Using validation attributes

Here is a TTEmployee entity with comprehensive validation:

unit Employee.Model;

{$WARN UNKNOWN_CUSTOM_ATTRIBUTE ERROR}

interface

uses
  Trysil.Types,
  Trysil.Attributes,
  Trysil.Validation.Attributes;

type
  [TTable('Employees')]
  [TSequence('EmployeesID')]
  TTEmployee = class
  strict private
    [TPrimaryKey]
    [TColumn('ID')]
    FID: TTPrimaryKey;

    [TRequired]
    [TMinLength(2)]
    [TMaxLength(50)]
    [TColumn('Firstname')]
    FFirstname: String;

    [TRequired]
    [TMinLength(2)]
    [TMaxLength(50)]
    [TColumn('Lastname')]
    FLastname: String;

    [TMaxLength(255)]
    [TEmail]
    [TColumn('Email')]
    FEmail: String;

    [TRange(18, 70)]
    [TColumn('Age')]
    FAge: Integer;

    [TGreater(0.00)]
    [TColumn('Salary')]
    FSalary: Double;

    [TColumn('VersionID')]
    [TVersionColumn]
    FVersionID: TTVersion;
  public
    property ID: TTPrimaryKey read FID;
    property Firstname: String read FFirstname write FFirstname;
    property Lastname: String read FLastname write FLastname;
    property Email: String read FEmail write FEmail;
    property Age: Integer read FAge write FAge;
    property Salary: Double read FSalary write FSalary;
  end;
Enter fullscreen mode Exit fullscreen mode

With this definition:

  • Firstname and Lastname must be non-empty and between 2 and 50 characters
  • Email must be a valid email address (if provided — TEmail skips empty strings)
  • Age must be between 18 and 70
  • Salary must be greater than zero

Display names for error messages

By default, validation errors use the column name. You can override this with TDisplayName:

[TRequired]
[TDisplayName('First name')]
[TColumn('Firstname')]
FFirstname: String;
Enter fullscreen mode Exit fullscreen mode

Now the error message will say "First name is required" instead of "Firstname is required".

Custom error messages

Every validation attribute accepts an optional error message:

[TRequired('Please enter the employee name')]
[TColumn('Firstname')]
FFirstname: String;

[TRange(18, 70, 'Age must be between 18 and 70')]
[TColumn('Age')]
FAge: Integer;
Enter fullscreen mode Exit fullscreen mode

Handling validation errors

Validation runs automatically on Insert and Update. If any rule fails, Trysil raises an ETValidationException:

uses
  Trysil.Exceptions;

var
  LEmployee: TTEmployee;
begin
  LEmployee := LContext.CreateEntity<TTEmployee>();
  LEmployee.Firstname := '';   // will fail TRequired
  LEmployee.Salary := -1000;   // will fail TGreater(0.00)

  try
    LContext.Insert<TTEmployee>(LEmployee);
  except
    on E: ETValidationException do
      ShowMessage(E.Message);
  end;
end;
Enter fullscreen mode Exit fullscreen mode

You can also validate explicitly without inserting:

try
  LContext.Validate<TTEmployee>(LEmployee);
except
  on E: ETValidationException do
    // handle errors
end;
Enter fullscreen mode Exit fullscreen mode

This is useful when you want to validate in a UI before the user clicks "Save".

Custom validators

For business rules that go beyond simple field checks, you can write a custom validator method and mark it with [TValidator]:

[TTable('Invoices')]
[TSequence('InvoicesID')]
TTInvoice = class
strict private
  [TPrimaryKey]
  [TColumn('ID')]
  FID: TTPrimaryKey;

  [TRequired]
  [TColumn('IssueDate')]
  FIssueDate: TDateTime;

  [TRequired]
  [TColumn('DueDate')]
  FDueDate: TDateTime;

  [TColumn('VersionID')]
  [TVersionColumn]
  FVersionID: TTVersion;
public
  [TValidator]
  procedure ValidateDates(const AErrors: TTValidationErrors);

  property ID: TTPrimaryKey read FID;
  property IssueDate: TDateTime read FIssueDate write FIssueDate;
  property DueDate: TDateTime read FDueDate write FDueDate;
end;
Enter fullscreen mode Exit fullscreen mode
procedure TTInvoice.ValidateDates(const AErrors: TTValidationErrors);
begin
  if FDueDate < FIssueDate then
    AErrors.Add('DueDate', 'Due date cannot be before issue date');
end;
Enter fullscreen mode Exit fullscreen mode

The method is called automatically alongside the attribute-based validation. You can add multiple errors to AErrors — they are all collected before the exception is raised.

Validation and the ORM pipeline

Here is what happens when you call LContext.Insert<T>(LEntity):

  1. Validation — all [TRequired], [TMaxLength], [TRange], etc. attributes are checked, plus any [TValidator] methods
  2. Events[BeforeInsert] methods are called (more on events in a future article)
  3. SQL execution — the INSERT statement is generated and executed
  4. Post-events[AfterInsert] methods are called

If step 1 fails, steps 2-4 never happen. Your database stays clean.

What is next

We now have entities that are both correctly mapped and validated before they reach the database. In the next article we will explore filtering and querying — how to search, sort, and paginate data using Trysil's fluent filter builder.


Trysil is open-source and available on GitHub. If you find it useful, a star goes a long way!

Top comments (0)