A CLI tool and Zig library that generates type-safe API client code from OpenAPI specifications.
Note: This project provides both a CLI tool for generating Zig code from OpenAPI specs and a library for parsing and working with OpenAPI documents programmatically in Zig.
This tool supports the following OpenAPI and Swagger specifications:
- Swagger v2.0 - Full support
- OpenAPI v3.0 - Full support
- OpenAPI v3.1 - Full support
- OpenAPI v3.2 - Full support
All specifications are supported in JSON and YAML format.
- Parse and generate from Swagger v2.0, OpenAPI v3.0, v3.1, and v3.2 specifications
- Generate type-safe Zig client code
- Support for complex OpenAPI schemas and operations
- Cross-platform support (Linux, macOS, Windows)
- Available as both CLI tool and Zig library
- Unified document representation for all OpenAPI and Swagger versions
- Zig v0.16.0 or newer
The fastest way to get started with development is using GitHub Codespaces, which provides a pre-configured development environment with Zig, ZLS (Zig Language Server), and all necessary VS Code extensions.
- Click the badge above or navigate to the repository on GitHub
- Click "Code" โ "Codespaces" โ "Create codespace"
- Wait for the environment to set up (2-3 minutes)
- Start coding! Everything is pre-configured.
If you prefer local development with Docker:
- Install VS Code and the Dev Containers extension
- Clone the repository and open it in VS Code
- When prompted, click "Reopen in Container"
- VS Code will build and configure the development environment automatically
Install Zig locally following the official installation guide.
Linux/macOS:
curl -fsSL https://christianhelle.com/openapi2zig/install | bashWindows (PowerShell):
irm https://christianhelle.com/openapi2zig/install.ps1 | iexThe install scripts will:
- Automatically detect your platform and architecture
- Download the latest release from GitHub
- Install the binary to an appropriate location
- Add it to your PATH (if desired)
Custom installation directory:
# Linux/macOS
INSTALL_DIR=$HOME/.local/bin curl -fsSL https://christianhelle.com/openapi2zig/install | bash
# Windows
irm https://christianhelle.com/openapi2zig/install.ps1 | iex -InstallDir "C:\Tools"Download the latest release for your platform from the GitHub Releases page:
- Linux x86_64:
openapi2zig-linux-x86_64.tar.gz - macOS x86_64:
openapi2zig-macos-x86_64.tar.gz - macOS ARM64:
openapi2zig-macos-aarch64.tar.gz - Windows x86_64:
openapi2zig-windows-x86_64.zip
Extract the archive and add the binary to your PATH.
Install the latest build for Linux from the Snap Store:
snap install --edge openapi2zigMake sure you have Zig installed (version 0.16.0 or newer).
git clone https://github.com/christianhelle/openapi2zig.git
cd openapi2zig
zig buildThe openapi2zig is available as a Docker image on Docker Hub at christianhelle/openapi2zig.
# Pull the latest image
docker pull christianhelle/openapi2zig-
Clone the repository:
git clone https://github.com/christianhelle/openapi2zig.git cd openapi2zig -
Build the project:
zig build
-
Run tests to verify everything works:
zig build test -
The compiled binary will be available in
zig-out/bin/openapi2zig
For development builds with debug information:
zig build -Doptimize=DebugTo run tests during development:
zig build testTo check code formatting:
zig fmt --check src/
zig fmt --check build.zigRun the broad smoke-test script to validate code generation against every supported sample specification:
pwsh test/smoke-tests.ps1What it does:
- Validates all eligible JSON and YAML API specs under
openapi/v2.0,openapi/v3.0,openapi/v3.1, andopenapi/v3.2. - Runs each spec through every resource-wrapper mode:
none,tags,paths, andhybrid. - Ignores the meta-schema documents under
openapi/json-schema/, which are outside the smoke-test discovery roots. - Writes generated outputs to
test/output/(gitignored), with filenames shaped like<basename>__<format>__<mode>.zigso JSON/YAML sibling fixtures do not collide. - Continues through individual failures and prints a final summary listing every failing spec/mode combination, then exits non-zero if any case failed.
- Honors a temporary denylist for known-unsupported spec/mode combinations so the PR gate can stay green while generator gaps are tracked explicitly.
In CI, the same script runs in the smoke-tests job on pull requests and main, alongside the existing zig build run-generate + zig run generated/main.zig curated sample harness. The broad smoke discovery does not require JSON/YAML twins: YAML-only roots such as openapi/v3.0/bot.paths.yaml are still included when they live under the covered version folders. When the smoke-tests job fails, test/output/ is uploaded as a workflow artifact for triage.
Build for different targets:
# Windows
zig build -Dtarget=x86_64-windows
# macOS
zig build -Dtarget=x86_64-macos
# Linux ARM64
zig build -Dtarget=aarch64-linuxopenapi2zig generate [options]The generate command reads a JSON or YAML OpenAPI/Swagger document from a local file or http/https URL, auto-detects the spec version, and writes one Zig source file containing models, runtime helpers, and API functions.
| Flag | Description |
|---|---|
-i, --input <PATH_OR_URL> |
OpenAPI/Swagger JSON or YAML spec from a file path or http/https URL. Required. |
-o, --output <path> |
Output file for the generated Zig code. Defaults to generated.zig. Parent directories are created when needed. |
--base-url <url> |
Base URL baked into the generated Client. Defaults to the server URL from the OpenAPI/Swagger document. |
--resource-wrappers <mode> |
Generate resource wrapper namespaces. Modes: none, tags, paths, hybrid. Defaults to paths. |
From a local file:
openapi2zig generate -i openapi/v3.0/petstore.json -o api.zigFrom a local YAML file:
openapi2zig generate -i openapi/v3.0/petstore.yaml -o api.zigFrom a remote URL:
openapi2zig generate -i https://petstore3.swagger.io/api/v3/openapi.json -o api.zigOverride the generated client's base URL:
openapi2zig generate -i openapi/v3.0/petstore.json -o api.zig --base-url https://petstore3.swagger.io/api/v3Disable resource wrapper namespaces and keep only flat endpoint functions:
openapi2zig generate -i openapi/v3.0/petstore.json -o api.zig --resource-wrappers noneThe build script also includes curated sample-generation targets used by the checked-in generated harness:
zig build run-generate-v2 # openapi/v2.0/petstore.json -> generated/generated_v2.zig
zig build run-generate-v2-yaml # openapi/v2.0/petstore.yaml -> generated/generated_v2_yaml.zig
zig build run-generate-v3 # openapi/v3.0/petstore.json -> generated/generated_v3.zig
zig build run-generate-v3-yaml # openapi/v3.0/petstore.yaml -> generated/generated_v3_yaml.zig
zig build run-generate-v31 # openapi/v3.1/webhook-example.json -> generated/generated_v31.zig
zig build run-generate-v31-yaml # openapi/v3.1/webhook-example.yaml -> generated/generated_v31_yaml.zig
zig build run-generate-v32 # openapi/v3.2/petstore.json -> generated/generated_v32.zig
zig build run-generate # runs all of the aboveThis quick harness is intentionally selective: it covers the curated v2/v3 petstore JSON+YAML outputs, the v3.1 webhook JSON+YAML outputs, and the v3.2 JSON output. openapi/v3.2 remains JSON-only here because the repository does not currently ship a v3.2 YAML root fixture. For broader JSON+YAML fixture coverage across the sample tree, use pwsh test/smoke-tests.ps1. generated/main.zig imports the curated v2/v3 JSON+YAML modules plus the v3.1 YAML module, initializes Client values, and exercises memory-managed endpoint calls. generated/compile_generated.zig extends compile coverage across all curated generated artifacts. When Zig is available, validate generated examples with:
zig build run-generate
zig build test
zig test generated/compile_generated.zig
zig build-exe generated/main.zig -fno-emit-bin
zig build test-packageopenapi2zig can also be used as a Zig library for parsing OpenAPI/Swagger specifications and generating code programmatically.
Add openapi2zig to your build.zig.zon:
.{
.name = "my-project",
.version = "0.1.0",
.dependencies = .{
.openapi2zig = .{
.url = "https://github.com/christianhelle/openapi2zig/archive/refs/tags/v1.0.0.tar.gz",
.hash = "12345...", // Replace with actual hash from `zig fetch`
},
},
}Then in your build.zig:
const openapi2zig_dep = b.dependency("openapi2zig", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("openapi2zig", openapi2zig_dep.module("openapi2zig"));The repository includes a minimal downstream consumer fixture in examples/package_consumer/, and zig build test-package builds it against a clean package snapshot so ignored local files cannot mask packaging issues.
const std = @import("std");
const openapi2zig = @import("openapi2zig");
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
// Read OpenAPI specification
const content = try std.Io.Dir.cwd().readFileAlloc(io, "api.json", allocator, .limited(1024 * 1024));
defer allocator.free(content);
// Detect version
const version = try openapi2zig.detectVersion(allocator, content);
std.debug.print("Detected version: {}\n", .{version});
// Parse to unified document representation
var unified_doc = try openapi2zig.parseToUnified(allocator, content);
defer unified_doc.deinit(allocator);
std.debug.print("API: {s} v{s}\n", .{ unified_doc.info.title, unified_doc.info.version });
// Generate Zig code
const args = openapi2zig.CliArgs{
.input_path = "api.json",
.output_path = null,
.base_url = "https://api.example.com",
};
const generated_code = try openapi2zig.generateCode(allocator, unified_doc, args);
defer allocator.free(generated_code);
// Write generated code to file
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = "generated.zig", .data = generated_code });
}detectVersion(allocator, json_content)- Detect OpenAPI/Swagger version from JSONdetectVersionFromYaml(allocator, yaml_content)- Detect OpenAPI/Swagger version from YAMLApiVersion- Enum representing supported API versions (.v2_0, .v3_0, .v3_1, .v3_2, .Unsupported)
parseToUnified(allocator, json_content)- Parse any supported JSON version (v2.0, v3.0, v3.1, v3.2) to unified representationparseOpenApi(allocator, json_content)- Parse OpenAPI v3.0 specificallyparseOpenApiYaml(allocator, yaml_content)- Parse OpenAPI v3.0 YAML specificallyparseOpenApi31(allocator, json_content)- Parse OpenAPI v3.1 specificallyparseOpenApi31Yaml(allocator, yaml_content)- Parse OpenAPI v3.1 YAML specificallyparseOpenApi32(allocator, json_content)- Parse OpenAPI v3.2 specificallyparseOpenApi32Yaml(allocator, yaml_content)- Parse OpenAPI v3.2 YAML specificallyparseSwagger(allocator, json_content)- Parse Swagger v2.0 specificallyparseSwaggerYaml(allocator, yaml_content)- Parse Swagger v2.0 YAML specifically
generateCode(allocator, unified_doc, args)- Generate complete Zig code (models + API)generateModels(allocator, unified_doc)- Generate only model structsgenerateApi(allocator, unified_doc, args)- Generate only API client functions
convertSwaggerToUnified(allocator, swagger_doc)- Convert Swagger v2.0 to unified formatconvertOpenApiToUnified(allocator, openapi_doc)- Convert OpenAPI v3.0 to unified formatconvertOpenApi31ToUnified(allocator, openapi_doc)- Convert OpenAPI v3.1 to unified formatconvertOpenApi32ToUnified(allocator, openapi_doc)- Convert OpenAPI v3.2 to unified format
UnifiedDocument- Common document representation for all OpenAPI and Swagger versionsSwaggerDocument- Swagger v2.0 specific document structureOpenApiDocument- OpenAPI v3.0 specific document structureOpenApi31Document- OpenAPI v3.1 specific document structureOpenApi32Document- OpenAPI v3.2 specific document structureDocumentInfo,Schema,Operation, etc. - Various OpenAPI components
Generated files are self-contained Zig source files. The current unified generator emits:
- Schema declarations such as
Pet,Order, and nested helper types. - A reusable
Clientstruct with allocator,std.Io,std.http.Client, API key, base URL, optional organization/project headers, and borroweddefault_headers.default_headersand all header name/value storage must stay alive while requests use them. - Memory-safe response wrappers:
Owned(T),RawResponse,ParseErrorResponse, andApiResult(T). - Endpoint triplets when a response schema is known:
operation(...) !Owned(T)for convenience parsed responses.operationRaw(...) !RawResponsefor status/body inspection.operationResult(...) !ApiResult(T)for parsed success plus preserved API/parse-error bodies.
- Generic helpers such as
requestRaw,getRaw,postJsonRaw,getJsonResult, andpostJsonResult. - Query parameter helpers that percent-encode names and string values with
std.Uri.Component.percentEncode; optional query parameters are nullable. - Bounded SSE parsing helpers:
parseSseBytes,parseSseReader,parseSseBytesTyped, andparseSseReaderTyped. OpenAI-style stream helpers such asstreamChatCompletion,streamChatCompletionEvents,streamResponse, andstreamResponseEventsare generated when matching operation IDs exist in the input spec. - Resource wrapper namespaces by default, for example
pet.get(...)andstore.order.get(...), derived from paths unless--resource-wrapperschanges the mode. Wrapper names are sanitized generated conveniences, not hand-designed SDK names.
Parsed JSON responses use .ignore_unknown_fields = true so compatible providers can add response fields without breaking callers. Ambiguous or intentionally open-ended schemas use std.json.Value; see docs/json-value-typing-policy.md for the current policy. For OpenAPI 3.1, the converter has stronger composite-schema handling for object/ref allOf, preserved oneOf/anyOf metadata, and nullable type arrays; do not assume every converter has identical composite support.
The snippets below reflect the current output from zig build run-generate-v3.
pub const Tag = struct {
id: ?i64 = null,
name: ?[]const u8 = null,
};
pub const Category = struct {
id: ?i64 = null,
name: ?[]const u8 = null,
};
pub const Pet = struct {
status: ?[]const u8 = null,
tags: ?[]const Tag = null,
category: ?Category = null,
id: ?i64 = null,
name: []const u8,
photoUrls: []const []const u8,
};pub fn Owned(comptime T: type) type {
return struct {
allocator: std.mem.Allocator,
body: []u8,
parsed: std.json.Parsed(T),
pub fn deinit(self: *@This()) void {
self.parsed.deinit();
self.allocator.free(self.body);
}
pub fn value(self: *@This()) *T {
return &self.parsed.value;
}
};
}
pub const RawResponse = struct {
allocator: std.mem.Allocator,
status: std.http.Status,
body: []u8,
pub fn deinit(self: *@This()) void {
self.allocator.free(self.body);
}
};
pub fn ApiResult(comptime T: type) type {
return union(enum) {
ok: Owned(T),
api_error: RawResponse,
parse_error: ParseErrorResponse,
pub fn deinit(self: *@This()) void {
switch (self.*) {
.ok => |*value| value.deinit(),
.api_error => |*value| value.deinit(),
.parse_error => |*value| value.raw.deinit(),
}
}
};
}
pub const Client = struct {
allocator: std.mem.Allocator,
io: std.Io,
http: std.http.Client,
api_key: []const u8,
base_url: []const u8 = "https://petstore3.swagger.io/api/v3",
organization: ?[]const u8 = null,
project: ?[]const u8 = null,
default_headers: []const std.http.Header = &.{},
pub fn init(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8) Client {
return .{
.allocator = allocator,
.io = io,
.http = .{ .allocator = allocator, .io = io },
.api_key = api_key,
};
}
pub fn deinit(self: *Client) void {
self.http.deinit();
}
pub fn withBaseUrl(self: *Client, base_url: []const u8) void {
self.base_url = base_url;
}
};pub fn getPetById(client: *Client, petId: i64) !Owned(Pet) {
var result = try getPetByIdResult(client, petId);
switch (result) {
.ok => |ok| return ok,
.api_error => |*err| {
err.deinit();
return error.ResponseError;
},
.parse_error => |*err| {
err.raw.deinit();
return error.ResponseParseError;
},
}
}
pub fn getPetByIdRaw(client: *Client, petId: i64) !RawResponse {
const allocator = client.allocator;
var uri_buf: std.Io.Writer.Allocating = .init(allocator);
defer uri_buf.deinit();
try uri_buf.writer.print("{s}/pet/{d}", .{ client.base_url, petId });
const payload: ?[]const u8 = null;
return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload);
}
pub fn getPetByIdResult(client: *Client, petId: i64) !ApiResult(Pet) {
return parseRawResponse(Pet, try getPetByIdRaw(client, petId));
}var client = api.Client.init(allocator, io, "");
defer client.deinit();
client.withBaseUrl("https://petstore3.swagger.io/api/v3");
var pet = try api.getPetById(&client, 1);
defer pet.deinit();
std.debug.print("pet name: {s}\n", .{pet.value().name});
var result = try api.getPetByIdResult(&client, 1);
defer result.deinit();
switch (result) {
.ok => |*ok| std.debug.print("pet id: {?}\n", .{ok.value().id}),
.api_error => |raw| std.debug.print("HTTP status: {}\n{s}\n", .{ raw.status, raw.body }),
.parse_error => |parse| std.debug.print("parse error: {s}\n{s}\n", .{ parse.error_name, parse.raw.body }),
}
// Default path resource wrappers are also exported:
var wrapped = try api.pet.get(&client, 1);
defer wrapped.deinit();- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests and ensure they pass (
zig build test) - Check code formatting (
zig fmt --check src/) - Commit your changes (
git commit -am 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project follows standard Zig formatting. Use zig fmt to format your code before committing.
๐ Active Development ๐
This project is in active development with solid foundation for OpenAPI/Swagger support. Current capabilities include:
- Full parsing support for Swagger v2.0, OpenAPI v3.0, v3.1, and v3.2 specifications
- Comprehensive data model structures for all OpenAPI versions
- Generate type-safe API client code using
std.http.Client - Extensive test suite covering all specification versions
- Cross-compilation support (Linux, macOS, Windows)
- Both CLI tool and Zig library interfaces
Planned features and enhancements:
- Enhanced authentication/authorization client support
- Automatic API documentation generation
- Performance optimizations for large specifications
This project is licensed under the MIT License - see the LICENSE file for details.
If you encounter any issues or have questions, please open an issue on GitHub.