Code Generation
Code generation turns a frozen schema into typed client stubs so callers get autocomplete, type checking, and inline doc comments without writing any glue code by hand.
What Gets Generated
For each language target, the generator produces one file per namespace plus a shared types file:
| Language | Types file | Client file per namespace | Index / re-export |
|---|---|---|---|
| TypeScript | types.ts |
<Namespace>Client.ts |
index.ts |
| Python | types.py |
<namespace>_client.py |
__init__.py |
| C# | Types.cs |
<Namespace>Client.cs |
Generated.cs |
Private functions are never emitted. Public and internal functions both appear in the generated stubs.
Running the Generator
Pass a schema file and a target language to saikuro-codegen:
saikuro-codegen --schema schema.json --lang typescript --out ./generated
saikuro-codegen --schema schema.json --lang python --out ./generated
saikuro-codegen --schema schema.json --lang csharp --out ./generated
Generate for multiple languages at once (each gets its own subdirectory):
saikuro-codegen --schema schema.json \
--lang typescript --lang python --lang csharp \
--out ./generated
# writes ./generated/typescript/, ./generated/python/, ./generated/csharp/
--out is created if it does not exist. Pass --quiet to suppress progress output.
TypeScript Example
Given this schema:
{
"version": 1,
"namespaces": {
"math": {
"functions": {
"add": {
"args": [
{ "name": "a", "type": { "kind": "primitive", "type": "i64" }, "optional": false },
{ "name": "b", "type": { "kind": "primitive", "type": "i64" }, "optional": false }
],
"returns": { "kind": "primitive", "type": "i64" },
"visibility": "public",
"capabilities": [],
"idempotent": false,
"doc": "Add two integers."
}
}
}
},
"types": {}
}
The generator produces MathClient.ts:
// AUTO-GENERATED by saikuro-codegen. DO NOT EDIT.
import { Client } from 'saikuro';
export class MathClient {
constructor(private readonly client: Client) {}
/** Add two integers. */
async add(a: number, b: number): Promise<number> {
return this.client.call('math.add', [a, b]);
}
}
Use it like this:
import { Client } from 'saikuro';
import { MathClient } from './generated/MathClient';
const client = new Client();
await client.connect();
const math = new MathClient(client);
const result = await math.add(1, 2);
console.log(result); // 3
Python Example
The generator produces math_client.py:
# AUTO-GENERATED by saikuro-codegen. DO NOT EDIT.
from saikuro import Client
class MathClient:
def __init__(self, client: Client) -> None:
self._client = client
async def add(self, a: int, b: int) -> int:
"""Add two integers."""
return await self._client.call('math.add', [a, b])
Use it:
from saikuro import Client
from generated import MathClient
client = Client()
await client.connect()
math = MathClient(client)
result = await math.add(1, 2)
print(result) # 3
C# Example
The generator produces MathClient.cs:
// AUTO-GENERATED by saikuro-codegen. DO NOT EDIT.
using Saikuro;
public sealed class MathClient
{
private readonly Client _client;
public MathClient(Client client) => _client = client;
/// <summary>Add two integers.</summary>
public Task<long> AddAsync(long a, long b)
=> _client.CallAsync<long>("math.add", new object[] { a, b });
}
Use it:
var client = new Client();
await client.ConnectAsync();
var math = new MathClient(client);
var result = await math.AddAsync(1, 2);
Console.WriteLine(result); // 3
Custom Types
Types defined in the types section of the schema become dataclasses, interfaces, or records in the generated code.
Schema:
{
"version": 1,
"types": {
"User": {
"kind": "record",
"fields": {
"id": { "type": { "kind": "primitive", "type": "string" }, "optional": false },
"name": { "type": { "kind": "primitive", "type": "string" }, "optional": false }
}
}
},
"namespaces": {
"users": {
"functions": {
"get": {
"args": [{ "name": "id", "type": { "kind": "primitive", "type": "string" }, "optional": false }],
"returns": { "kind": "named", "name": "User" },
"visibility": "public",
"capabilities": [],
"idempotent": false
}
}
}
}
}
TypeScript generates:
// types.ts
export interface User {
id: string;
name: string;
}
// UsersClient.ts
import { User } from './types';
// ...
async get(id: string): Promise<User> { ... }
Python generates:
# types.py
from dataclasses import dataclass
@dataclass
class User:
id: str
name: str
C# generates:
// Types.cs
public record User(string Id, string Name);
Visibility Filtering
The visibility field on each function controls whether it appears in generated stubs:
| Visibility | Generated |
|---|---|
public |
Yes |
internal |
Yes |
private |
No |
Private functions are never part of the public surface and are excluded from all output.
doc Comments
If a function has a doc field in the schema, the generator emits it as a doc comment in the target language:
- TypeScript:
/** ... */ - Python:
"""...""" - C#:
/// <summary>...</summary>
This is the only place doc has any effect. It has no impact on routing or validation at runtime.
Using Codegen in a Build Pipeline
The cleanest approach is to commit the schema to your repo and regenerate stubs as part of your build:
# Extract the schema from your provider (TypeScript example)
npx saikuro extract --provider src/math-provider.ts --out schema.json
# Generate stubs for each consumer language
saikuro-codegen --schema schema.json --lang python --out clients/python/generated
saikuro-codegen --schema schema.json --lang csharp --out clients/csharp/Generated
Add the generated files to version control so consumers don’t need the codegen tool installed.
Next Steps
- Schema Reference: All schema fields, including
idempotent,capabilities, and custom types - Language Adapters: Provider and client API for each language