Skip to content

ndsev/zswag

Repository files navigation

Zswag

CI codecov Quality Gate Status Release Coverage License

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.

Components

%%{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> &middot; 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[" &nbsp;Clients&nbsp; "]
      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[" &nbsp;Server&nbsp; "]
      oaserver["<b>OAServer</b> (Python only)<br/>request dispatch<br/>+ auth enforcement"]:::py
    end

    subgraph cppcore[" &nbsp;C++ core (shared with Python via pybind11)&nbsp; "]
      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[" &nbsp;Java core&nbsp; "]
      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[" &nbsp;External libraries&nbsp; "]
      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
Loading
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.

Per-language documentation

Detailed guides for each client + the server + the generator:

The shared YAML format for HTTP_SETTINGS_FILE (used by all three clients) is documented in the HTTP Settings File section below.

Quick start

Python

pip install zswag
from zswag import OAClient
import services.api as services

client = services.MyService.Client(OAClient("http://localhost:5000/openapi.json"))
client.my_api(services.Request(1))

C++

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));

Java (JVM)

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));

Java (Android)

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.

Setup details

Python users

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.

C++ users

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 users

Java 11+ source/target. The integration test depends on pip install zswag for its counterparty server. See docs/java.md.

Client environment variables

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.

HTTP Settings File

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.

Schema

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..64

A 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.

Environment variable references

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_TOKEN

Map keys are structural and are not expanded.

Scope matching

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/*" β€” matches https://api.foo.com/data (the dot before foo is 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: ...

OAuth2

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 requirements

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.

Precedence rules

When both http-settings.yaml and the OpenAPI spec specify a value:

  1. tokenUrl β€” http-settings.yaml overrides the spec's flows.clientCredentials.tokenUrl.
  2. refreshUrl β€” http-settings.yaml overrides the spec's flows.clientCredentials.refreshUrl.
  3. scope β€” http-settings.yaml overrides the per-operation security scopes.

Token endpoint authentication methods

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.

Spec fetch protection

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.

Debugging OAuth2

export HTTP_LOG_LEVEL=debug   # OAuth2 flow (mint/cache/refresh/auth method)
export HTTP_LOG_LEVEL=trace   # adds request/response bodies, signatures

Keychain integration

Storing 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

Disabling persistent settings programmatically

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.

Result code handling

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.

Swagger UI

If pip install "connexion[swagger-ui]" is available, OAServer exposes API docs at [/prefix]/ui.

OpenAPI Options Interoperability

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.).

HTTP method

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.

Request body

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 βœ”οΈ βœ”οΈ βœ”οΈ βœ”οΈ βœ”οΈ

URL Blob Parameter

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|binary

About format:

  • string and binary produce a raw URL-encoded buffer.
  • byte and base64 produce standard Base64.
  • base64url is URL-safe Base64.
  • hex is 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 βœ”οΈ βœ”οΈ βœ”οΈ βœ”οΈ βœ”οΈ

URL Scalar Parameter

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|binary

For 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> βœ”οΈ βœ”οΈ βœ”οΈ βœ”οΈ βœ”οΈ

URL Array Parameter

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|binary

The 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 βœ”οΈ βœ”οΈ βœ”οΈ βœ”οΈ βœ”οΈ

URL Compound Parameter

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> ❌️ ❌️ ❌️ ❌️ ❌️

Server URL base path

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

Authentication schemes

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 / Python HTTPConfig.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-key to 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) and rfc5849-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.

About

Access/Serve zserio services via REST/OpenAPI 🌍.

Topics

Resources

License

Stars

17 stars

Watchers

5 watching

Forks

Packages

 
 
 

Contributors