{"id":50911169,"url":"https://github.com/h0rv/zchema","last_synced_at":"2026-06-16T10:30:36.280Z","repository":{"id":362920129,"uuid":"1259519092","full_name":"h0rv/zchema","owner":"h0rv","description":"Typed, validated JSON APIs and OpenAPI 3.1 for Zig's std.http.Server","archived":false,"fork":false,"pushed_at":"2026-06-06T15:18:49.000Z","size":61,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-06T15:24:30.024Z","etag":null,"topics":["api","http","json","jsonschema","openapi","zig","zig-package"],"latest_commit_sha":null,"homepage":"","language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/h0rv.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-04T15:32:55.000Z","updated_at":"2026-06-06T15:18:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/h0rv/zchema","commit_stats":null,"previous_names":["h0rv/zchema"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/h0rv/zchema","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/h0rv%2Fzchema","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/h0rv%2Fzchema/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/h0rv%2Fzchema/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/h0rv%2Fzchema/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/h0rv","download_url":"https://codeload.github.com/h0rv/zchema/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/h0rv%2Fzchema/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34402648,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","http","json","jsonschema","openapi","zig","zig-package"],"created_at":"2026-06-16T10:30:35.061Z","updated_at":"2026-06-16T10:30:36.272Z","avatar_url":"https://github.com/h0rv.png","language":"Zig","funding_links":[],"categories":[],"sub_categories":[],"readme":"# zchema\n\n[![CI](https://github.com/h0rv/zchema/actions/workflows/ci.yml/badge.svg)](https://github.com/h0rv/zchema/actions/workflows/ci.yml)\n\nTyped, validated JSON APIs and OpenAPI 3.1 for Zig's `std.http.Server`.\n\n`zchema` is a thin layer over `std.http.Server`. It adds JSON request\nparsing, response serialization, JSON Schema validation, and OpenAPI 3.1\ngeneration. It does not own the accept loop, the socket lifecycle, the threading\nmodel, or any non-JSON behavior. `std.http.Server.Request` stays available\neverywhere, so the raw stdlib path is always one call away.\n\nThe approach is borrowed from FastAPI and Pydantic: your declarative Zig types\nare the single source of truth, and validation and an OpenAPI spec fall out of\nthem.\n\nSchemas and validation come from\n[`h0rv/jsonschema.zig`](https://github.com/h0rv/jsonschema.zig) (Draft 2020-12).\nRequires Zig 0.16.0+.\n\n## Install\n\n```sh\nzig fetch --save \"git+https://github.com/h0rv/zchema.git\"\n```\n\nWire the module into `build.zig`:\n\n```zig\nconst zchema = b.dependency(\"zchema\", .{ .target = target, .optimize = optimize });\nexe.root_module.addImport(\"zchema\", zchema.module(\"zchema\"));\n```\n\n`jsonschema` is pulled in automatically as a transitive dependency of\n`zchema`, so you do not need to fetch or wire it yourself.\n\nThe snippets below import the module under a short alias:\n\n```zig\nconst z = @import(\"zchema\");\n```\n\n## Migrating an existing handler\n\nKeep your server loop and your routing. Adopt contracts where you want them. A\nraw stdlib handler:\n\n```zig\nconst reader = try req.readerExpectContinue(\u0026buf);\nconst raw = try reader.allocRemaining(arena, .limited(1 \u003c\u003c 20));\nconst input = std.json.parseFromSliceLeaky(Echo, arena, raw, .{}) catch\n    return req.respond(\"{\\\"error\\\":\\\"invalid json\\\"}\", .{ .status = .bad_request });\n// ...validate by hand, serialize by hand...\n```\n\nbecomes:\n\n```zig\nconst input = z.jsonBody(Echo, arena, \u0026req, .{}) catch |err|\n    return z.respondError(arena, \u0026req, err, .{});\ntry z.respondJson(Greeting, arena, \u0026req, .ok, .{ .message = input.name }, .{});\n```\n\n`jsonBody` reads the body under a byte limit, validates it against the schema\nemitted from `Echo`, then parses. `respondError` turns a zchema error into a\nstructured JSON body. These helpers work inside any existing handler; you do not\nneed to register routes to use them.\n\n## Registered routes and markers\n\nThe handler signature is the contract. Markers tell the dispatcher what each\nparameter and the return type mean, and the same information drives OpenAPI:\n\n- `Body(T)`: parsed and validated request body.\n- `Path(T)`: path params, parsed from the `{...}` segments into `T`.\n- `Query(T)`: query params, parsed into `T` (fields with a default or `?T` are optional).\n- `Header(\"name\")`: one request header (case-insensitive) as `value: ?[]const u8`;\n  the name is in the type, so it is also documented as an OpenAPI header parameter.\n  For dynamic or case-sensitive lookups use `z.header(req, name)` /\n  `z.headerWith(req, name, .sensitive)` on a `*Request`.\n- `*std.http.Server.Request`: the raw request. `std.mem.Allocator`: the per-request arena.\n- Return type: `Created(T)`/`Status(code, T)` for a fixed status, a plain `T` for 200,\n  or `!?T` for \"200 with T, or 404\".\n\n```zig\nconst Api = z.Api(.{\n    z.post(\"/users\", createUser),\n    z.get(\"/users\", listUsers),\n    z.get(\"/users/{id}\", getUser),\n    z.delete(\"/users/{id}\", deleteUser),\n});\n\nfn createUser(store: *Store, body: z.Body(CreateUser)) !z.Created(User) {\n    return .{ .value = try store.create(body.value.name) };\n}\n\nfn listUsers(store: *Store, page: z.Query(struct { limit: u32 = 50 })) ![]const User {\n    return store.list(page.value.limit);\n}\n\nfn getUser(store: *Store, path: z.Path(struct { id: u32 })) !?User {\n    return store.find(path.value.id); // null -\u003e 404\n}\n```\n\nNo explicit contracts are needed for the common cases above. Path and query are\nparsed before the body, so they stay valid even though reading the body\ninvalidates `req.head`. Invalid params return a 422 with per-field detail.\n\n## Explicit contracts\n\nReach for these only when the signature cannot express it: extra response cases,\nor naming a body type that is not a `Body(T)` param. Declare contracts and attach\nthem with `op` (or `route(...).with(...)`):\n\n```zig\nconst CreateUserResponse = z.Response(.{\n    z.case(.created, User),\n    z.case(.unprocessable_entity, z.ErrorBody),\n});\n\nconst Api = z.Api(.{\n    z.op(.POST, \"/users\", createUser, .{ .response = CreateUserResponse }),\n});\n```\n\n## Serving\n\nzchema owns the contracts, not the server. You run `std.http.Server` and call\n`Server.handle` per request; it returns `false` when nothing matched, so you stay\nin control of the loop, threading, and socket lifecycle:\n\n```zig\nconst Server = z.App(Api, .{ .openapi = .{ .title = \"Users API\", .version = \"1.0.0\" } });\n\nfn serveConnection(io: std.Io, gpa: std.mem.Allocator, ctx: *Ctx, stream: std.Io.net.Stream) void {\n    defer stream.close(io);\n    var recv: [16 * 1024]u8 = undefined;\n    var send: [16 * 1024]u8 = undefined;\n    var sr = stream.reader(io, \u0026recv);\n    var sw = stream.writer(io, \u0026send);\n    var http = std.http.Server.init(\u0026sr.interface, \u0026sw.interface);\n    // One arena per connection, reset (not freed) between requests so keep-alive\n    // requests reuse the same memory instead of allocating each time.\n    var arena_state = std.heap.ArenaAllocator.init(gpa);\n    defer arena_state.deinit();\n    while (true) {\n        var req = http.receiveHead() catch return;\n        defer _ = arena_state.reset(.retain_capacity);\n        const arena = arena_state.allocator();\n        if (Server.handle(ctx, arena, \u0026req, .{}) catch return) continue;\n        z.respondErrorBody(arena, \u0026req, z.errorBody(.not_found, \"No matching route.\", \u0026.{}), .{}) catch return;\n    }\n}\n```\n\n`examples/users_api.zig` is the full single-threaded version; `examples/threaded.zig`\nruns a fixed pool of worker threads accepting on a shared socket (the default\n`init.io` is `std.Io.Threaded`, which is safe to share across threads). For an\nevent loop, drive `handle` from a single-threaded io_uring/kqueue reactor.\n\nNon-JSON endpoints live in the same table via `z.raw`, which takes the raw\nrequest, responds itself, and is excluded from OpenAPI:\n\n```zig\nz.raw(.GET, \"/health\", health) // fn health(req: *std.http.Server.Request) !void\n```\n\n## Data models\n\nModels are plain structs. Schema metadata rides on an optional\n`pub const jsonschema`. HTTP meaning lives in the contract wrappers, never on\nthe model, because models are shared across endpoints:\n\n```zig\nconst CreateUser = struct {\n    name: []const u8,\n\n    pub const jsonschema = .{ .fields = .{ .name = .{ .minLength = 1 } } };\n};\n```\n\n## OpenAPI 3.1\n\nAny registered `Api` generates an OpenAPI 3.1 document:\n\n```zig\nconst doc = try z.openApiJson(Api, allocator, .{ .title = \"Users API\", .version = \"1.0.0\" });\n// or stream it: try z.writeOpenApi(Api, writer, .{});\n```\n\nRequest bodies, response bodies, multiple response cases, and path and query\nparameters all come from the registered types. Object schemas are hoisted into\n`components/schemas` and referenced with `$ref`.\n\nThe document is validated against the official OpenAPI 3.1 JSON Schema in the\ntest suite, so it stays compliant.\n\nAttach operation metadata with `.with(...)` (or in `endpoint`), and document-level\nmetadata through `OpenApiOptions`:\n\n```zig\nconst Api = z.Api(.{\n    z.get(\"/users/{id}\", getUser).with(.{ .summary = \"Fetch a user\", .tags = \u0026.{\"users\"} }),\n});\n\nconst doc = try z.openApiJson(Api, gpa, .{\n    .title = \"Users API\",\n    .version = \"1.0.0\",\n    .servers = \u0026.{.{ .url = \"https://api.example.com\" }},\n    .tags = \u0026.{.{ .name = \"users\", .description = \"User operations\" }},\n    .security_schemes = \u0026.{.{ .http = .{ .name = \"BearerAuth\", .scheme = \"bearer\", .bearer_format = \"JWT\" } }},\n    .security = \u0026.{\"BearerAuth\"},\n});\n```\n\n`openApiJson` is just a renderer over inspectable data. The route table is\npublic: walk `Api.routes`, call `operation(route)` for each, and read the request\nbody, params, and response models. Use that to build your own artifacts (a custom\nspec, client codegen, a route listing). See `examples/introspect.zig`.\n\n## App: spec and docs on by default\n\n`App` wraps an `Api` and serves your routes plus an OpenAPI spec endpoint\n(`/openapi.json`) and a docs UI (`/docs`), both on by default:\n\n```zig\nconst Server = z.App(Api, .{\n    .openapi = .{ .title = \"Users API\", .version = \"1.0.0\" },\n});\n\n// in the request loop:\nif (!try Server.handle(\u0026store, arena, \u0026req, .{})) {\n    // not a route, the spec, or the docs page: fall through to raw stdlib.\n}\n```\n\nReserved paths are checked against your routes at comptime, so registering\n`GET /docs` or `GET /openapi.json` yourself is a compile error. Override or turn\nthings off:\n\n```zig\nz.App(Api, .{\n    .docs = .{\n        .ui = .redoc,            // .scalar (default), .redoc, .swagger_ui, .elements\n        .ui_path = \"/reference\", // default \"/docs\"\n        .spec_path = \"/spec.json\", // default \"/openapi.json\"\n        // .enabled = false,     // turn the spec and docs off entirely\n    },\n});\n```\n\n## Docs UI\n\nScalar is the default. Its promotional and telemetry features (AI chat, MCP,\ntelemetry) are off by default and configurable, and the CDN URLs are\noverridable so you can pin a version or self-host:\n\n```zig\nz.App(Api, .{\n    .docs = .{\n        .scalar = .{\n            .hide_models = true,\n            .disable_ai = true,   // default\n            .theme = \"moon\",\n            .extra_json = \"\\\"showSidebar\\\":false\", // anything Scalar supports\n        },\n        .assets = .{ .script = \"https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.25.0\" },\n    },\n});\n```\n\nIf you would rather serve the page yourself, `docsHtml`, `writeDocsHtml`, and\n`respondDocs` return, stream, or send the same HTML with the same `DocsOptions`.\n\n## Validation with any server\n\nThe validation and schema layers do not depend on `std.http`; they operate on\nraw bytes and Zig types. So with any server, including\n[http.zig](https://github.com/karlseguin/http.zig), you still get request and\nresponse validation by calling the primitives directly:\n\n```zig\n// validate a raw request body into a typed value (structured errors on failure)\nconst input = try z.parseAndValidate(CreateUser, req.arena, req.body() orelse \"\", null);\n\n// serialize a value to JSON bytes, optionally validating it first\nres.body = try z.serializeAndValidate(User, res.arena, user, false);\n\n// emit a JSON Schema for any type\nconst schema = z.schemaText(CreateUser);\n```\n\nFor an OpenAPI document, declare the endpoints directly with `endpoint`/`Spec`,\npassing your models. No handlers and no dispatcher are involved, so you keep your\nown framework's router and just serve the generated document:\n\n```zig\nconst ApiSpec = z.Spec(.{\n    z.endpoint(.POST, \"/users\", .{\n        .body = CreateUser,\n        .responses = .{ z.case(.created, User), z.case(.bad_request, z.ErrorBody) },\n    }),\n    z.endpoint(.GET, \"/users/{id}\", .{\n        .path = struct { id: u32 },\n        .responses = .{ z.case(.ok, User), z.case(.not_found, z.ErrorBody) },\n    }),\n});\n\nconst doc = try z.openApiJson(ApiSpec, gpa, .{ .title = \"Users API\", .version = \"1.0.0\" });\n// serve `doc` from your server at /openapi.json\n```\n\nThe dispatcher, markers, and `App` remain `std.http`-specific (they own routing);\n`endpoint`/`Spec` are the handler-free path that works anywhere.\n\n`examples/byo_server.zig` is a complete runnable version of this pattern: a\nhand-rolled router that declares a `Spec`, validates bodies with\n`parseAndValidate`, and serves the spec and docs (rendered once) itself.\n\n## Non-JSON behavior\n\n`zchema` only touches JSON. HTML, bytes, files, redirects, streaming, and\nWebSockets pass straight through to the stdlib:\n\n```zig\ntry req.respond(bytes, .{\n    .status = .ok,\n    .extra_headers = \u0026.{.{ .name = \"content-type\", .value = \"application/octet-stream\" }},\n});\n```\n\n## Errors\n\nBoundary failures produce an `ErrorBody` following RFC 9457\n(`application/problem+json`): `type`, `title`, `status`, `detail`, and an\n`errors` array of `{pointer, message}` (JSON Pointer per field). Covered cases:\ninvalid JSON (400), validation failure (422), unsupported content type (415),\nand body too large (413).\n\n```json\n{\n  \"type\": \"about:blank\",\n  \"title\": \"Unprocessable Entity\",\n  \"status\": 422,\n  \"detail\": \"Request body failed validation.\",\n  \"errors\": [{ \"pointer\": \"/name\", \"message\": \"unexpected property\" }]\n}\n```\n\n`respondError(arena, req, err, .{})` maps a caught zchema error to this body;\n`errorBody(status, detail, fields)` plus `respondErrorBody` send a custom one.\nDeclare your own error body types as response cases when you need more.\n\n## Develop\n\n```sh\nmise run test        # zig build test, including example compilation\nmise run check       # formatting checks plus tests\nzig build run        # print the demo API's OpenAPI document\nzig build run-users_api\nzig build run-migration\n```\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fh0rv%2Fzchema","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fh0rv%2Fzchema","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fh0rv%2Fzchema/lists"}