Zig Add ServiceStack Reference

ServiceStack's Add ServiceStack Reference feature allows clients to generate Native Types for Zig - providing a simple way to give Zig clients typed access to your ServiceStack Services.

First class development experience

Zig is a fast-growing systems language offering a simpler alternative to C with compile-time metaprogramming, explicit allocators and no hidden control flow, which sees it increasingly used for performance-critical services, embedded software and cross-compilation toolchains. To maximize the experience for calling ServiceStack APIs from these environments, Zig is supported as a 1st class Add ServiceStack Reference language which gives Zig developers an end-to-end typed API for consuming ServiceStack APIs, with DTOs generated from a single command-line.

Ideal idiomatic Typed Message-based API

Zig DTOs are generated as plain structs whose field names match the JSON they're serialized with, so they work directly with std.json without any custom parsing. Every field is given a default value so Responses that omit them parse cleanly, and inherited properties are flattened into their sub types since Zig has no inheritance.

Here's a sample of generated Zig DTOs containing a string Enum, a standard Request DTO and an AutoQuery Request:

const std = @import("std");
const ss = @import("servicestack");

pub const RoomType = enum {
    Single,
    Double,
    Queen,
    Twin,
    Suite,
};

pub const HelloResponse = struct {
    result: ?[]const u8 = null,
};

// @Route("/hello/{Name}")
pub const Hello = struct {
    pub const ss_name = "Hello";
    pub const ss_verb = "GET";
    pub const Response = HelloResponse;

    name: ?[]const u8 = null,
};

/// Find Bookings
// @Route("/bookings", "GET")
pub const QueryBookings = struct {
    pub const ss_name = "QueryBookings";
    pub const ss_verb = "GET";
    pub const Response = ss.QueryResponse(Booking);

    // Inherited AutoQuery params
    skip: ?i32 = null,
    take: ?i32 = null,
    orderBy: ?[]const u8 = null,
    orderByDesc: ?[]const u8 = null,

    id: ?i32 = null,
};

The generated ss_name, ss_verb and Response declarations are what enable the end-to-end typed API, letting the client resolve each API's route, HTTP Method and Response Type from its Request DTO at comptime - so client.send() returns the correct Response Type with no type arguments and no runtime overhead.

Installation

The only requirements for Zig Apps to perform typed API Requests are the generated Zig DTOs and the generic JsonServiceClient in the servicestack-zig module, which only uses the Zig standard library:

Then add the module to your build.zig:

const servicestack = b.dependency("servicestack", .{ .target = target, .optimize = optimize });

const exe = b.addExecutable(.{
    .name = "myapp",
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
        .imports = &.{
            .{ .name = "servicestack", .module = servicestack.module("servicestack") },
        },
    }),
});

Requires Zig 0.15+.

Simple command-line utility for Zig

Zig DTOs can be generated from the command-line with the cross-platform x command line utility.

To install first install the latest .NET SDK for your OS then install the x dotnet tool with:

dotnet tool install --global x

Alternative (without .NET): npx get-dtos

Alternatively API consumers can use npx get-dtos to Add/Update ServiceStack References without needing .NET installed, where any command starting with:

x <lang>

Can be replaced with:

npx get-dtos <lang>

To instead Add / Update ServiceStack references using the npm get-dtos package.

Adding a ServiceStack Reference

To Add a Zig ServiceStack Reference just call x zig with the URL of a remote ServiceStack instance:

Result:

Saved to: dtos.zig

Calling x zig with just a URL will save the DTOs using the Host name, you can override this by specifying a FileName as the 2nd argument:

Result:

Saved to: Bookings.dtos.zig

Updating a ServiceStack Reference

To Update an existing ServiceStack Reference, call x zig with the Filename:

x zig dtos.zig

Result:

Updated: dtos.zig

Which will update the File with the latest Zig Server DTOs. You can also customize how DTOs are generated by uncommenting the Zig DTO Customization Options and updating them again.

Updating all Zig DTOs

Calling x zig without any arguments will update all Zig DTOs in the current directory:

x zig

Smart Generic JsonServiceClient

