Vix.cpp v2.8.5 is here Read the blog
Skip to content

Base Models ​

BaseModel<T> lets a type own its validation schema.

This is useful when validation belongs naturally to a model or input type. Instead of creating a schema in every handler or helper function, the type declares a static schema() function once and then receives instance-level validation methods through CRTP.

The result is a simple workflow:

txt
model instance -> model schema -> ValidationResult

BaseModel<T> does not add fields to the derived type. It only gives the derived type a reusable validation entry point built on top of Schema<T>.

Use the public Vix header when working with base models:

cpp
#include <vix/validation.hpp>

For examples that print output:

cpp
#include <vix/print.hpp>

Basic model ​

A base model inherits from vix::validation::BaseModel<Derived> and provides a static schema() function.

cpp
#include <string>
#include <vix/validation.hpp>
#include <vix/print.hpp>

struct UserInput : vix::validation::BaseModel<UserInput>
{
  std::string email;

  static vix::validation::Schema<UserInput> schema()
  {
    return vix::validation::schema<UserInput>()
        .field(
            "email",
            &UserInput::email,
            vix::validation::field<std::string>()
                .required("email is required")
                .email("invalid email"));
  }
};

int main()
{
  UserInput input{"not-an-email"};

  auto result = input.validate();

  if (!result.ok())
  {
    for (const auto &error : result.errors.all())
    {
      vix::print(
          error.field,
          vix::validation::to_string(error.code),
          error.message);
    }

    return 1;
  }

  vix::print("user input is valid");

  return 0;
}

The model owns its validation policy. Any place that receives a UserInput can call input.validate() without knowing how the schema is built.

BaseModel contract ​

A derived type must follow this shape:

cpp
struct MyType : vix::validation::BaseModel<MyType>
{
  static vix::validation::Schema<MyType> schema()
  {
    return vix::validation::schema<MyType>();
  }
};

The static schema() function must return exactly:

cpp
vix::validation::Schema<MyType>

This is checked at compile time. If the derived type does not provide the expected schema function, the error is reported while compiling the validation code.

Instance validation ​

Use validate() on an instance when you want to validate the object itself.

cpp
#include <string>
#include <vix/validation.hpp>
#include <vix/print.hpp>

struct RegisterInput : vix::validation::BaseModel<RegisterInput>
{
  std::string email;
  std::string password;

  static vix::validation::Schema<RegisterInput> schema()
  {
    return vix::validation::schema<RegisterInput>()
        .field(
            "email",
            &RegisterInput::email,
            vix::validation::field<std::string>()
                .required("email is required")
                .email("invalid email"))
        .field(
            "password",
            &RegisterInput::password,
            vix::validation::field<std::string>()
                .required("password is required")
                .length_min(8, "password is too short"));
  }
};

int main()
{
  RegisterInput input{
      "bad",
      "123"};

  auto result = input.validate();

  if (!result.ok())
  {
    for (const auto &error : result.errors.all())
    {
      vix::print(error.field, error.message);
    }

    return 1;
  }

  return 0;
}

This is the most common use of BaseModel<T>.

Convenience validity check ​

Use is_valid() when you only need a boolean answer.

cpp
#include <string>
#include <vix/validation.hpp>
#include <vix/print.hpp>

struct TokenInput : vix::validation::BaseModel<TokenInput>
{
  std::string token;

  static vix::validation::Schema<TokenInput> schema()
  {
    return vix::validation::schema<TokenInput>()
        .field(
            "token",
            &TokenInput::token,
            vix::validation::field<std::string>()
                .required("token is required")
                .length_min(10, "token is too short"));
  }
};

int main()
{
  TokenInput input{"abc"};

  if (!input.is_valid())
  {
    vix::print("token input is invalid");
    return 1;
  }

  vix::print("token input is valid");

  return 0;
}

is_valid() is convenient for small checks. Use validate() when you need the errors.

Static validation ​

BaseModel<T> can also validate an object without calling through the instance.

cpp
#include <string>
#include <vix/validation.hpp>
#include <vix/print.hpp>

struct ProductInput : vix::validation::BaseModel<ProductInput>
{
  std::string title;
  int quantity{0};

  static vix::validation::Schema<ProductInput> schema()
  {
    return vix::validation::schema<ProductInput>()
        .field(
            "title",
            &ProductInput::title,
            vix::validation::field<std::string>()
                .required("title is required")
                .length_min(3, "title is too short"))
        .field(
            "quantity",
            &ProductInput::quantity,
            vix::validation::field<int>()
                .min(1, "quantity must be at least 1"));
  }
};

int main()
{
  ProductInput input{
      "TV",
      0};

  auto result = ProductInput::validate(input);

  if (!result.ok())
  {
    for (const auto &error : result.errors.all())
    {
      vix::print(error.field, error.message);
    }

    return 1;
  }

  return 0;
}

This form is useful in generic code or helper functions where the object is passed separately.

Access the cached schema ​

Use BaseModel<T>::schema() through the derived type when advanced code needs direct access to the schema.

cpp
#include <string>
#include <vix/validation.hpp>
#include <vix/print.hpp>

struct ProfileInput : vix::validation::BaseModel<ProfileInput>
{
  std::string username;

  static vix::validation::Schema<ProfileInput> schema()
  {
    return vix::validation::schema<ProfileInput>()
        .field(
            "username",
            &ProfileInput::username,
            vix::validation::field<std::string>()
                .required("username is required")
                .length_min(3, "username is too short")
                .length_max(30, "username is too long"));
  }
};

int main()
{
  ProfileInput input{"ga"};

  const auto &schema = ProfileInput::BaseModel::schema();
  auto result = schema.validate(input);

  if (!result.ok())
  {
    vix::print("profile input is invalid");
    return 1;
  }

  return 0;
}

In normal application code, input.validate() is clearer. Direct schema access is mainly useful for advanced workflows such as introspection, reusable adapters, or integration code.

Schema caching ​

BaseModel<T> caches the schema internally.

The derived type still declares the schema as a function:

cpp
static vix::validation::Schema<UserInput> schema()
{
  return vix::validation::schema<UserInput>()
      .field(...);
}

But BaseModel<T> stores the constructed schema in a static local cache and reuses it for validation calls. This avoids rebuilding the schema every time validate() is called.

The cache is initialized once and is thread-safe under normal C++11 static local initialization rules.

Use typed fields ​

Base models use the same Schema<T> field validation API.

cpp
#include <string>
#include <vix/validation.hpp>
#include <vix/print.hpp>

struct AccountInput : vix::validation::BaseModel<AccountInput>
{
  std::string username;
  int age{0};

  static vix::validation::Schema<AccountInput> schema()
  {
    return vix::validation::schema<AccountInput>()
        .field(
            "username",
            &AccountInput::username,
            vix::validation::field<std::string>()
                .required("username is required")
                .length_min(3, "username is too short")
                .length_max(30, "username is too long"))
        .field(
            "age",
            &AccountInput::age,
            vix::validation::field<int>()
                .between(18, 120, "age is out of range"));
  }
};

int main()
{
  AccountInput input{
      "ga",
      10};

  auto result = input.validate();

  if (!result.ok())
  {
    for (const auto &error : result.errors.all())
    {
      vix::print(error.field, error.message);
    }

    return 1;
  }

  return 0;
}

The model can mix string rules, numeric rules, and custom field rules through the schema.

Use parsed fields ​

A base model schema can also parse text fields before applying typed rules.

cpp
#include <string>
#include <vix/validation.hpp>
#include <vix/print.hpp>

struct ProfileInput : vix::validation::BaseModel<ProfileInput>
{
  std::string age;

  static vix::validation::Schema<ProfileInput> schema()
  {
    return vix::validation::schema<ProfileInput>()
        .parsed<int>(
            "age",
            &ProfileInput::age,
            vix::validation::parsed<int>()
                .between(18, 120, "age is out of range")
                .parse_message("age must be a number"));
  }
};

int main()
{
  ProfileInput input{"abc"};

  auto result = input.validate();

  if (!result.ok())
  {
    for (const auto &error : result.errors.all())
    {
      vix::print(
          error.field,
          vix::validation::to_string(error.code),
          error.message);
    }

    return 1;
  }

  return 0;
}

This is useful for boundary models that store raw text but still need typed validation rules.

Use whole-object checks ​

The schema owned by a base model can include object-level checks.

cpp
#include <string>
#include <vix/validation.hpp>
#include <vix/print.hpp>

struct RegisterInput : vix::validation::BaseModel<RegisterInput>
{
  std::string password;
  std::string confirm_password;

