Skip to content

AnyVali C++ SDK Reference

Installation

CMake with FetchContent

Add AnyVali to your CMakeLists.txt:

include(FetchContent)

FetchContent_Declare(
  anyvali
  GIT_REPOSITORY https://github.com/BetterCorp/AnyVali-cpp.git
  GIT_TAG        v1.0.0
)
FetchContent_MakeAvailable(anyvali)

target_link_libraries(your_target PRIVATE anyvali::anyvali)

AnyVali C++ is header-only. It depends on nlohmann/json for JSON value representation, which is fetched automatically.

Requires C++17 or later.

Quick Start

#include <anyvali/anyvali.hpp>
#include <iostream>

using namespace anyvali;
using json = nlohmann::json;

int main() {
    // Define a schema
    auto user_schema = object()
        ->prop("id", int64())
        ->prop("name", string_()->min_length(1)->max_length(100))
        ->prop("email", string_()->format("email"))
        ->prop("age", optional_(int_()->min(0)->max(150)))
        ->prop("role", enum_({"admin", "user", "guest"}))
        ->required({"id", "name", "email"});

    // Parse input (throws ValidationError on failure)
    json input = {
        {"id", 42},
        {"name", "Alice"},
        {"email", "alice@example.com"},
        {"role", "admin"},
    };
    json user = user_schema->parse(input);

    // Safe parse (never throws)
    json bad_input = {{"id", "not-a-number"}, {"name", ""}};
    auto result = user_schema->safe_parse(bad_input);
    if (!result.success) {
        for (const auto& issue : result.issues) {
            std::cout << issue.path_string() << ": ["
                      << issue.code << "] " << issue.message << "\n";
        }
    }

    return 0;
}

Schema Builders

All schema builders are free functions in the anyvali namespace. They return std::shared_ptr to schema objects that can be further refined with chained constraints.

Several builder names use trailing underscores to avoid conflicts with C++ keywords and standard library names: string_(), bool_(), int_(), union_(), optional_().

Special Types

#include <anyvali/anyvali.hpp>
using namespace anyvali;

auto a = any();       // accepts any value
auto u = unknown();   // accepts any value, requires explicit handling
auto n = never();     // rejects all values

Primitives

auto s = string_();   // trailing underscore avoids std::string conflict
auto n = number();    // IEEE 754 float64
auto b = bool_();     // trailing underscore avoids bool keyword
auto z = null();

Numeric Types

AnyVali provides sized integer and float types for cross-language safety:

// Default integer (int64)
auto i = int_();      // trailing underscore avoids int keyword

// Signed integers
auto i8  = int8();    // -128 to 127
auto i16 = int16();   // -32,768 to 32,767
auto i32 = int32();   // -2^31 to 2^31-1
auto i64 = int64();   // -2^63 to 2^63-1

// Unsigned integers
auto u8  = uint8();   // 0 to 255
auto u16 = uint16();  // 0 to 65,535
auto u32 = uint32();  // 0 to 2^32-1
auto u64 = uint64();  // 0 to 2^64-1

// Floats
auto f32 = float32();
auto f64 = float64(); // same as number()

Literal and Enum

// Literal matches a single exact value
auto active = literal("active");
auto zero   = literal(0);
auto yes    = literal(true);

// Enum matches one of several allowed values
auto status = enum_({"pending", "active", "disabled"});

Note that enum_ uses a trailing underscore because enum is a C++ keyword.

Arrays and Tuples

// Homogeneous array of strings
auto tags = array(string_());

// Array with length constraints
auto top_three = array(string_())
    ->min_items(1)
    ->max_items(3);

// Tuple with fixed positional types
auto coordinate = tuple({
    float64(),
    float64(),
});

Objects

Objects use a builder pattern with ->prop() and ->required():

// Basic object with required and optional fields
auto user = object()
    ->prop("name", string_())
    ->prop("email", string_()->format("email"))
    ->prop("age", int_())
    ->required({"name", "email"});

// Unknown key handling
// Reject: error on unexpected keys (default)
auto strict = object()
    ->prop("name", string_())
    ->required({"name"})
    ->unknown_keys(UnknownKeyMode::Reject);