The generic JsonServiceClient is a 1st class client with the same rich featureset of the smart ServiceClients in other 1st class supported languages sporting a terse, typed flexible API with support for custom URLs and HTTP Methods and raw Response bodies.

It includes built-in support for a number of ServiceStack Auth options including HTTP Basic Auth and stateless Bearer Token Auth Providers like API Key and JWT Auth as well as stateful Sessions used by the popular credentials Auth Provider, whose Session Cookies are retained by the client's built-in CookieJar which std.http.Client doesn't provide. Refresh Tokens are also supported, where expired JWT Bearer Tokens are transparently refreshed behind-the-scenes before automatically retrying the failed Request.

pub const JsonServiceClient = struct {
    pub fn init(allocator: std.mem.Allocator, base_url: []const u8) !Self
    pub fn deinit(self: *Self) void

    pub fn setBasePath(self: *Self, base_path: []const u8) !void
    pub fn setBearerToken(self: *Self, token: []const u8) void
    pub fn setCredentials(self: *Self, user_name: []const u8, password: []const u8) void
    pub fn setHeader(self: *Self, name: []const u8, value: []const u8) !void
    pub fn getError(self: *Self) ?WebServiceException

    // Typed API
    pub fn send(self: *Self, request: anytype) !std.json.Parsed(ResponseTypeOf(@TypeOf(request)))
    pub fn get(self: *Self, request: anytype) !std.json.Parsed(...)
    pub fn post(self: *Self, request: anytype) !std.json.Parsed(...)
    pub fn put(self: *Self, request: anytype) !std.json.Parsed(...)
    pub fn patch(self: *Self, request: anytype) !std.json.Parsed(...)
    pub fn delete(self: *Self, request: anytype) !std.json.Parsed(...)
    pub fn sendVoid(self: *Self, request: anytype) !void
    pub fn sendAs(self: *Self, comptime ResponseType: type, request: anytype) !std.json.Parsed(ResponseType)
    pub fn api(self: *Self, request: anytype) !ApiResult(...)
    pub fn authenticate(self: *Self, user_name: []const u8, password: []const u8) !std.json.Parsed(AuthenticateResponse)
    pub fn setRefreshToken(self: *Self, token: []const u8) void
    on_authentication_required: ?*const fn (client: *Self) anyerror!void

    // File Uploads
    pub fn postFileWithRequest(self: *Self, request: anytype, file: UploadFile) !std.json.Parsed(...)
    pub fn postFilesWithRequest(self: *Self, request: anytype, files: []const UploadFile) !std.json.Parsed(...)

    // Batched and one-way Requests
    pub fn sendAll(self: *Self, comptime ResponseType: type, requests: anytype) !std.json.Parsed([]const ResponseType)
    pub fn publish(self: *Self, request: anytype) !void

    // URL API
    pub fn getUrl(self: *Self, comptime ResponseType: type, path: []const u8) !std.json.Parsed(ResponseType)
    pub fn postUrl(self: *Self, comptime ResponseType: type, path: []const u8, request: anytype) !std.json.Parsed(ResponseType)
    pub fn sendUrlString(self: *Self, method: std.http.Method, path: []const u8, request: anytype) ![]u8
};

Making Typed API Requests

Making API Requests in Zig is the same as all other ServiceStack's Service Clients by sending a populated Request DTO using a JsonServiceClient which returns a typed Response DTO:

const std = @import("std");
const ss = @import("servicestack");
const dtos = @import("dtos.zig");

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

    var client = try ss.JsonServiceClient.init(allocator, "https://blazor-vue.web-templates.io");
    defer client.deinit();

    var res = try client.send(dtos.Hello{ .name = "World" });
    defer res.deinit();

    std.debug.print("{s}\n", .{res.value.result.?});
}

Responses are returned as a std.json.Parsed(T) that owns the memory of its value, so call deinit() when you're done with it.

send uses the HTTP Method the API is annotated with, use get, post, put, patch or delete to send a Request DTO with a specific HTTP Method:

var res = try client.post(dtos.CreateBooking{ .name = "Booking" });
defer res.deinit();

APIs that don't return a Response Body are sent with sendVoid:

try client.sendVoid(dtos.DeleteBooking{ .id = 1 });

