Skip to content

Migrate request input validation from tsoa annotations to Zod #871

Description

@plouka13

Background

Our request DTOs currently express validation constraints through tsoa JSDoc annotations, e.g.:

export interface RegisterRequest {
  /** @minLength 1 */
  firstName: string
  /** @pattern ^(.+)@(.+)$ Please provide valid email */
  email: string
  /** @minLength 14 Password must be at least 14 characters */
  password: string
  role: Role
}

Problem

tsoa annotations only accept hardcoded literals — there's no way to reference a variable or shared constant inside them (confirmed after digging through the tsoa docs). This has a few consequences:

  • Non-DRY: the same constraints (min lengths, patterns, password rules) are duplicated as string literals across DTOs with no single source of truth.
  • Brittle / drift-prone: the annotations can silently fall out of sync with the actual runtime validation logic (e.g. the PasswordStrength function). Right now our only guard against this is a set of backend tests that flag when the annotations and PasswordStrength diverge — a safety net, not a fix.

Proposed solution

Port input validation to Zod, using zod-openapi to generate the OpenAPI/Swagger schema from the Zod schemas. This gives us:

  • A single source of truth for each field's constraints, reusable across validation and doc generation.
  • Shared constants (e.g. MIN_PASSWORD_LENGTH) that can feed both the schema and functions like PasswordStrength, eliminating the drift problem.
  • Consistency with Elsa Data, which already uses Zod.

Scope / next steps

  • Evaluate zod-openapi integration with our current tsoa/OpenAPI setup
  • Prototype migration of RegisterRequest as a reference DTO
  • Define shared validation constants and wire them into PasswordStrength
  • Plan incremental rollout across remaining DTOs

Notes

Surfaced from this slack discussion.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions