Extra Models
Use separate Zig structs for request input and response output to control exactly which fields are accepted and which are exposed.
Overview
Real-world APIs rarely use the same shape for input and output. A user creation endpoint might accept a username and email but return those fields plus a server-generated ID. By defining distinct structs -- one for the request body, one for the response -- you get compile-time guarantees that sensitive or internal fields never leak, and your OpenAPI documentation accurately describes both directions.
Zigmund supports this through its Body parameter wrapper for input and the .response_model route option for output. The framework generates separate OpenAPI schemas for each struct, so consumers see clear request and response contracts.
Example
const std = @import("std");
const zigmund = @import("zigmund");
const UserIn = struct {
username: []const u8,
email: []const u8,
full_name: ?[]const u8 = null,
};
const UserOut = struct {
username: []const u8,
email: []const u8,
full_name: ?[]const u8 = null,
id: u32,
};
fn createUser(
body: zigmund.Body(UserIn, .{}),
allocator: std.mem.Allocator,
) !zigmund.Response {
const user_in = body.value.?;
return zigmund.Response.json(allocator, UserOut{
.username = user_in.username,
.email = user_in.email,
.full_name = user_in.full_name,
.id = 1001,
});
}
fn getUser(
user_id: zigmund.Path(u32, .{ .alias = "user_id" }),
allocator: std.mem.Allocator,
) !zigmund.Response {
return zigmund.Response.json(allocator, UserOut{
.username = "alice",
.email = "alice@example.com",
.full_name = "Alice Wonderland",
.id = user_id.value.?,
});
}
pub fn buildExample(app: *zigmund.App) !void {
try app.post("/tutorial/extra-models/users", createUser, .{
.summary = "Create user with separate input/output models",
.tags = &.{ "parity", "tutorial" },
.operation_id = "tutorial_extra_models_create_user",
.response_model = UserOut,
});
try app.get("/tutorial/extra-models/users/{user_id}", getUser, .{
.summary = "Get user by ID with typed response model",
.tags = &.{ "parity", "tutorial" },
.operation_id = "tutorial_extra_models_get_user",
.response_model = UserOut,
});
}
How It Works
-
Define the input model.
UserIndescribes what the client sends:username,email, and an optionalfull_name. There is noidfield because the server assigns it. -
Define the output model.
UserOutincludes every field the client should see, including the server-generatedid. If your application stored a password hash, that field would exist only in an internal model, never inUserOut. -
Accept the input.
zigmund.Body(UserIn, .{})parses the JSON request body intoUserIn. The framework validates that required fields are present and types are correct. -
Map input to output. The
createUserhandler reads the validatedUserIn, constructs aUserOutwith the same fields plus an assignedid, and returns it as JSON. -
Declare the response model.
.response_model = UserOutin the route options tells Zigmund to useUserOutas the schema for the200response in the generated OpenAPI document. This gives documentation tools the exact shape of the response payload. -
Reuse models across endpoints. Both the
POSTandGETroutes specify.response_model = UserOut, sharing the same response contract.
Key Points
- Separate input and output models prevent accidental exposure of internal fields (password hashes, internal flags, audit timestamps).
- The
.response_modeloption is purely for OpenAPI documentation and does not perform runtime filtering. Your handler is responsible for returning the correct struct. - You can define as many model variants as needed:
UserIn,UserOut,UserDB,UserPublic, etc. - Optional fields (
?[]const u8 = null) appear in the OpenAPI schema as non-required properties with nullable types. - When multiple endpoints share a response model, changes to the struct automatically update the OpenAPI schema for all of them.
See Also
- Request Body -- Basics of parsing JSON request bodies into structs.
- Response Model -- Filtering response fields using response models.
- Path Operation Configuration -- Route-level options including
.response_model. - Schema Examples -- Add example payloads to your input and output schemas.