Schemas ​
Schemas in zigantic allow you to define complex data structures with validation rules. This is similar to Pydantic models in Python.
Defining a Schema ​
A schema in zigantic is simply a Zig struct with validated fields:
zig
const z = @import("zigantic");
const UserSchema = struct {
// Required fields with validation
id: z.PositiveInt(u32),
name: z.String(1, 100),
email: z.Email,
// Optional fields
bio: ?z.String(0, 500) = null,
website: ?z.Url = null,
// Fields with defaults
role: z.Default([]const u8, "user"),
active: z.Default(bool, true),
};Parsing JSON into a Schema ​
zig
const std = @import("std");
const z = @import("zigantic");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const json =
\\{
\\ "id": 123,
\\ "name": "Alice Johnson",
\\ "email": "alice@example.com",
\\ "bio": "Software developer",
\\ "role": "admin"
\\}
;
var result = try z.fromJson(UserSchema, json, allocator);
defer result.deinit();
if (result.value) |user| {
std.debug.print("User: {s} ({s})\n", .{user.name.get(), user.email.get()});
std.debug.print("Role: {s}\n", .{user.role.get()});
}
}Nested Schemas ​
Schemas can be nested for complex data structures:
zig
const Address = struct {
street: z.String(1, 200),
city: z.String(1, 100),
country: z.String(2, 100),
postal_code: z.String(1, 20),
};
const Company = struct {
name: z.String(1, 100),
website: z.HttpsUrl,
employees: z.PositiveInt(u32),
};
const Profile = struct {
user: UserSchema,
address: Address,
company: ?Company = null,
};Schema with Collections ​
Use validated collections in your schemas:
zig
const BlogPost = struct {
title: z.String(1, 200),
content: z.String(1, 50000),
author: z.Email,
tags: z.List([]const u8, 1, 10), // 1-10 tags
published: z.Default(bool, false),
};Schema Validation ​
All fields are validated when parsing:
zig
const json =
\\{
\\ "title": "",
\\ "content": "Hello",
\\ "author": "invalid-email",
\\ "tags": []
\\}
;
var result = try z.fromJson(BlogPost, json, allocator);
defer result.deinit();
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,
});
}
// Output:
// [E001] title: value is too short
// [E010] author: must be a valid email
// [E030] tags: too few items
}Serializing Schemas ​
Convert schemas back to JSON:
zig
const post = BlogPost{
.title = try z.String(1, 200).init("My First Post"),
.content = try z.String(1, 50000).init("Hello, world!"),
.author = try z.Email.init("author@example.com"),
.tags = try z.List([]const u8, 1, 10).init(&.{"zig", "tutorial"}),
.published = z.Default(bool, false).init(),
};
const json_str = try z.toJsonPretty(post, allocator);
defer allocator.free(json_str);
std.debug.print("{s}\n", .{json_str});Best Practices ​
- Use meaningful type aliases - Create type aliases for reusable validation types
- Group related fields - Use nested structs for logical grouping
- Set sensible defaults - Use
Defaulttype for optional configuration - Document constraints - Add comments explaining validation rules
- Handle errors gracefully - Always check
result.isValid()before using values
zig
// Good: Meaningful type aliases
const Username = z.String(3, 30);
const Bio = z.String(0, 500);
const Age = z.Int(i32, 13, 120);
const UserProfile = struct {
username: Username,
bio: ?Bio = null,
age: Age,
};