AutoQuery Requests

AutoQuery APIs return a typed ss.QueryResponse(T), with the query params of their base type flattened into the Request DTO:

var res = try client.send(dtos.QueryBookings{ .take = 5, .orderByDesc = "id" });
defer res.deinit();

for (res.value.results.?) |booking| { // booking is a dtos.Booking
    std.debug.print("{d} {s}\n", .{ booking.id, booking.name.? });
}

Making API Requests with URLs

In addition to making Typed API Requests you can also call Services using relative or absolute urls:

var res = try client.getUrl(dtos.HelloResponse, "/hello/World");
defer res.deinit();

Raw Data Responses

Use sendUrlString to access a raw Response Body, useful for APIs returning content like CSV:

const csv = try client.sendUrlString(.GET, "/api/QueryBookings.csv", null);
defer allocator.free(csv);

Batched Requests

Multiple Request DTOs of the same Type can be sent together in a single Request with sendAll, which returns all their Responses:

const requests = [_]dtos.Hello{ .{ .name = "A" }, .{ .name = "B" } };
var res = try client.sendAll(dtos.HelloResponse, requests[0..]);
defer res.deinit();

Or send a Request to a one-way endpoint that ignores its Response:

try client.publish(dtos.Hello{ .name = "World" });

Error Handling

As Zig errors can't carry a payload, failed API Requests return error.WebServiceException with the HTTP Status Code and the API's structured ResponseStatus error available from client.getError():

if (client.send(dtos.CreateBooking{})) |res| {
    defer res.deinit();
} else |_| {
    const web_ex = client.getError().?;
    std.debug.print("{d} {s}: {s}\n", .{
        web_ex.status_code,     // 400
        web_ex.errorCode(),     // "NotEmpty"
        web_ex.errorMessage(),  // "'Name' must not be empty."
    });
    std.debug.print("{?s}\n", .{web_ex.fieldError("Name")});
    std.debug.print("{}\n", .{web_ex.isUnauthorized()}); // false
}

Alternatively api returns errors in its result instead of an error union, which can be preferable when handling validation errors is part of normal control flow:

const api = try client.api(dtos.CreateBooking{});
defer api.deinit();

if (api.failed()) {
    std.debug.print("{s} {?s}\n", .{ api.errorCode(), api.fieldError("Name") });
} else {
    std.debug.print("{s}\n", .{api.response.?.id.?});
}

Authenticating using Basic Auth

Basic Auth support is implemented in JsonServiceClient and follows the same API made available in the C# Service Clients:

client.setCredentials("username", "password");

var res = try client.send(dtos.SecureRequest{});
defer res.deinit();

Authenticating using Credentials

Alternatively you can authenticate using userName/password credentials with the authenticate API:

var auth = try client.authenticate("username", "password");
defer auth.deinit();

This will populate the client's CookieJar with Session Cookies which will transparently be sent on subsequent requests to make authenticated requests, as well as using any Bearer Token the Server returns.

Authenticating using JWT

Use setBearerToken to Authenticate with a ServiceStack JWT Provider using a JWT Token:

client.setBearerToken(jwt);

Alternatively you can use just a Refresh Token instead:

client.setRefreshToken(refresh_token);

Where the client will automatically fetch a new JWT Bearer Token using the Refresh Token before retrying requests that returned 401 Unauthorized (v0.1.2+).

Authenticating using an API Key

Use setBearerToken to Authenticate with an API Key:

client.setBearerToken("ak-87949de37e894627a9f6173154e7cafa");

Uploading Files

Use postFileWithRequest to upload a file with an API Request:

var res = try client.postFileWithRequest(dtos.UploadPhoto{ .album = "Holiday" }, .{
    .field_name = "file",
    .file_name = "photo.png",
    .content_type = "image/png",
    .contents = bytes,
});
defer res.deinit();

The Request DTO's populated properties are sent as form fields alongside the file. To upload multiple files use postFilesWithRequest:

const files = [_]ss.UploadFile{
    .{ .field_name = "file1", .file_name = "a.png", .contents = a },
    .{ .field_name = "file2", .file_name = "b.png", .contents = b },
};
var res = try client.postFilesWithRequest(dtos.UploadPhoto{ .album = "Holiday" }, files[0..]);
defer res.deinit();