  static vix::validation::Schema<RegisterInput> schema()
  {
    return vix::validation::schema<RegisterInput>()
        .field(
            "password",
            &RegisterInput::password,
            vix::validation::field<std::string>()
                .required("password is required")
                .length_min(8, "password is too short"))
        .field(
            "confirm_password",
            &RegisterInput::confirm_password,
            vix::validation::field<std::string>()
                .required("password confirmation is required"))
        .check([](const RegisterInput &input, vix::validation::ValidationErrors &errors)
        {
          if (input.password != input.confirm_password)
          {
            errors.add(
                "confirm_password",
                vix::validation::ValidationErrorCode::Custom,
                "password confirmation does not match");
          }
        });
  }
};

int main()
{
  RegisterInput input{
      "secret-123",
      "other-secret"};

  auto result = input.validate();

  if (!result.ok())
  {
    for (const auto &error : result.errors.all())
    {
      vix::print(error.field, error.message);
    }

    return 1;
  }

  return 0;
}

Whole-object checks are useful for constraints that cannot be expressed by validating one field alone.

Add custom rules ​

Custom rules can be used inside a base model schema the same way they are used in normal schemas.

cpp
#include <string>
#include <vix/validation.hpp>
#include <vix/print.hpp>

struct UserInput : vix::validation::BaseModel<UserInput>
{
  std::string username;

  static vix::validation::Rule<std::string> not_reserved_username()
  {
    return [](std::string_view field,
              const std::string &value,
              vix::validation::ValidationErrors &errors)
    {
      if (value == "admin" || value == "root")
      {
        errors.add(
            std::string(field),
            vix::validation::ValidationErrorCode::Custom,
            "username is reserved");
      }
    };
  }

  static vix::validation::Schema<UserInput> schema()
  {
    return vix::validation::schema<UserInput>()
        .field(
            "username",
            &UserInput::username,
            vix::validation::field<std::string>()
                .required("username is required")
                .length_min(3, "username is too short")
                .rule(not_reserved_username()));
  }
};

int main()
{
  UserInput input{"admin"};

  auto result = input.validate();

  if (!result.ok())
  {
    for (const auto &error : result.errors.all())
    {
      vix::print(error.field, error.message);
    }

    return 1;
  }

  return 0;
}

This keeps application-specific validation close to the type that owns the data.

Use BaseModel in helper functions ​

A helper can accept a base-model type and call validation directly on it.

cpp
#include <string>
#include <vix/validation.hpp>
#include <vix/print.hpp>

struct SettingsInput : vix::validation::BaseModel<SettingsInput>
{
  std::string mode;

  static vix::validation::Schema<SettingsInput> schema()
  {
    return vix::validation::schema<SettingsInput>()
        .field(
            "mode",
            &SettingsInput::mode,
            vix::validation::field<std::string>()
                .required("mode is required")
                .in_set(
                    {"development", "production"},
                    "mode is not allowed"));
  }
};

template <typename Model>
bool print_validation_status(const Model &model)
{
  auto result = model.validate();

  if (!result.ok())
  {
    for (const auto &error : result.errors.all())
    {
      vix::print(error.field, error.message);
    }

    return false;
  }

  vix::print("model is valid");
  return true;
}

int main()
{
  SettingsInput input{"test"};

  if (!print_validation_status(input))
  {
    return 1;
  }

  return 0;
}

This works well when your application uses several validated input types with a consistent validation interface.

When to use BaseModel ​

Use BaseModel<T> when a type should own its validation rules and expose validation as part of its interface.

It is a good fit for request input models, configuration models, command input structures, and internal DTOs that have stable validation rules.

For one-off field checks, use validate(...). For reusable validation of a type without inheritance, use a standalone Schema<T>. For raw input binding and cleaned output, use Form<T>.

API overview ​

APIPurpose
BaseModel<Derived>CRTP base class for schema-driven validation.
Derived::schema()Static function required by the derived type.
validate() constValidate the current instance.
is_valid() constReturn true when the current instance has no validation errors.
static validate(const Derived &obj)Validate a provided instance.
static schema()Access the cached schema through the base model.

Next step ​

Continue with forms to see how the module handles the full boundary workflow: binding raw input, validating the form object, and returning either cleaned output or structured errors.

Released under the MIT License.