// Strip: silently remove unexpected keys
auto stripped = object()
    ->prop("name", string_())
    ->required({"name"})
    ->unknown_keys(UnknownKeyMode::Strip);

// Allow: keep unexpected keys as-is
auto passthrough = object()
    ->prop("name", string_())
    ->required({"name"})
    ->unknown_keys(UnknownKeyMode::Allow);

Records

A record validates a JSON object where all values conform to a single schema:

// Map of string keys to integer values
auto scores = record(int_());

json input = {{"alice", 95}, {"bob", 87}};
json result = scores->parse(input);
// => {"alice": 95, "bob": 87}

Composition

// Union: value must match at least one variant
auto string_or_int = union_({
    string_(),
    int_(),
});

// Intersection: value must match all schemas
auto named_and_aged = intersection({
    object()->prop("name", string_())->required({"name"}),
    object()->prop("age", int_())->required({"age"}),
});

Note that union_ uses a trailing underscore because union is a C++ keyword.

Modifiers

// Optional: value may be absent (field can be omitted)
auto maybe_age = optional_(int_());

// Nullable: value may be null
auto nullable_name = nullable(string_());

// Combine both: field can be omitted, or present as null or string
auto opt_nullable_name = optional_(
    nullable(string_())
);

Note that optional_ uses a trailing underscore to avoid conflict with std::optional.

References

References allow recursive and reusable schema definitions:

auto tree_node = object()
    ->prop("value", string_())
    ->prop("children", array(ref("#/definitions/TreeNode")))
    ->required({"value"});

String Constraints

All string constraints return the schema pointer for chaining:

auto schema = string_()
    ->min_length(1)          // minimum character count
    ->max_length(255)        // maximum character count
    ->pattern("^[A-Z]")     // regex pattern the value must match
    ->starts_with("Hello")  // value must start with this prefix
    ->ends_with("!")         // value must end with this suffix
    ->includes("world")     // value must contain this substring
    ->format("email");      // named format (email, uri, uuid, date, etc.)

Each constraint can be used independently:

auto slug = string_()
    ->pattern("^[a-z0-9]+(?:-[a-z0-9]+)*$")
    ->min_length(1)
    ->max_length(100);

auto email = string_()->format("email");
auto url   = string_()->format("uri");
auto uuid  = string_()->format("uuid");

Number Constraints

Number constraints apply to all numeric types (number, int_, int8--int64, uint8--uint64, float32, float64):

auto price = float64()
    ->min(0)               // value >= 0
    ->max(999999.99)       // value <= 999999.99
    ->multiple_of(0.01);   // must be a multiple of 0.01

auto rating = int_()
    ->min(1)
    ->max(5);

auto temperature = float64()
    ->exclusive_min(-273.15)    // value > -273.15
    ->exclusive_max(1000.0);    // value < 1000.0

Array Constraints

auto tags = array(string_())
    ->min_items(1)      // at least 1 element
    ->max_items(10);    // at most 10 elements

Coercion

Coercion transforms the input value before validation. It runs only when the value is present. Call ->coerce(config) on any schema to enable coercion.

Available Coercions

String to Integer

auto age = int_()->coerce({{"from", "string"}});
age->parse("42");   // => 42 (string coerced to integer)
age->parse(42);     // => 42 (already an integer, no coercion needed)

String to Number

auto price = number()->coerce({{"from", "string"}});
price->parse("3.14");  // => 3.14

String to Boolean

auto flag = bool_()->coerce({{"from", "string"}});
flag->parse("true");   // => true
flag->parse("false");  // => false
flag->parse("1");      // => true
flag->parse("0");      // => false

Trim Whitespace

auto trimmed = string_()->coerce({{"trim", true}});
trimmed->parse("  hello  ");  // => "hello"

Lowercase / Uppercase

auto lower = string_()->coerce({{"lower", true}});
lower->parse("HELLO");  // => "hello"

auto upper = string_()->coerce({{"upper", true}});
upper->parse("hello");  // => "HELLO"

Transformations can be combined:

auto normalized = string_()->coerce({{"trim", true}, {"lower", true}});
normalized->parse("  Hello World  ");  // => "hello world"

Defaults

Defaults fill in missing (absent) values. They run after coercion and before validation. Call ->default_(value) on any schema, passing a nlohmann::json value.

