Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

#DecisionRationale
1Field terminator is ; (not ,)Standard idiomatic TypeScript
2Drop FromJSON / ToJSON / FromJSONTyped / ToJSONTypedInterfaces are structural; runtime conversion is noise when field names already match
3Drop instanceOfXStructural checks in TS are a code smell; use discriminators or validators
4Drop XxxPropertyValidationAttributesMapDead data; validation belongs in a schema library
5Drop /* tslint:disable */ and /* eslint-disable */ headerstslint is deprecated; keep @generated header
6Validator is opt-in via --validator zod|valibot|none (default none)Schemas ship as siblings, never force-imported
7No models/index.ts barrelBarrels defeat tree-shaking and slow down IDEs at scale
8Every field is readonlyGenerated DTOs are immutable views of wire data
9Runtime is embedded, ~80 lines target (was 450)Replace dead abstractions with a single request() helper
10Per-tag API classes + top-level Client wrapperErgonomic new Client({ baseUrl }).pets.getPetById(...)
11Client name derived from info.title via PascalCase + Client suffix"Studio AMS — ASM" becomes StudioAmsAsmClient
12Errors throw ApiError with ApiNetworkError and ApiResponseError subclassesIdiomatic JS try/catch + instanceof
13Methods take an options object; path params, query, body are named keysConsistent 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;
}
  • readonly on every field
  • type imports only (no value imports)
  • Array<T> becomes T[] 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

OldNew
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 }