TypeScript Generator Configuration
Full Example
[generators.typescript-fetch]
file_naming_convention = "PascalCase"
package_scope = "@myorg"
package_name = "my-api-client"
generate_package = true
ts_target = "ES2020"
ts_module = "ES2020"
ts_lib = ["ES2020", "DOM"]
generate_esm_config = true
include_build_scripts = true
emit_enum_constants = true
emit_type_guards = true
property_naming = "camelCase"
untagged_union_strategy = "firstMatch"
indent = " "
Options Reference
file_naming_convention
Controls the file naming style. One of "PascalCase", "camelCase", "kebab-case", "snake_case". Defaults to "PascalCase".
package_scope
NPM package scope prefix, e.g. "@myorg". If set, the generated package.json uses "name": "@myorg/my-api-client".
package_name
Override the generated package name. Defaults to the spec title converted to kebab-case.
generate_package
Whether to generate npm package files (package.json, tsconfig.json, tsconfig.esm.json). Defaults to true.
ts_target
TypeScript compiler target. Defaults to "ES2020".
ts_module
TypeScript module system. One of "commonjs", "ES2020", "ES2022", "ESNext". Defaults to "ES2020".
ts_lib
TypeScript compiler lib array. Accepts a TOML array or comma-separated string. Defaults to ["ES2020", "DOM"].
generate_esm_config
Whether to generate an ESM tsconfig (tsconfig.esm.json). Defaults to true.
include_build_scripts
Whether to include build scripts in package.json. Defaults to true.
emit_enum_constants
When true, emits a companion const object alongside each enum type alias:
export type ItemKind = 'BOOK' | 'MOVIE' | 'MUSIC';
export const ItemKind = {
BOOK: 'BOOK' as const,
MOVIE: 'MOVIE' as const,
MUSIC: 'MUSIC' as const,
};
Consumers can use the same import for both type annotations and runtime value comparisons:
import { ItemKind } from "@scope/package";
function getLabel(kind: ItemKind) {
switch (kind) {
case ItemKind.BOOK: return "Book";
// ...
}
}
Handles string, integer, number, and mixed-value enums. Quotes keys that aren’t valid JavaScript identifiers. Defaults to false.
emit_type_guards
When true, emits is* type guard functions alongside each tagged union type alias:
export type Shape = ({ kind: 'circle' } & Circle) | ({ kind: 'rectangle' } & Rectangle);
export function isCircle(value: Shape): value is ({ kind: 'circle' } & Circle) {
return value.kind === 'circle';
}
export function isRectangle(value: Shape): value is ({ kind: 'rectangle' } & Rectangle) {
return value.kind === 'rectangle';
}
The narrowing expression depends on the tagging style:
- Internal/Adjacent:
value.<field> === '<value>' - External:
'<value>' in value
Variants with “UNSPECIFIED” in the discriminator value, discriminator values equal to the field name, or bare string-literal content types are skipped. Defaults to false.
toolchain
Selects the type checking and build toolchain. One of "tsc" (default) or "vp".
"tsc"— bare TypeScript compiler. Generatestsconfig.jsononly. Build script:tsc."vp"— vite-plus. Generatesvite.config.tswithpackconfig for library output (ESM + .d.ts). Build script:vp pack. Format script:vp fmt. Check script:vp check.
When toolchain = "vp", the generated package.json includes vite-plus (^1.5.0) and typescript (^7.0.2) as dev dependencies, and scripts use vp pack (library mode) rather than vp build (app mode).
Vite+ manages Vite, Vitest, Rolldown, Oxfmt, Oxlint, oxlint-tsgolint, and tsdown as
one toolchain. Upgrading vite-plus upgrades these bundled components together.
Run vp --version after vp install in a generated project to inspect the resolved
component versions.
Vite+ 1.5.0 requires Node.js ^22.18.0 || ^24.11.0 || >=26.0.0.
After generating or regenerating a Vite+ client, run:
vp install
vp check
vp pack
The generator emits sources, JSON, and Markdown using the configured single-quote
style and standard Oxfmt layout. vp check validates formatting, lint rules, and
types directly on these files without modifying them. No formatter is required
between generation and validation. Use vp fmt (or npm run fmt) for subsequent
manual edits. The generated configuration excludes dist/, so checks also pass
after packing. Generation itself requires only Rust, without Node.js or Vite+.
[generators.typescript-fetch]
toolchain = "vp"
indent
Controls the indentation string used in generated TypeScript files. Accepts any string; common values are " " (2 spaces, default), " " (4 spaces), or "\t" (tabs).
[generators.typescript-fetch]
indent = " "
property_naming
Controls the naming convention for properties in generated interfaces. When set to "camelCase", generates dual-type model files:
- A
Name$Wireinterface preserving the original wire-format property names (snake_case, kebab-case, etc.) - A
Nameinterface with camelCase property names for ergonomic usage nameFromJSON(json: Name$Wire): Nameconverter functionnameToJSON(value: Name): Name$Wireconverter function
[generators.typescript-fetch]
property_naming = "camelCase"
Example output for a schema with snake_case properties:
export interface User$Wire {
readonly user_id: number;
readonly first_name: string;
readonly last_name: string;
}
export interface User {
readonly userId: number;
readonly firstName: string;
readonly lastName: string;
}
export function userFromJSON(json: User$Wire): User {
return {
userId: json.user_id,
firstName: json.first_name,
lastName: json.last_name,
};
}
export function userToJSON(value: User): User$Wire {
return {
user_id: value.userId,
first_name: value.firstName,
last_name: value.lastName,
};
}
Works with all schema kinds:
- Objects: dual interfaces + converters
- Tagged unions (internal, adjacent, external): dual type aliases + switch/if-chain converters
- Intersections (allOf): dual type aliases; compatible open objects share one conversion plan, while other intersections use spread-based converters
- Untagged unions (oneOf/anyOf): dual type aliases + member-selecting converters
Models, JSON request bodies, JSON multipart parts, successful responses, and lazy
error values use one value plan per generation. It projects public and wire types
and delegates conversion recursively through aliases, properties, arrays, and
dictionaries. An unchanged type does not remove a strict runtime check: even an
inline integer | number union must still detect overlapping matches.
Compound conversions and structural predicates use statically generated helper functions in internal runtime modules, not a runtime schema interpreter. They are not re-exported by the public barrel. Conversion helpers retain separate dependency modules so importing a model does not load unrelated model graphs. Helper inputs accept readonly arrays without widening public API results or changing model array mutability. Source layout is emitted directly; full Vite+ checks need no formatter preparation.
Defaults to "preserve" (no renaming, single interface, no converters).
untagged_union_strategy
Controls untagged-union conversion when property_naming = "camelCase":
"firstMatch"(default): select the first structurally matching member and call its converter. Concrete members precede unconstrained JSON/object/map fallbacks, with schema order preserved within each group. Unmatched values pass through unchanged rather than making imperfect specifications unusable."strict": use the same structural selection, but throwTypeErrorfor multiple concrete matches or no matching member. A raw fallback is considered only after the concrete members, so it does not itself create ambiguity."raw": do not select or convert union members. BothNameandName$Wireexpose the wire representation; pass original wire keys such asinput_schema, not ergonomic keys such asinputSchema. This is an explicit escape hatch, not an identity cast pretending to return a renamed model. This applies to the entire union subtree, including inline container unions and aliases; ordinary enclosing objects and sibling properties still convert normally.
[generators.typescript-fetch]
property_naming = "camelCase"
untagged_union_strategy = "strict"
The same choice is available through a CLI override:
openapi-nexus generate --input openapi.yaml --generators typescript-fetch \
--generator-config typescript-fetch.property_naming=camelCase \
--generator-config typescript-fetch.untagged_union_strategy=strict
Selection checks generated value kinds, required own fields, declared property
shapes, typed additional values, and scalar literal/enum values in the appropriate
wire or ergonomic naming direction. Named fields are excluded from the additional
value check, even though TypeScript widens the dictionary’s index type to include
them.
Nested enum constraints intersect at their original nullable boundaries; an outer
enum does not replace or widen an inner enum.
Untyped scalar enums match their finite values without treating the legacy
string type fallback as an authored constraint.
Arrays and dictionaries inspect their element/value shapes; dictionary keys and
external variant tags stay verbatim. This is conversion dispatch, not full JSON
Schema validation: formats, numeric bounds, and other validation keywords are
not checked. Productive recursive references use statically generated matching
functions rather than becoming unrestricted guards. These helpers are private to
the generated runtime, not public barrel exports; selected member converters
still handle recursive payload conversion. Same-value circular type aliases are
not made representable by this mechanism.
Binary properties match their generated Blob | File representation rather
than strings. Adjacent-tagged unit variants do not require a content property;
payload variants retain their content checks.
A named nested union can provide both concrete matches and a raw fallback.
Its concrete matches take priority over fallbacks, and both paths delegate to
the nested converter so its strict ambiguity policy is not bypassed. Nullable
and allOf wrappers do not turn an unrestricted map/JSON fallback into a
concrete match. A dictionary of open nested unions retains a concrete selection
path when at least one entry matches concretely, even when other entries are
opaque. Empty or entirely opaque dictionaries remain fallbacks. Both paths
delegate through the dictionary converter, preserving inner strict errors.
Matching is pure: select/count the original member candidates before running a converter, then invoke only the selected delegate. A nested strict error propagates instead of triggering a retry with another member. Repeated nullable wrappers are idempotent; they do not create extra candidates or promote an opaque fallback. An explicit null alternative does not bypass ambiguity among other nullable members, or suppress a selected nested converter’s strict error. An unrestricted fallback beside authored nullable members does not erase those members’ concrete null matches. Null permission added only by an opaque wrapper remains a fallback rather than creating an extra concrete candidate. Property, array and dictionary nullable wrappers delegate null when the inner value already accepts it. Null admitted only by the outer wrapper still bypasses a non-nullable inner converter. This remains true when property nullability was extracted through scalar enum metadata: conversion restores the wrapper rather than passing null directly to the nonnullable delegate.
Inline unions inside a selected array or dictionary member use the same
selection policy recursively, rather than returning their contents unchanged.
A single-member allOf alias delegates without spreading its result,
preserving primitive, array, and null values too.
For example, a mixed ClientTool | SearchTool union can distinguish a required
input_schema / inputSchema field from a search member’s required literal
type, then convert inputSchema and maxUses to input_schema and max_uses.
Conversions also work through containing arrays, dictionaries, and model aliases.
Declared additional-property behavior remains owned by the selected member’s
converter. Direct tagged payloads and optional type guards use the same type and
conversion projections. Preserve mode does not add JSON converters.
Tagged declarations and guards namespace-qualify built-in utilities when a model
name shadows them, including open external wrappers. A tagged schema named
Record or Readonly keeps its public model and converter names; its generated
utility types use globalThis.Record or globalThis.Readonly instead. Planned
dictionaries and structural matching casts use the same qualification.
A required JSON body must be supplied, but a schema-permitted null is a value,
not a missing body. Such bodies serialize as JSON null; omitted bodies still
throw RequiredError before sending a request.
Optional JSON bodies skip conversion only when omitted (undefined). A supplied
null uses the same planned conversion and strict ambiguity checks as a required
body or direct model conversion.
Additional properties
Objects declaring additionalProperties expose a readonly string dictionary in
both naming modes, including objects that also declare named fields. Typed map
values keep their model types; arbitrary JSON values use unknown.
TypeScript’s string index covers named fields too. For mixed schemas, the index
value type therefore includes the additional-value type, every declared field
type, and undefined when a declared field is optional. Named fields retain their
more precise types. For example, an optional known string plus string extensions
extends Readonly<Record<string, string | undefined>>.
In camelCase mode, both converters preserve extension keys verbatim. Typed model
values are converted recursively, including arrays, nullable values, and nested
maps; opaque JSON contents are not renamed. Omitted optional fields stay absent
on these open objects. Object/map allOf composition and open tagged variants
also retain their extensions. additionalProperties: false and unspecified
additional-property behavior are unchanged.
Compatible allOf dictionary contracts are narrowed together (for example,
true with string uses string values), and named fields are planned across
all members before conversion. This includes JSON unions that render as unknown:
intersecting an arbitrary-JSON dictionary with a model dictionary converts the
additional values as that model, without treating other members’ named fields as
extensions. Their public intersection aliases retain the original member types
inside Readonly<...> in preserve, ergonomic, and wire declarations.
Aliases of models, arrays, maps, and nullable
values receive matching wire aliases and converters, including alias chains.
External tag keys stay verbatim while both the referenced payload and any typed
outer extension values are converted; each closed alternative remains closed.
An extension can collide with a renamed field: {known_field: 'fixed', knownField: 'extension'} must still expose value.knownField === 'fixed'. The
converter retains the colliding extension under the enumerable symbol
Symbol.for('openapi-nexus.additionalProperties'); nameToJSON restores it.
Object spread preserves this metadata. Serialize through nameToJSON, not
JSON.stringify(value) or a JSON-based clone, which omits symbol metadata. Use
Name$Wire or property_naming = "preserve" when direct access to all original
wire keys is preferable.
Barrel Exports
When either feature is enabled, models/index.ts automatically emits value re-exports:
// Enum const: single line covers both type and value
export { ItemKind } from './ItemKind';
// Tagged union: separate type and guard re-exports
export type { Shape } from './Shape';
export { isCircle, isRectangle } from './Shape';