The default only applies when the value is absent -- if a value is present, it is validated normally. Defaults must be static values (for portability across SDKs).

auto role = string_()->default_("user");
role->parse(nullptr);   // => "user" (absent value filled)
role->parse("admin");   // => "admin"

auto tags = array(string_())->default_(json::array());

Defaults work with optional fields in objects:

auto config = object()
    ->prop("theme", optional_(string_()->default_("light")))
    ->prop("language", optional_(string_()->default_("en")));

config->parse(json::object());
// => {"theme": "light", "language": "en"}

If the default value itself fails validation, a default_invalid issue is produced.

Parsing

Throwing Parse

parse() returns the validated and coerced value as nlohmann::json. If validation fails, it throws anyvali::ValidationError:

#include <anyvali/anyvali.hpp>
using namespace anyvali;

auto schema = string_()->min_length(1);

try {
    json value = schema->parse("hello");
    // value == "hello"
} catch (const ValidationError& e) {
    std::cerr << e.what() << "\n";
    for (const auto& issue : e.issues()) {
        std::cerr << "[" << issue.code << "] " << issue.message << "\n";
    }
}

Safe Parse

safe_parse() never throws. It returns a ParseResult with structured success/failure information:

auto schema = object()
    ->prop("name", string_()->min_length(1))
    ->prop("email", string_()->format("email"))
    ->required({"name", "email"});

json input = {{"name", ""}, {"email", "not-an-email"}};
auto result = schema->safe_parse(input);

if (result.success) {
    // result.value contains the parsed data
    json user = result.value;
} else {
    // result.issues contains all validation errors
    for (const auto& issue : result.issues) {
        std::cout << issue.path_string() << ": ["
                  << issue.code << "] " << issue.message << "\n";
        // "name: [too_small] String must have at least 1 character"
        // "email: [invalid_format] Invalid email format"
    }
}

Typed Parse Helpers

The C++ SDK provides template helpers that parse and cast the result to a native C++ type:

#include <anyvali/anyvali.hpp>
using namespace anyvali;

auto name_schema = string_()->min_length(1);

// Throwing version: returns T directly
std::string name = parse_as<std::string>(name_schema, "Alice");

auto age_schema = int_()->min(0)->max(150);
int64_t age = parse_as<int64_t>(age_schema, 25);

// Safe version: returns TypedParseResult<T>
auto result = safe_parse_as<std::string>(name_schema, "");
if (result.success) {
    std::string value = result.value;   // typed as std::string
} else {
    for (const auto& issue : result.issues) {
        std::cerr << issue.message << "\n";
    }
}

TypedParseResult<T> mirrors ParseResult but carries a typed value:

Field Type Description
success bool Whether parsing succeeded
value T The parsed value (only meaningful when success is true)
issues std::vector<ValidationIssue> Validation issues (empty when success is true)

Issue Structure

Each ValidationIssue contains:

Field Type Description
code std::string Machine-readable error code (e.g., invalid_type, too_small)
message std::string Human-readable error description
path std::vector<PathSegment> Path to the invalid value
expected std::string What was expected (e.g., string, int64)
received std::string What was received (e.g., null, bool)

The path_string() method on ValidationIssue returns a dot-separated string representation of the path (e.g., "users.0.email").

Export and Import

Exporting Schemas

Any schema can be exported to AnyVali's portable JSON format:

auto schema = object()
    ->prop("id", int64())
    ->prop("name", string_()->min_length(1)->max_length(100))
    ->required({"id", "name"});

// Export to portable JSON
json doc = export_schema(schema, ExportMode::Portable);

std::cout << doc.dump(2) << "\n";
// {
//   "anyvaliVersion": "1.0",
//   "schemaVersion": "1",
//   "root": {
//     "kind": "object",
//     "properties": {
//       "id": { "kind": "int64" },
//       "name": { "kind": "string", "minLength": 1, "maxLength": 100 }
//     },
//     "required": ["id", "name"],
//     "unknownKeys": "reject"
//   },
//   "definitions": {},
//   "extensions": {}
// }

Export modes:

  • ExportMode::Portable -- fails if the schema uses non-portable features. This is the safe default.
  • ExportMode::Extended -- emits the core schema plus language-specific extension namespaces.

