zigantic

logo Documentation Zig Version GitHub stars GitHub issues GitHub pull requests GitHub last commit License CI Supported Platforms Latest Release Sponsor GitHub Sponsors Repo Visitors

Type-safe data validation and JSON serialization for Zig with compile-time guarantees.

Documentation | API Reference | Quick Start | Contributing

zigantic is a data validation library for Zig, using the type system for compile-time guarantees. Define validation rules as types, parse JSON with automatic error handling, and serialize with zero runtime overhead for unused features.

Features

Feature Description Docs
Compile-Time Driven Validation logic is types. Constraints are checked at compile time. Philosophy
Idiomatic Zig No macros, no DSLs, no magic. Just types and functions. Getting Started
Human-Readable Errors Field-aware messages with error codes (E001, E010, etc.) Error Handling
Zero Overhead Unused features have zero runtime cost. Benchmarks
60+ Built-in Types Strings, numbers, formats, dates, geo, crypto, and collections. Types API
JSON Serialization Parse and serialize JSON with automatic validation. JSON API
Custom Validators Define custom validation functions and transformations. Validators
Custom Messages Override error messages per-type with comptime config. Error Handling
Lifecycle Callbacks Hooks for validation and serialization lifecycle events. Callbacks
Color Overrides Customize terminal colors per validation error type. Error Handling
Schemas Define complex data structures with nested validation. Schemas
Auto Updates Automatic version checking (can be disabled). Version & Updates

Installation

Install the latest stable release for zig 0.16+ (use v0.0.3 or newer):

zig fetch --save https://github.com/muhammad-fiaz/zigantic/archive/refs/tags/0.0.3.tar.gz

Nightly Installation

Install the latest development version:

zig fetch --save git+https://github.com/muhammad-fiaz/zigantic

Configure build.zig

Then in your build.zig:

