Target Output Spec
This document defines the target output for the sigil-stitch based code generation. It shows what generated code should look like, serving as the design reference for golden test updates.
This is a breaking change from pre-sigil-stitch output. Users who depend on FromJSON/ToJSON/instanceOfX helpers, ApiXxxRequest wrapper interfaces, or barrel index.ts must migrate.
Locked Decisions
| # | Decision | Rationale |
|---|---|---|
| 1 | Field terminator is ; (not ,) | Standard idiomatic TypeScript |
| 2 | Drop FromJSON / ToJSON / FromJSONTyped / ToJSONTyped | Interfaces are structural; runtime conversion is noise when field names already match |
| 3 | Drop instanceOfX | Structural checks in TS are a code smell; use discriminators or validators |
| 4 | Drop XxxPropertyValidationAttributesMap | Dead data; validation belongs in a schema library |
| 5 | Drop /* tslint:disable */ and /* eslint-disable */ headers | tslint is deprecated; keep @generated header |
| 6 | Validator is opt-in via --validator zod|valibot|none (default none) | Schemas ship as siblings, never force-imported |
| 7 | No models/index.ts barrel | Barrels defeat tree-shaking and slow down IDEs at scale |
| 8 | Every field is readonly | Generated DTOs are immutable views of wire data |
| 9 | Runtime is embedded, ~80 lines target (was 450) | Replace dead abstractions with a single request() helper |
| 10 | Per-tag API classes + top-level Client wrapper | Ergonomic new Client({ baseUrl }).pets.getPetById(...) |
| 11 | Client name derived from info.title via PascalCase + Client suffix | "Studio AMS — ASM" becomes StudioAmsAsmClient |
| 12 | Errors throw ApiError with ApiNetworkError and ApiResponseError subclasses | Idiomatic JS try/catch + instanceof |
| 13 | Methods take an options object; path params, query, body are named keys | Consistent regardless of param count |
Model File
Before (~90 lines)
/* tslint:disable */
/* eslint-disable */
import { type Category, CategoryFromJSON, CategoryToJSON, instanceOfCategory } from './Category';
export interface Pet {
category?: null | Category,
id?: number | null,
name: string,
}
export function instanceOfPet(value: unknown): value is Pet { ... }
export function PetFromJSON(json: any): Pet { ... }
export function PetToJSON(value?: Pet | null): any { ... }
After (~20 lines)
/**
* @generated by openapi-nexus. Do not edit.
*
* Petstore API — 1.0.0
*/
import type { Category } from './Category';
export interface Pet {
readonly category?: Category | null;
readonly id?: number | null;
readonly name: string;
}
readonlyon every fieldtypeimports only (no value imports)Array<T>becomesT[]for simple types
Enum
/** Pet status in the store */
export type PetStatus = 'available' | 'pending' | 'sold';
String-literal union type alias. No enum (not erasable-syntax compatible).
Validator Schemas (Opt-in)
Emitted only when --validator zod or --validator valibot is set. Lives next to the model file as Pet.schema.ts.
Zod
import { z } from 'zod';
export const PetSchema = z.object({
category: CategorySchema.nullable().optional(),
name: z.string(),
});
export const parsePet = (input: unknown) => PetSchema.parse(input);
Valibot
import * as v from 'valibot';
export const PetSchema = v.object({
category: v.optional(v.nullable(CategorySchema)),
name: v.string(),
});
export const parsePet = (input: unknown) => v.parse(PetSchema, input);
API File
Per-tag class with methods taking options objects:
export class PetApi {
constructor(private readonly config: ClientConfig) {}
async getPetById(options: { petId: number; signal?: AbortSignal }): Promise<Pet> {
return request<Pet>(this.config, {
method: 'GET',
path: `/pet/${encodeURIComponent(options.petId)}`,
signal: options.signal,
});
}
}
Removed: ApiXxxRequest wrappers, PetApiInterface, xxxRaw variants, typed response unions.
Client Wrapper
Name derived from info.title (e.g., PetstoreApiClient):
export class PetstoreApiClient {
readonly pets: PetApi;
readonly store: StoreApi;
constructor(options: PetstoreApiClientOptions = {}) {
const config: ClientConfig = { /* ... */ };
this.pets = new PetApi(config);
this.store = new StoreApi(config);
}
}
Runtime (~80 lines)
export interface ClientConfig {
baseUrl: string;
fetch: typeof fetch;
headers: Record<string, string>;
beforeRequest?: (init: RequestInit & { url: string }) => ...;
}
export class ApiError extends Error { }
export class ApiNetworkError extends ApiError { }
export class ApiResponseError<T = unknown> extends ApiError { }
export async function request<T>(config: ClientConfig, options: RequestOptions): Promise<T> { }
No BaseAPI, no response wrappers, no RequiredError.
Directory Layout
petstore/
├── apis/
│ ├── PetApi.ts
│ ├── StoreApi.ts
│ └── UserApi.ts
├── models/
│ ├── Pet.ts
│ ├── Pet.schema.ts ← only with --validator
│ └── PetStatus.ts
├── runtime.ts
├── Client.ts
├── index.ts ← re-exports Client + types
├── package.json
└── tsconfig.json
Go Side
Go keeps its current shape: per-tag client structs, struct + method receiver idiom, encoding/json marshaling. Same info.title-derived client naming. Concrete Go target goes in a follow-up spec doc.
Migration Table
| Old | New |
|---|---|
import { Pet, PetFromJSON } from 'petstore/models/Pet' | import type { Pet } from 'petstore/models/Pet' |
PetFromJSON(json) | Direct assign or parsePet(json) with --validator |
instanceOfPet(x) | parsePet(x) with validator, or write your own guard |
new PetApi(config).getPetByIdRaw(...) | new PetstoreApiClient({ baseUrl }).pets.getPetById(...) |
catch (e: ResponseError) { e.response } | catch (e) { if (e instanceof ApiResponseError) e.body } |