zswag is a set of libraries for using and hosting zserio services through OpenAPI/REST. It provides parallel client implementations in Python, C++, and Java that consume the same OpenAPI specification, plus a Python server layer for exposing zserio services.
%%{init: {
'theme':'base',
'flowchart': {'curve': 'stepAfter', 'nodeSpacing': 50, 'rankSpacing': 70, 'padding': 20},
'themeVariables': {
'fontFamily': 'system-ui, -apple-system, sans-serif',
'fontSize': '14px',
'lineColor': '#6c757d',
'clusterBkg': '#fafbfc',
'clusterBorder': '#cbd5e0'
}
}}%%
flowchart TB
gen["<b>zswag.gen</b> (Python CLI)<br/>generates spec from a<br/>zserio service definition"]:::py
spec(["<b>OpenAPI spec</b> · zserio-derived<br/>shared contract: clients read it, server exposes it"]):::spec
zserio["<b>zserio</b><br/>schema compiler + runtimes<br/>(C++ / Python / Java)"]:::ext
subgraph clients[" Clients "]
direction LR
py["<b>Python</b><br/>OAClient<br/>zswag wheel"]:::py
cpp["<b>C++</b><br/>OAClient<br/>zswagcl"]:::cpp
jvm["<b>Java JVM</b><br/>OAClient<br/>jzswag-jvm"]:::java
andr["<b>Java Android</b><br/>OAClient<br/>jzswag-android"]:::java
end
subgraph server[" Server "]
oaserver["<b>OAServer</b> (Python only)<br/>request dispatch<br/>+ auth enforcement"]:::py
end
subgraph cppcore[" C++ core (shared with Python via pybind11) "]
direction LR
zswagcl["<b>zswagcl</b><br/>OpenAPI parser<br/>+ dispatch"]:::cpp
httpcl["<b>httpcl</b><br/>HTTP transport<br/>+ OS keychain"]:::cpp
end
subgraph javacore[" Java core "]
direction LR
jshared["<b>jzswag-shared</b><br/>dispatch, encoding,<br/>OAuth2 / YAML loader"]:::java
japi["<b>jzswag-api</b><br/>interfaces,<br/>value types"]:::java
end
subgraph extlibs[" External libraries "]
direction LR
libcurl["libcurl + nghttp2"]:::ext
keychain["keychain"]:::ext
flask["Flask /<br/>Connexion"]:::ext
okhttp["OkHttp /<br/>Android Keystore"]:::ext
end
zserio --> gen
gen --> spec
spec --> clients
spec --> server
py ==> zswagcl
cpp ==> zswagcl
zswagcl ==> httpcl
httpcl --> libcurl
httpcl --> keychain
jvm ==> jshared
andr ==> jshared
jshared ==> japi
andr --> okhttp
oaserver --> flask
classDef py fill:#e7f5ec,stroke:#2f855a,stroke-width:2px,color:#1c4532
classDef cpp fill:#e6f0fb,stroke:#2c5282,stroke-width:2px,color:#1a365d
classDef java fill:#fdf3e7,stroke:#9c4221,stroke-width:2px,color:#652b19
classDef spec fill:#fffbea,stroke:#975a16,stroke-width:2.5px,color:#5f370e
classDef ext fill:#f1f3f5,stroke:#868e96,stroke-width:1.5px,color:#495057
| Component | Language | Role |
|---|---|---|
zswagcl |
C++ | Core OpenAPI client (OAClient, OpenApiClient, OpenApiConfig); reused by the Python client via pybind11. |
httpcl |
C++ | HTTP wrapper around libcurl with HTTP/2 support; request configuration; OS keychain integration via keychain. |
zswag |
Python | Python OAClient, the Flask/Connexion-based OAServer, and the zswag.gen OpenAPI generator. |
pyzswagcl |
Python | pybind11 bindings exposing zswagcl to Python. Bundled inside the zswag wheel; not installed separately. |
jzswag-api |
Java | Platform-agnostic contracts (HttpConfig, HttpSettings, OpenAPIParameter, IHttpClient, IKeychain, β¦). No third-party deps. |
jzswag-shared |
Java | Portable core: OpenAPI dispatch, x-zserio-request-part, parameter encoding, OAuth2/OAuth1 token endpoint flow, YAML loader. Used by both platform modules. |
jzswag-jvm |
Java | JVM port using JDK 11 HttpClient. Runs on any standard JVM (server, desktop, lambda, CLI). Implements zserio's ServiceClientInterface. |
jzswag-android |
Java | Android port using OkHttp + Android Keystore + AES-GCM-encrypted SharedPreferences. Implements zserio's ServiceClientInterface. |
Detailed guides for each client + the server + the generator:
docs/python.mdβ PythonOAClientandOAServer.docs/cpp.mdβ C++ client and CMake integration.docs/java.mdβ Java client.docs/openapi-generator.mdβzswag.genCLI reference.docs/release-process.mdβ CI/CD platforms, release tagging, and dev snapshot conventions (maintainer-facing).
The shared YAML format for HTTP_SETTINGS_FILE (used by all three clients) is documented in the HTTP Settings File section below.
pip install zswagfrom zswag import OAClient
import services.api as services
client = services.MyService.Client(OAClient("http://localhost:5000/openapi.json"))
client.my_api(services.Request(1))In your CMakeLists.txt:
FetchContent_Declare(zswag
GIT_REPOSITORY https://github.com/ndsev/zswag.git
GIT_TAG v2.0.1)
FetchContent_MakeAvailable(zswag)
add_zserio_library(myapp-zserio-cpp
WITH_REFLECTION
ROOT "${CMAKE_CURRENT_SOURCE_DIR}"
ENTRY services.zs
TOP_LEVEL_PKG myapp_services)
target_link_libraries(myapp myapp-zserio-cpp zswagcl)auto httpClient = std::make_unique<httpcl::CurlHttpClient>();
auto config = zswagcl::fetchOpenAPIConfig("http://localhost:5000/openapi.json", *httpClient);
auto transport = zswagcl::OAClient(config, std::move(httpClient));
auto client = MyService::Client(transport);
auto resp = client.myApiMethod(Request(1));dependencies {
implementation project(':libs:jzswag:jzswag-jvm')
implementation "io.github.ndsev:zserio-runtime:2.16.1"
}import io.github.ndsev.zswag.jvm.OAClient;
OAClient transport = new OAClient("http://localhost:5000/openapi.json");
MyService.MyServiceClient client = new MyService.MyServiceClient(transport);
Response r = client.myApiMethod(new Request(1));dependencies {
implementation project(':libs:jzswag:jzswag-android')
implementation "io.github.ndsev:zserio-runtime:2.16.1"
}import io.github.ndsev.zswag.android.OAClient;
OAClient transport = new OAClient(context, "http://localhost:5000/openapi.json");
MyService.MyServiceClient client = new MyService.MyServiceClient(transport);
Response r = client.myApiMethod(new Request(1));The only difference is the Context parameter on the constructor β needed so AndroidKeychain can reach SharedPreferences for credential storage.
Wheels are published for 64-bit Python 3.10-3.14 on Linux (x86_64), macOS (x86_64 / arm64), and Windows (x64). On Windows install the Microsoft Visual C++ Redistributable.
zswag uses CMake's FetchContent for dependencies; CMake β₯ 3.22.3 required. See docs/cpp.md for full build options including offline / disconnected builds and code coverage.
Java 11+ source/target. The integration test depends on pip install zswag for its counterparty server. See docs/java.md.
| Variable | Effect |
|---|---|
HTTP_SETTINGS_FILE |
Path to YAML settings file (see HTTP Settings File below). Empty/unset β no persistent config. |
HTTP_LOG_LEVEL |
Verbosity (debug, trace). Useful for OAuth2 troubleshooting. |
HTTP_LOG_FILE |
Logfile path with rotation (3-file window: FILE, FILE-1, FILE-2). Supported by all clients (C++/Python via spdlog, Java via logback RollingFileAppender). |
HTTP_LOG_FILE_MAXSIZE |
Rotation size in bytes; default 1 GB. Supported by all clients. |
HTTP_TIMEOUT |
Request timeout (connect + transfer) in seconds. Default 60. |
HTTP_SSL_STRICT |
Set to any non-empty value (e.g. 1) to enable strict SSL certificate validation. Unset or empty disables. Note: this is "any-non-empty enables," not a boolean β HTTP_SSL_STRICT=0 also enables. |
The Python (OAClient / CurlHttpClient), C++, and Java clients all read a YAML file pointed to by the HTTP_SETTINGS_FILE environment variable. The format is identical across all three clients β the same file works for all of them.
If HTTP_SETTINGS_FILE is unset or empty, no persistent settings are applied.
http-settings:
- scope: "*" # URL match pattern (glob), e.g. https://*.example.com/*
# Use 'url:' instead for raw regex.
basic-auth: # Basic auth credentials for matching requests.
user: johndoe
keychain: keychain-service-string # OR
password: cleartext-password
proxy: # HTTP proxy.
host: localhost
port: 8888
user: test # optional
keychain: ... # OR
password: cleartext-password
cookies: # Additional cookies for matching requests.
key: value
headers: # Additional headers.
X-Trace: enabled
query: # Additional query parameters.
api_version: v2
api-key: value # API key β auto-routed to header/query/cookie based on the
# OpenAPI scheme's 'in:' (see Authentication Schemes section).
oauth2:
clientId: my-client-id # REQUIRED
clientSecretKeychain: kc-string # RECOMMENDED β load from keychain
clientSecret: cleartext-secret # OR cleartext (discouraged)
tokenUrl: https://issuer/oauth/token
refreshUrl: https://issuer/oauth/token # optional; defaults to tokenUrl
audience: https://api.example.com/ # optional
scope: ["api.read", "api.write"] # optional override of per-operation scopes
useForSpecFetch: true # optional, default true
tokenEndpointAuth:
method: rfc6749-client-secret-basic # OR rfc5849-oauth1-signature
nonceLength: 16 # only for rfc5849, range 8..64A multi-scope file simply has multiple list entries; for a given request URL, all matching scopes are merged in declaration order, with later scopes overriding scalar fields. Multi-valued fields (headers, query, cookies) are unioned.
For proxy configs, user is optional; if user is set, then password or keychain is required.
String values in http-settings.yaml may reference process environment variables as $env.NAME. This is intended for container secrets where keychain setup is too cumbersome. References are expanded when the settings file is parsed; if a referenced variable is not set, parsing fails with an explicit error.
http-settings:
- scope: https://api.example.com/*
basic-auth:
user: service-user
password: $env.API_PASSWORD
headers:
Authorization: Bearer $env.API_TOKENMap keys are structural and are not expanded.
scope: is a shell-style glob with * as the only wildcard, matched against the full request URL after request building. Examples:
"*"β matches all requests."https://*.foo.com/*"β matcheshttps://api.foo.com/data(the dot beforefoois literal βhttps://foo.com/does NOT match)."http://localhost:5555/*"β matches local dev servers on a specific port.
To match by raw regex instead, use url: in place of scope::
http-settings:
- url: "^https?://(api|admin)\\.example\\.com/.*$"
headers: ...Only the clientCredentials flow is supported across all zswag clients. Other flows (authorizationCode, implicit, password) and OpenID Connect cause the spec parser to reject the security scheme.
| Field | Required? | Notes |
|---|---|---|
clientId |
Always | OAuth2 client identifier. |
tokenUrl |
When useForSpecFetch: true (default) |
If false, the URL falls back to the spec's flows.clientCredentials.tokenUrl. |
clientSecret / clientSecretKeychain |
For confidential clients | Omit both for public clients (client_id goes in the request body). |
refreshUrl |
Optional | Defaults to spec value, then to tokenUrl. |
scope |
Optional | Defaults to per-operation scopes from the OpenAPI spec. |
audience |
Provider-specific | Some IdPs require it. |
useForSpecFetch |
Optional | Default true. Set false if the OpenAPI spec endpoint is publicly readable. |
tokenEndpointAuth |
Optional | Default rfc6749-client-secret-basic. |
When both http-settings.yaml and the OpenAPI spec specify a value:
tokenUrlβhttp-settings.yamloverrides the spec'sflows.clientCredentials.tokenUrl.refreshUrlβhttp-settings.yamloverrides the spec'sflows.clientCredentials.refreshUrl.scopeβhttp-settings.yamloverrides the per-operationsecurityscopes.
Two authentication methods for the request to the token endpoint itself:
rfc6749-client-secret-basic (default) β RFC 6749 Β§2.3.1: client_id:client_secret in the Authorization: Basic header. Works with most providers.
rfc5849-oauth1-signature β RFC 5849: OAuth 1.0 HMAC-SHA256 signature. The token request is signed using the client secret; the secret itself is never transmitted. nonceLength controls the random nonce length (8β64). Required by some providers that use OAuth 1.0 signature-based token authentication.
By default (useForSpecFetch: true), the OAuth2 token is acquired before fetching the OpenAPI specification, so a spec endpoint that itself requires authentication can be reached. Set useForSpecFetch: false if your spec is public β this defers token acquisition to the first API call, which is faster.
export HTTP_LOG_LEVEL=debug # OAuth2 flow (mint/cache/refresh/auth method)
export HTTP_LOG_LEVEL=trace # adds request/response bodies, signaturesStoring cleartext secrets in http-settings.yaml works but is discouraged. Use the keychain: field instead and pre-load the secret with the platform's native tool. The keychain "package" is lib.openapi.zserio.client (this is hardcoded across all zswag clients so secrets stored by one are visible to the others).
| Platform | Tool | C++ / Python | Java | Example |
|---|---|---|---|---|
| Linux | secret-tool |
β | β | secret-tool store --label='zswag dev' package lib.openapi.zserio.client service my-service user my-user |
| macOS | add-generic-password |
β | β | security add-generic-password -s my-service -a my-user -w 'thepassword' |
| Windows | cmdkey |
β | β β Java keychain lookup on Windows throws KeychainException. Use cleartext password: in http-settings.yaml, or configure credentials adhoc via HttpConfig.basicAuth(...) instead. |
cmdkey /generic:lib.openapi.zserio.client /user:my-user /pass:thepassword |
To disable persistent settings (e.g. in tests), set the env var to empty:
import os
os.environ['HTTP_SETTINGS_FILE'] = ''setenv("HTTP_SETTINGS_FILE", "", 1);// Java: pass HttpSettings.empty() explicitly to the client constructor.All clients treat any HTTP response other than 200 as an error and raise/throw a typed exception with a descriptive message. To accept other codes (e.g. 204 No Content), catch the exception and inspect its status code.
If pip install "connexion[swagger-ui]" is available, OAServer exposes API docs at [/prefix]/ui.
The Server, Clients, and Generator support different subsets of OpenAPI. The tables below detail which feature is supported by which component. Differences are mostly due to limited development scope β open an issue if you need something missing.
For options not supported by zswag.gen, edit the OpenAPI YAML by hand. You'll also need to edit it manually for spec-level metadata (provider name, service version, etc.).
To change the HTTP method, place the desired method name as the key under the method path:
paths:
/methodName:
{get|post|put|delete}:
...| Feature | C++ Client | Python Client | Java Client | OAServer | zswag.gen |
|---|---|---|---|---|---|
get post put delete |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
patch |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
patch is intentionally unsupported across the stack: the partial-object-update semantics it implies cannot be realised in the zserio transport layer interface.
Set requestBody/content to application/x-zserio-object to instruct clients to send the zserio request object in the body when using post/put/delete:
requestBody:
content:
application/x-zserio-object:
schema:
type: string| Feature | C++ Client | Python Client | Java Client | OAServer | zswag.gen |
|---|---|---|---|---|---|
application/x-zserio-object |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
x-zserio-request-part: "*" indicates a parameter holds the whole zserio request as a blob:
parameters:
- in: query|path|header
name: parameterName
required: true
x-zserio-request-part: "*"
schema:
format: string|byte|base64|base64url|hex|binaryAbout format:
stringandbinaryproduce a raw URL-encoded buffer.byteandbase64produce standard Base64.base64urlis URL-safe Base64.hexis hexadecimal.
When a parameter is in path, its value must not be empty (also applies to arrays).
| Feature | C++ Client | Python Client | Java Client | OAServer | zswag.gen |
|---|---|---|---|---|---|
x-zserio-request-part: * |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
format: string |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
format: byte |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
format: hex |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
x-zserio-request-part can also point to a scalar (nested) member of the request:
parameters:
- in: query|path|header
name: parameterName
required: true
x-zserio-request-part: "[parent.]*member"
schema:
format: string|byte|base64|base64url|hex|binaryFor integer types, hex is the natural numeric representation, not binary.
| Feature | C++ Client | Python Client | Java Client | OAServer | zswag.gen |
|---|---|---|---|---|---|
x-zserio-request-part: <[parent.]*member> |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
x-zserio-request-part can point to an array member:
parameters:
- in: query|path|header
style: form|simple|label|matrix
explode: true|false
name: parameterName
required: true
x-zserio-request-part: "[parent.]*array_member"
schema:
format: string|byte|base64|base64url|hex|binaryThe array is encoded according to format, style, and explode per the OpenAPI 3.1 spec.
| Feature | C++ Client | Python Client | Java Client | OAServer | zswag.gen |
|---|---|---|---|---|---|
x-zserio-request-part: <[parent.]*array_member> |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
style: simple |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
style: form |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
style: label |
βοΈ | βοΈ | βοΈ | β | βοΈ |
style: matrix |
βοΈ | βοΈ | βοΈ | β | βοΈ |
explode: true |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
explode: false |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
Compound (struct-typed) x-zserio-request-part is unsupported across all components.
| Feature | C++ Client | Python Client | Java Client | OAServer | zswag.gen |
|---|---|---|---|---|---|
x-zserio-request-part: <[parent.]*compound_member> |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
Each client takes the URL base path from servers[N] (default N = 0). Per OpenAPI 3.0+ (clarified in 3.2.0 Β§4.5.2.1), three URL forms are supported, all resolved against the spec URL via RFC 3986 Β§5.3 reference resolution:
servers:
# 1. Absolute - used as-is
- url: https://api.example.com/v1
# 2. Server-relative path - host+scheme from the spec URL, given path
- url: /v1
# 3. Document-relative - resolved against the spec's directory:
- url: . # spec at https://x/foo/openapi.json -> https://x/foo/
- url: ./v2 # -> https://x/foo/v2
- url: ../v2 # -> https://x/v2
- url: v2 # -> https://x/foo/v2 (same as ./v2)An absent or empty servers array defaults to [{ "url": "/" }]
(server-relative to the spec's origin root).
To target a non-default server entry, pass serverIndex / server_index:
// C++
auto client = zswagcl::OAClient(config, std::move(httpClient), httpConfig, /*serverIndex=*/1);# Python
client = OAClient("http://host/openapi.json", server_index=1)// Java (JVM)
OAClient transport = new OAClient(url, persistent, adhoc, /*serverIndex=*/ 1);
// Java (Android)
OAClient transport = new OAClient(context, url, persistent, adhoc, 1);| Feature | C++ Client | Python Client | Java Client | OAServer | zswag.gen |
|---|---|---|---|---|---|
servers (absolute URL) |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
servers (server-relative /path) |
βοΈ | βοΈ | βοΈ | βοΈ | βοΈ |
servers (document-relative ., ./v2, ../v2) |
βοΈ | βοΈ | βοΈ | n/a | n/a |
Selecting servers[N] (multi-server) |
βοΈ | βοΈ | βοΈ | n/a | n/a |
OpenAPI's securitySchemes and security fields drive auth. Per-operation security: overrides the root-level one; security: [] explicitly disables auth for an operation.
Supported schemes:
- HTTP Basic β credentials checked from
httpcl::Config::auth/HttpConfig.auth/ PythonHTTPConfig.basic_auth. Throws if missing. - HTTP Bearer β verifies an
Authorization: Bearer <token>header is present. Throws if missing. - API key (cookie/header/query) β applies the configured
api-keyto the matching location, or verifies the user has provided it directly. - OAuth2 client credentials β clients automatically acquire, cache, refresh access tokens from the configured token endpoint. Two token-endpoint authentication methods are supported:
rfc6749-client-secret-basic(default) andrfc5849-oauth1-signature(HMAC-SHA256). See HTTP Settings File above for full configuration.
If you don't want to put credentials in HTTP_SETTINGS_FILE, pass httpcl::Config (C++) / HTTPConfig (Python) / HttpConfig (Java) directly to the client constructor.
| Feature | C++ Client | Python Client | Java Client | OAServer | zswag.gen |
|---|---|---|---|---|---|
| HTTP Basic / HTTP Bearer / Cookie API-Key / Header API-Key / Query API-Key | βοΈ | βοΈ | βοΈ | βοΈ(**) | βοΈ |
OAuth2[clientCredentials] |
βοΈ | βοΈ | βοΈ | βοΈ(**) | βοΈ |
OpenID Connect OAuth2[authorizationCode] OAuth2[implicit] OAuth2[password] |
βοΈ | βοΈ | βοΈ | βοΈ(**) | βοΈ |
(**) OAServer's actual support depends on your WSGI server (Apache/Nginx/...) wrapping the Flask app.