const zigantic_dep = b.dependency("zigantic", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("zigantic", zigantic_dep.module("zigantic"));

Quick Start

Direct Validation

const std = @import("std");
const z = @import("zigantic");

pub fn main() !void {
    // String with length constraints
    const name = try z.String(1, 50).init("Alice");
    std.debug.print("Name: {s} (len: {d})\n", .{name.get(), name.len()});

    // Email with domain parsing
    const email = try z.Email.init("alice@company.com");
    std.debug.print("Email: {s}\n", .{email.get()});
    std.debug.print("Domain: {s}\n", .{email.domain()});
    std.debug.print("Business email: {}\n", .{email.isBusinessEmail()});

    // Password with strength checking
    const pwd = try z.Secret(8, 100).init("MyP@ssw0rd!");
    std.debug.print("Password: {s}\n", .{pwd.masked()});
    std.debug.print("Strength: {d}/6\n", .{pwd.strength()});

    // Integer with range and utilities
    const age = try z.Int(i32, 18, 120).init(25);
    std.debug.print("Age: {d} (even: {}, positive: {})\n", .{
        age.get(), age.isEven(), age.isPositive()
    });

    // IP address with network utilities
    const ip = try z.Ipv4.init("192.168.1.1");
    std.debug.print("IP: {s} (private: {})\n", .{ip.get(), ip.isPrivate()});
}

Note: zigantic automatically checks for updates when using JSON functions. To disable, call z.disableUpdateCheck() at the start of your program.

Custom validation messages can be set per-type via the f suffix variants:

const Name = z.Stringf(3, 50, .{ .too_short = "name must be at least 3 chars" });
const err = Name.init("Jo") catch |e| e;
std.debug.print("{s}\n", .{Name.messageFor(err).?});

Or globally via the message formatter in Config:

JSON Parsing with Validation

const std = @import("std");
const z = @import("zigantic");

pub fn main() !void {
    var gpa = std.heap.DebugAllocator(.{}).init;
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Define a validated struct
    const User = struct {
        id: z.PositiveInt(u32),
        name: z.String(1, 50),
        age: z.Int(i32, 18, 120),
        email: z.Email,
        role: z.Default([]const u8, "user"),
        website: ?z.Url = null,
    };

    const json =
        \\{"id": 1, "name": "Alice", "age": 25, "email": "alice@example.com"}
    ;

    var result = try z.fromJson(User, json, allocator);
    defer result.deinit();

    if (result.value) |user| {
        std.debug.print("Welcome, {s}!\n", .{user.name.get()});
        std.debug.print("Role: {s} (default)\n", .{user.role.get()});
    }

    if (!result.isValid()) {
        for (result.error_list.errors.items) |err| {
            std.debug.print("[{s}] {s}: {s}\n", .{
                z.errorCode(err.error_type),
                err.field,
                err.message,
            });
        }
    }
}

URL Query Parameters & Form URL-Encoded Parsing

const std = @import("std");
const z = @import("zigantic");

pub fn main() !void {
    var gpa = std.heap.DebugAllocator(.{}).init;
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const SearchQuery = struct {
        query: z.String(1, 100),
        page: z.Default(u32, 1),
        active_only: bool,
    };

    const qs = "query=Mechanical+Keyboard&page=2&active_only=true";
    var result = try z.fromQueryString(SearchQuery, qs, allocator);
    defer result.deinit();

    if (result.isValid()) {
        const q = result.value.?;
        std.debug.print("Query: {s}, Page: {d}\n", .{q.query.get(), q.page.get()});
        
        // Serialize back to query string!
        const serialized = try z.toQueryString(q, allocator);
        defer allocator.free(serialized);
        std.debug.print("Serialized query string: {s}\n", .{serialized});
    }
}

Compile-Time Field Aliases & Naming Policies

Map custom aliases or use automatic naming policies (like snake_case or camelCase) completely at compile time with zero runtime cost.

const std = @import("std");
const z = @import("zigantic");

const User = struct {
    firstName: []const u8,
    lastName: []const u8,

    // Automatically convert camelCase struct fields to snake_case in JSON/Query strings
    pub const zigantic_naming = z.utils.NamingPolicy.snake_case;
    
    // Explicit field aliases (overrides naming policies)
    pub const zigantic_aliases = .{
        .firstName = "first",
    };
};

Advanced Features

zigantic supports high-value features including dynamic default factories, field-level validation, and model-level cross-field validation.

Dynamic Default Factories (DefaultFactory)

Use DefaultFactory when default values need to be dynamically generated at instantiation/parsing time (e.g. unique IDs or dynamic timestamps).

const std = @import("std");
const z = @import("zigantic");

var call_counter: i32 = 0;
fn nextId() i32 {
    call_counter += 1;
    return call_counter;
}

const Device = struct {
    name: []const u8,
    id: z.DefaultFactory(i32, nextId),
};

Field-Level Validators (validate_[field_name])

Structs can define field-level validator methods to run custom validation or coercion/normalization logic for specific fields. A field validator receives the parsed field value and returns the final value (or an error).

const User = struct {
    username: z.String(3, 50),
    age: i32,

    // Runs after basic parsing succeeds for 'age'
    pub fn validate_age(val: i32) !i32 {
        if (val < 18) return error.AgeTooYoung;
        // Cap age at 100 as a coercion/normalization
        if (val > 100) return 100;
        return val;
    }
};

Model-Level Validation (validateModel)

Structs can define a validateModel method to perform cross-field validation after all individual fields have successfully parsed and validated.

const Order = struct {
    item: []const u8,
    quantity: i32,
    discount_code: ?[]const u8,

    pub fn validateModel(self: *const @This()) !void {
        if (self.discount_code != null and self.quantity < 5) {
            return error.DiscountRequiresMinimumQuantity;
        }
    }
};

All Types

String Types (9)

Type Description Example
String(min, max) Length-constrained string String(1, 50)
NonEmptyString(max) Non-empty string NonEmptyString(100)
Trimmed(min, max) Auto-trim whitespace Trimmed(1, 50)
Lowercase(max) Lowercase only Lowercase(50)
Uppercase(max) Uppercase only Uppercase(50)
Alphanumeric(min, max) Letters and digits Alphanumeric(1, 20)
AsciiString(min, max) ASCII only (0-127) AsciiString(1, 100)
Secret(min, max) Password with strength Secret(8, 100)
StrongPassword(min, max) Requires upper+lower+digit+special StrongPassword(8, 100)

String Methods:

str.get()           // Get value
str.len()           // Length
str.isEmpty()       // Check empty
str.startsWith("A") // Prefix check
str.endsWith("z")   // Suffix check
str.contains("bc")  // Contains check
str.charAt(0)       // Character at index
str.slice(0, 5)     // Substring

// Secret-specific
pwd.masked()        // "********"
pwd.strength()      // 0-6 score
pwd.hasUppercase()  // bool
pwd.hasLowercase()  // bool
pwd.hasDigit()      // bool
pwd.hasSpecial()    // bool

Number Types (14)

Type Description Example
Int(T, min, max) Signed integer range Int(i32, 0, 100)
UInt(T, min, max) Unsigned integer range UInt(u32, 1, 1000)
PositiveInt(T) > 0 PositiveInt(i32)
NonNegativeInt(T) >= 0 NonNegativeInt(i32)
NegativeInt(T) < 0 NegativeInt(i32)
EvenInt(T, min, max) Even numbers only EvenInt(i32, 0, 100)
OddInt(T, min, max) Odd numbers only OddInt(i32, 1, 99)
MultipleOf(T, divisor) Must be multiple of N MultipleOf(i32, 5)
Float(T, min, max) Float range Float(f64, 0.0, 1.0)
Percentage(T) 0-100 Percentage(f64)
Probability(T) 0-1 Probability(f64)
PositiveFloat(T) > 0 PositiveFloat(f64)
NegativeFloat(T) < 0 NegativeFloat(f64)
FiniteFloat(T) No NaN/Infinity FiniteFloat(f64)

Number Methods:

n.get()          // Get value
n.isPositive()   // > 0
n.isNegative()   // < 0
n.isZero()       // == 0
n.isEven()       // Even check
n.isOdd()        // Odd check
n.abs()          // Absolute value
n.clamp(0, 50)   // Clamp to range

// Float-specific
f.floor()        // Floor
f.ceil()         // Ceiling
f.round()        // Round
f.trunc()        // Truncate

Format Types (11)

Type Description Methods
Email Email address domain(), localPart(), isBusinessEmail()
Url HTTP/HTTPS URL isHttps(), protocol(), host()
HttpsUrl HTTPS only -
Uuid UUID format version()
Ipv4 IPv4 address isPrivate(), isLoopback()
Ipv6 IPv6 address isLoopback()
Slug URL slug -
Semver Semantic version -
PhoneNumber Phone number hasCountryCode()
CreditCard Credit card (Luhn) cardType(), masked()
Regex(pattern) Pattern matching -

Collection Types (3)

Type Description Methods
List(T, min, max) List with length len(), first(), last(), at(i)
NonEmptyList(T, max) Non-empty list Same as List
FixedList(T, len) Exact size at(i)

Special Types (11)

Type Description Methods
Default(T, value) Default value isDefault(), getOrDefault()
DefaultFactory(T, fn) Dynamic default initDefault(), getOrDefault()
Custom(T, fn) Custom validator -
Transform(T, fn) Transform value getOriginal()
Coerce(From, To) Type conversion -
Literal(T, value) Exact value match -
Partial(T) All fields optional -
OneOf(T, values) Allowed values isFirst(), isLast()
Range(T, s, e, step) Range with step -
Nullable(T) Explicit null isNull(), unwrapOr()
Lazy(T) Lazy evaluation isComputed(), reset()

Validators

Direct validation functions without types:

const v = z.validators;

// Format validators
v.isValidEmail("user@example.com")     // true
v.isValidUrl("https://example.com")    // true
v.isUuid("550e8400-...")               // true
v.isIpv4("192.168.1.1")                // true
v.isIpv6("::1")                        // true
v.isSlug("hello-world")                // true
v.isSemver("1.2.3")                    // true
v.isPhoneNumber("+1234567890")         // true
v.isJwt("header.payload.signature")    // true
v.isValidCreditCard("4111...")         // true

// String validators
v.isAlphanumeric("abc123")             // true
v.isAlpha("hello")                     // true
v.isNumeric("12345")                   // true
v.isLowercase("hello")                 // true
v.isUppercase("HELLO")                 // true
v.isHexString("0123abcdef")            // true

// Pattern matching
v.matchesPattern("[0-9][0-9][0-9]", "123")  // true

Error Handling

// Error messages and codes
if (z.String(3, 50).init("Jo")) |_| {} else |err| {
    z.errorMessage(err)  // "value is too short"
    z.errorCode(err)     // "E001"
}

// ErrorList for collecting multiple errors
var errors = z.errors.ErrorList.init(allocator);
defer errors.deinit();

try errors.add("name", error.TooShort, "too short", "Jo");
errors.count()           // 1
errors.containsField("name")  // true

// JSON output
const json = try errors.toJsonArray(allocator);
// [{"field":"name","message":"too short","value":"Jo"}]

Custom Error Messages

Override error messages per-type via the comptime messages parameter:

const Name = z.Stringf(3, 50, .{ .too_short = "name is required" });
const Age = z.Intf(i32, 18, 120, .{ .too_small = "must be 18+" });
const Pwd = z.StrongPasswordf(8, 100, .{
    .too_short = "password too short",
    .weak_password = "needs upper, lower, digit, special",
});

The messageFor(err) method returns the custom message for the given error, or null if no override was set.

For global message formatting (works with all types including Email, Url, etc.), use the config formatter:

var cfg = z.getConfig();
cfg.validation_message_formatter = struct {
    fn f(err: z.errors.ValidationError) []const u8 {
        return switch (err) {
            error.InvalidEmail => "please enter a valid email address",
            else => z.errorMessage(err),
        };
    }
}.f;
z.setConfig(cfg);

Lifecycle Callbacks

Register callbacks for validation and serialization lifecycle events:

var cfg = z.getConfig();
cfg.before_validation_callback = struct {
    fn call(type_name: []const u8) void {
        std.debug.print("Validating: {s}\n", .{type_name});
    }
}.call;
cfg.on_field_validated_callback = struct {
    fn call(field: []const u8, field_type: []const u8, success: bool) void { }
}.call;
cfg.on_field_error_callback = struct {
    fn call(field: []const u8, msg: []const u8) void { }
}.call;
cfg.on_validation_complete_callback = struct {
    fn call(valid: bool, error_count: usize) void { }
}.call;
cfg.before_serialize_callback = struct {
    fn call() void { }
}.call;
cfg.after_serialize_callback = struct {
    fn call(result: []const u8) void { }
}.call;
z.setConfig(cfg);

Error Codes

Code Error Message
E001 TooShort value is too short
E002 TooLong value is too long
E003 TooSmall value is too small
E004 TooLarge value is too large
E010 InvalidEmail must be a valid email
E011 InvalidUrl must be a valid URL
E020 MissingField field is required
E021 TypeMismatch wrong type
E099 CustomValidationFailed validation failed

Examples

The library includes 8 comprehensive examples:

zig build run-basic               # Direct validation + JSON
zig build run-advanced_types      # All 50+ types demo
zig build run-validators          # Validator functions
zig build run-json_example        # Full JSON workflow
zig build run-error_handling      # Error management
zig build run-naming_conventions  # Compile-time Casing conventions and explicit Aliases
zig build run-custom_messages     # Custom validation messages
zig build run-callbacks           # Lifecycle callbacks

Building

zig build            # Build library
zig build test       # Run 148+ tests
zig build example    # Run basic example

License

This project is licensed under the MIT License - see the LICENSE file for details.