Importing Schemas

Schemas can be imported from a JSON document produced by any AnyVali SDK:

// Import from a portable JSON document
auto imported = import_schema(doc);

// The imported schema works exactly like a locally-built one
json user = imported->parse({{"id", 1}, {"name", "Bob"}});

This enables cross-language schema sharing. A schema authored in TypeScript, Python, or any other supported language can be exported, stored, and imported back into C++.

Practical Examples

REST API Request Validation

#include <anyvali/anyvali.hpp>
#include <iostream>

using namespace anyvali;

auto create_order_schema() {
    return object()
        ->prop("customer_id", int64())
        ->prop("items", array(
            object()
                ->prop("product_id", int64())
                ->prop("quantity", uint16()->min(1))
                ->prop("unit_price", float64()->min(0)->multiple_of(0.01))
                ->required({"product_id", "quantity", "unit_price"})
        )->min_items(1))
        ->prop("notes", optional_(nullable(string_()->max_length(1000))))
        ->required({"customer_id", "items"});
}

void handle_create_order(const json& body) {
    static auto schema = create_order_schema();

    auto result = schema->safe_parse(body);
    if (!result.success) {
        json errors = json::array();
        for (const auto& issue : result.issues) {
            errors.push_back({
                {"field", issue.path_string()},
                {"message", issue.message},
            });
        }
        // respond with 400 and errors
        std::cout << json({{"errors", errors}}).dump(2) << "\n";
        return;
    }

    json order = result.value;
    // proceed with validated data
}

Configuration File Validation

#include <anyvali/anyvali.hpp>
#include <fstream>

using namespace anyvali;

auto config_schema() {
    return object()
        ->prop("database", object()
            ->prop("host", string_())
            ->prop("port", uint16()->min(1)->max(65535))
            ->prop("name", string_()->min_length(1))
            ->prop("user", string_())
            ->prop("password", string_())
            ->required({"host", "port", "name", "user", "password"})
        )
        ->prop("cache", object()
            ->prop("driver", enum_({"redis", "memcached", "file"}))
            ->prop("ttl", int_()->min(0))
            ->required({"driver", "ttl"})
        )
        ->prop("debug", optional_(bool_()))
        ->required({"database", "cache"});
}

int main() {
    std::ifstream file("config.json");
    json raw;
    file >> raw;

    static auto schema = config_schema();

    try {
        json config = schema->parse(raw);
        std::string db_host = config["database"]["host"];
        uint16_t db_port = config["database"]["port"];
        // use validated config
    } catch (const ValidationError& e) {
        std::cerr << "Invalid configuration:\n";
        for (const auto& issue : e.issues()) {
            std::cerr << "  " << issue.path_string()
                      << ": " << issue.message << "\n";
        }
        return 1;
    }

    return 0;
}

Message Protocol Validation

#include <anyvali/anyvali.hpp>

using namespace anyvali;

// Define message types for a protocol
auto text_message = object()
    ->prop("type", literal("text"))
    ->prop("content", string_()->min_length(1)->max_length(4096))
    ->prop("sender_id", int64())
    ->required({"type", "content", "sender_id"});

auto image_message = object()
    ->prop("type", literal("image"))
    ->prop("url", string_()->format("uri"))
    ->prop("width", uint32()->min(1))
    ->prop("height", uint32()->min(1))
    ->prop("sender_id", int64())
    ->required({"type", "url", "width", "height", "sender_id"});

auto message_schema = union_({text_message, image_message});

void process_message(const json& raw) {
    auto result = message_schema->safe_parse(raw);
    if (!result.success) {
        // handle invalid message
        return;
    }

    json msg = result.value;
    std::string type = msg["type"];
    if (type == "text") {
        std::string content = msg["content"];
        // handle text message
    } else if (type == "image") {
        std::string url = msg["url"];
        // handle image message
    }
}

Cross-Language Schema Sharing

#include <anyvali/anyvali.hpp>
#include <fstream>

using namespace anyvali;