Requires servicestack v0.1.2+.

Transparently handle 401 Unauthorized Responses

If the server returns a 401 Unauthorized Response either because the client was Unauthenticated or the configured Bearer Token or API Key used had expired or was invalidated, you can use the on_authentication_required callback to re-authenticate before automatically retrying the original request, e.g:

fn signIn(client: *ss.JsonServiceClient) anyerror!void {
    var auth = try client.authenticate("username", "password");
    auth.deinit();
}

client.on_authentication_required = signIn;

// Automatically retries requests returning 401 Responses
var res = try client.send(dtos.Secured{});
defer res.deinit();

Alternatively configure a Refresh Token, which takes precedence over the callback and is used to transparently fetch a new JWT Bearer Token before retrying:

client.setRefreshToken(refresh_token);

Requires servicestack v0.1.2+.

Client Configuration

try client.setHeader("X-Custom", "Value");
try client.setBasePath("");   // use the /json/reply pre-defined routes
client.cookies.clear();       // clear the Session Cookies

JsonServiceClient.init sends Requests to ServiceStack's pre-defined /api route. Use setBasePath("") for older ServiceStack instances that only have the /json/reply routes enabled.

DTO Customization Options

In most cases you'll just use the generated Zig DTOs as-is, however you can further customize how the DTOs are generated by overriding the default options.

The header in the generated DTOs show the different options Zig native types support with their defaults. To override a value, remove a / from its /// prefix and specify the value to the right of the :. Any uncommented value will be sent to the server to override any server defaults.

/// Options:
/// Date: 2026-08-06 15:38:36
/// Version: 10.09
/// Tip: To override a DTO option, remove "/" prefix before updating
/// BaseUrl: https://blazor-vue.web-templates.io
///
/// GlobalNamespace:
/// MakePropertiesOptional: False
/// AddServiceStackTypes: True
/// AddResponseStatus: False
/// AddImplicitVersion:
/// AddDescriptionAsComments: True
/// IncludeTypes:
/// ExcludeTypes:
/// DefaultImports: const std = @import("std");

AddResponseStatus

Automatically add a responseStatus property on all Response DTOs, regardless if it wasn't already defined:

pub const GetTechnologyResponse = struct {
    responseStatus: ?ss.ResponseStatus = null,
};

IncludeTypes

Is used as a Whitelist to specify only the types you would like to have code-generated:

IncludeTypes: GetTechnology,GetTechnologyResponse

To include a Request DTO and all its dependent types, use the .* suffix:

IncludeTypes: GetTechnology.*

Or include all types within a Tag Group with:

IncludeTypes: {tag}

ExcludeTypes

Is used as a Blacklist to specify which types you would like excluded from being generated:

ExcludeTypes: GetTechnology,GetTechnologyResponse

Change Default Server Configuration

The above defaults are also overridable on the ServiceStack Server by modifying the default config on the NativeTypesFeature Plugin, e.g:

//Server example in C#
var nativeTypes = this.GetPlugin<NativeTypesFeature>();
nativeTypes.MetadataTypesConfig.AddResponseStatus = true;

Zig specific functionality can be added by the ZigGenerator, e.g. to change the module generated DTOs reference:

ZigGenerator.LibraryModule = "my_servicestack";

Properties of abstract Types with sub types are emitted as std.json.Value since Zig doesn't support sub classing, which can be disabled with:

ZigGenerator.PolymorphicPropertiesAsAny = false;

Customize DTO Type generation

Additional Zig specific customization can be statically configured like PreTypeFilter, InnerTypeFilter & PostTypeFilter (available in all languages) which can be used to inject custom code in the generated DTOs output, e.g:

ZigGenerator.PreTypeFilter = (sb, type) => {
    if (type.IsEnum != true)
    {
        sb.AppendLine("// Generated DTO");
    }
};

There's also PrePropertyFilter & PostPropertyFilter for generating source before and after properties.

Whilst the generic [EmitCode] attribute lets you emit the same code in multiple languages with the same syntax.