# 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
| 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)
```ts
/* tslint:disable */
/* eslint-disable */
import { type Category, CategoryFromJSON, CategoryToJSON, instanceOfCategory } from './Category';
export interface Pet {
name: string,
}
export function instanceOfPet(value: unknown): value is Pet { ... }
export function PetFromJSON(json: any): Pet { ... }
### After (~20 lines)
```ts
/**
* @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;
}
```
- `readonly` on every field
- `type` imports only (no value imports)
- `Array<T>` becomes `T[]` for simple types
## Enum
```ts
/** 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
```ts
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
```ts
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:
```ts
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`):
```ts
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)
```ts
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
| `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 }` |