int main() {
    // Import a schema produced by another SDK (e.g., Python, TypeScript)
    std::ifstream file("schemas/user.anyvali.json");
    json doc;
    file >> doc;
    auto user_schema = import_schema(doc);

    // Validate data against the imported schema
    json input = {{"id", 1}, {"name", "Bob"}, {"email", "bob@test.com"}};
    json user = user_schema->parse(input);

    // Build a local schema and export it for other services
    auto audit_event = object()
        ->prop("action", enum_({"create", "update", "delete"}))
        ->prop("actor_id", int64())
        ->prop("timestamp", string_()->format("date-time"))
        ->prop("payload", record(any()))
        ->required({"action", "actor_id", "timestamp"});

    json exported = export_schema(audit_event, ExportMode::Portable);

    std::ofstream out("schemas/audit_event.anyvali.json");
    out << exported.dump(2);

    return 0;
}

Typed Parsing with Structs

#include <anyvali/anyvali.hpp>

using namespace anyvali;

struct User {
    int64_t id;
    std::string name;
    std::string email;
};

// nlohmann::json conversion (standard nlohmann pattern)
void from_json(const json& j, User& u) {
    j.at("id").get_to(u.id);
    j.at("name").get_to(u.name);
    j.at("email").get_to(u.email);
}

int main() {
    auto schema = object()
        ->prop("id", int64())
        ->prop("name", string_()->min_length(1))
        ->prop("email", string_()->format("email"))
        ->required({"id", "name", "email"});

    json input = {{"id", 42}, {"name", "Alice"}, {"email", "alice@example.com"}};

    // parse_as<T> validates then converts to your struct
    User user = parse_as<User>(schema, input);
    // user.id == 42, user.name == "Alice", user.email == "alice@example.com"

    // Safe version
    auto result = safe_parse_as<User>(schema, input);
    if (result.success) {
        User u = result.value;
        // use typed struct
    }

    return 0;
}

Common Patterns

Validating Configuration Files

Use UnknownKeyMode::Strip when parsing JSON objects that contain many extra keys you don't care about, like configuration files with additional entries:

auto env_schema = anyvali::object()
    ->prop("DATABASE_URL", anyvali::string_())
    ->required({"DATABASE_URL"})
    ->unknown_keys(anyvali::UnknownKeyMode::Strip);

Without Strip, parse would fail with unknown_key issues for every extra key because the default mode is Reject.

Mode What happens with extra keys
Reject (default) Parse fails with unknown_key issues
Strip Extra keys silently removed from output
Allow Extra keys passed through to output

Eagerly Evaluated vs Lazy Defaults

->default_() accepts any nlohmann::json value. Expressions like std::filesystem::current_path() are evaluated immediately when the schema is created and stored as a static value -- this works fine. What AnyVali does not support is lazy std::function defaults that re-evaluate on each parse call. If you need a fresh value on every parse, apply it after:

auto config_schema = object()
    ->prop("profile", optional_(string_()->default_("default")))
    ->prop("appDir", optional_(string_()))
    ->unknown_keys(UnknownKeyMode::Strip);

json config = config_schema->parse(input);
if (!config.contains("appDir") || config["appDir"].is_null()) {
    config["appDir"] = std::filesystem::current_path().string();
}

This keeps the schema fully portable -- the same JSON document can be imported in Go, Python, or any other SDK without relying on language-specific function calls.

API Reference

Builder Functions (anyvali namespace)

Function Returns Description
string_() shared_ptr<StringSchema> String schema
number() shared_ptr<NumberSchema> IEEE 754 float64 schema
int_() shared_ptr<IntSchema> Signed int64 schema
int8() shared_ptr<IntSchema> Signed 8-bit integer
int16() shared_ptr<IntSchema> Signed 16-bit integer
int32() shared_ptr<IntSchema> Signed 32-bit integer
int64() shared_ptr<IntSchema> Signed 64-bit integer
uint8() shared_ptr<IntSchema> Unsigned 8-bit integer
uint16() shared_ptr<IntSchema> Unsigned 16-bit integer
uint32() shared_ptr<IntSchema> Unsigned 32-bit integer
uint64() shared_ptr<IntSchema> Unsigned 64-bit integer
float32() shared_ptr<NumberSchema> 32-bit float
float64() shared_ptr<NumberSchema> 64-bit float (same as number())
bool_() shared_ptr<BoolSchema> Boolean schema
null() shared_ptr<NullSchema> Null schema
any() shared_ptr<AnySchema> Accepts any value
unknown() shared_ptr<UnknownSchema> Accepts any value (explicit handling)
never() shared_ptr<NeverSchema> Rejects all values
literal(value) shared_ptr<LiteralSchema> Matches a single exact value
enum_(values) shared_ptr<EnumSchema> Matches one of the given values
array(items) shared_ptr<ArraySchema> Homogeneous array
tuple(elements) shared_ptr<TupleSchema> Fixed-position typed array
object() shared_ptr<ObjectSchema> Object schema (use builder to add properties)
record(values) shared_ptr<RecordSchema> String-keyed map with uniform value type
union_(variants) shared_ptr<UnionSchema> Value must match at least one variant
intersection(schemas) shared_ptr<IntersectionSchema> Value must match all schemas
optional_(inner) shared_ptr<OptionalSchema> Value may be absent
nullable(inner) shared_ptr<NullableSchema> Value may be null
ref(ref_path) shared_ptr<RefSchema> Reference to a definition

Free Functions

Function Returns Description
export_schema(schema, mode) nlohmann::json Export schema to portable JSON document
import_schema(doc) shared_ptr<Schema> Import schema from portable JSON document
parse_as<T>(schema, input) T Parse, validate, and convert to type T; throws on failure
safe_parse_as<T>(schema, input) TypedParseResult<T> Parse, validate, and convert to T; never throws

Schema Instance Methods

Method Returns Description
schema->parse(input) nlohmann::json Parse and validate; throws ValidationError on failure
schema->safe_parse(input) ParseResult Parse and validate; never throws

ObjectSchema Builder Methods

Method Returns Description
->prop(name, schema) shared_ptr<ObjectSchema> Add a property
->required(names) shared_ptr<ObjectSchema> Set required property names
->unknown_keys(mode) shared_ptr<ObjectSchema> Set unknown key handling mode

StringSchema Constraints

Method Parameter Description
->min_length(n) size_t Minimum character count
->max_length(n) size_t Maximum character count
->pattern(p) std::string Regex pattern the value must match
->starts_with(s) std::string Required prefix
->ends_with(s) std::string Required suffix
->includes(s) std::string Required substring
->format(f) std::string Named format (email, uri, uuid, date, date-time, etc.)

NumberSchema Constraints

Method Parameter Description
->min(n) double Minimum value (inclusive)
->max(n) double Maximum value (inclusive)
->exclusive_min(n) double Minimum value (exclusive)
->exclusive_max(n) double Maximum value (exclusive)
->multiple_of(n) double Value must be a multiple of this

ArraySchema Constraints

Method Parameter Description
->min_items(n) size_t Minimum number of elements
->max_items(n) size_t Maximum number of elements

UnknownKeyMode (Enum)

Value Description
UnknownKeyMode::Reject Error on unexpected keys (default)
UnknownKeyMode::Strip Silently remove unexpected keys
UnknownKeyMode::Allow Keep unexpected keys as-is

ExportMode (Enum)

Value Description
ExportMode::Portable Fail on non-portable features (safe default)
ExportMode::Extended Include language-specific extensions

ParseResult

Field Type Description
success bool Whether parsing succeeded
value nlohmann::json The parsed value (only meaningful when success is true)
issues std::vector<ValidationIssue> Validation issues (empty when success is true)

TypedParseResult\<T>

Field Type Description
success bool Whether parsing succeeded
value T The parsed value (only meaningful when success is true)
issues std::vector<ValidationIssue> Validation issues (empty when success is true)

ValidationIssue

Field Type Description
code std::string Machine-readable error code (e.g., invalid_type, too_small)
message std::string Human-readable error description
path std::vector<PathSegment> Path to the invalid value
expected std::string What was expected (e.g., string, int64)
received std::string What was received (e.g., null, bool)
path_string() std::string Dot-separated path (e.g., "users.0.email")

ValidationError

Inherits from std::runtime_error. Thrown by parse() and parse_as<T>() on validation failure.

Method Returns Description
what() const char* Summary error message
issues() const std::vector<ValidationIssue>& List of validation issues