Tutorial: writing a generator from scratch
This walks through building a small but real generator - OpenAPI schemas → TypeScript
interfaces - step by step. Every command below was actually run against this repo's
test/resources/petstore.yaml fixture; the output shown is real, not illustrative.
Prerequisites: a built openapi-yagen binary (see the root README.md for build
instructions) and a spec to generate from - we use test/resources/petstore.yaml throughout.
1. An empty generator
Create a new directory for the generator with two files:
generator.yml:
name: openapi_to_ts
description: Tutorial generator - OpenAPI schemas to TypeScript interfaces
mainScriptPath: main.js
main.js:
dump(Object.keys(schema.components.schemas));
Run it:
openapi-yagen g -o out -g ./openapi_to_ts -c test/resources/petstore.yaml
Dump: [Error,Pet,Pets]
schema is the fully-resolved OpenAPI document (see
javascript-api.md) - schema.components.schemas is a plain
object keyed by schema name, exactly like the spec's own components.schemas.
2. Your first template
Generators write files via renderTemplate (see
javascript-api.md). Add a template with static content
first, to see the plumbing work before adding real logic:
models.ts.j2:
// Generated by openapi_to_ts - do not edit by hand.
export interface Placeholder {
id: string;
}
main.js:
renderTemplate("models.ts.j2", {}, "models.ts");
Running the same command as above now produces out/models.ts with exactly that content.
3. Real data: object schemas → interfaces
Now build the actual model list in main.js, using kindOf
to pick out object schemas and map each property's OpenAPI type to a TypeScript one:
function mapType(propSchema) {
if (kindOf(propSchema) === "Enum") return propSchema.enum.map((v) => JSON.stringify(v)).join(" | ");
if (propSchema.type === "integer" || propSchema.type === "number") return "number";
if (propSchema.type === "string") return "string";
if (propSchema.type === "boolean") return "boolean";
return "unknown";
}
const models = [];
for (const [name, s] of Object.entries(schema.components.schemas)) {
if (kindOf(s) !== "Object") continue;
const required = new Set(s.required || []);
const properties = Object.entries(s.properties || {}).map(([propName, propSchema]) => ({
name: propName,
type: mapType(propSchema),
required: required.has(propName),
}));
models.push({ name, properties });
}
renderTemplate("models.ts.j2", { models }, "models.ts");
{name, properties} is a plain object built in JS before the template is rendered - not the raw
schema object. That matters: kindOf (and nameOf, used later) only work on schemas that still
have their original JS identity, which is gone once something crosses into a template (see
javascript-api.md).
4. The template renders interfaces
// Generated by openapi_to_ts - do not edit by hand.
{% for model in models -%}
export interface {{ model.name }} {
{% for p in model.properties -%}
{{ p.name }}{% if not p.required %}?{% endif %}: {{ p.type }};
{% endfor -%}
}
{% endfor -%}
(The -%} trims are explained in templating.md - without
them you'd get a blank line after every tag.)
openapi-yagen g -o out -g ./openapi_to_ts -c test/resources/petstore.yaml
cat out/models.ts
// Generated by openapi_to_ts - do not edit by hand.
export interface Error {
code: number;
message: string;
}
export interface Pet {
id: number;
name: string;
tag?: string;
}
(This minimal example doesn't reindent the property loop, hence the flat output - see
templating.md for indent()/{% filter %}, which the
built-in generators use to emit correctly nested code directly. Either reach for those in your own
templates, or pipe output through a formatter via the -p/--post-process flag in the root
README.md.)
5. A generator variable
Add a readonly variable so callers can opt into readonly properties without forking the
generator:
generator.yml:
name: openapi_to_ts
description: Tutorial generator - OpenAPI schemas to TypeScript interfaces
mainScriptPath: main.js
variables:
- name: readonly
description: Generate readonly properties
defaultValue: "false"
main.js (last line only):
renderTemplate("models.ts.j2", { models, readonly: vars.readonly === "true" }, "models.ts");
models.ts.j2 (property line only):
{% if readonly %}readonly {% endif %}{{ p.name }}{% if not p.required %}?{% endif %}: {{ p.type }};
openapi-yagen g -o out -g ./openapi_to_ts -c test/resources/petstore.yaml -v "readonly=true"
export interface Pet {
readonly id: number;
readonly name: string;
readonly tag?: string;
}
Omit -v readonly=... and it falls back to defaultValue: "false" - properties come out
non-readonly, as in step 4.
6. Arrays and enums
Two more schema shapes, both handled the same way: classify with kindOf, then look up the right
TypeScript spelling.
Arrays (Pets in petstore.yaml is type: array, items: $ref: Pet) become a type alias.
nameOf recovers Pet from the (already $ref-resolved) items schema:
const aliases = [];
for (const [name, s] of Object.entries(schema.components.schemas)) {
const kind = kindOf(s);
if (kind === "Object") {
// ... same as step 3 ...
} else if (kind === "Array") {
const itemType = nameOf(s.items) || mapType(s.items);
aliases.push({ name, itemType });
}
}
renderTemplate("models.ts.j2", { models, aliases, readonly: vars.readonly === "true" }, "models.ts");
{% for alias in aliases -%}
export type {{ alias.name }} = {{ alias.itemType }}[];
{% endfor -%}
openapi-yagen g -o out -g ./openapi_to_ts -c test/resources/petstore.yaml
cat out/models.ts
export type Pets = Pet[];
(appended after the two interfaces from step 4 - full file unchanged otherwise)
Enums were actually already handled - look back at mapType in step 3: if (kindOf(propSchema) === "Enum") return propSchema.enum.map((v) => JSON.stringify(v)).join(" | "). petstore.yaml
doesn't have one, but adding
status:
type: string
enum: [available, sold]
to Pet's properties and regenerating produces exactly what you'd expect:
export interface Pet {
id: number;
name: string;
status?: "available" | "sold";
tag?: string;
}
7. Where to go next
This tutorial's generator is deliberately small. For fuller, real-world examples, see
generators/:
sample_cpp_models_generator- the same idea as this tutorial, targeting C++ structs instead of TypeScript interfaces.kotlin_ktor_client_generator/kotlin_ktor_server_generator- full Kotlin/Ktor client + server generators covering operations (collectOperations), constraints (constraintsOf), discriminated and undiscriminatedoneOf/anyOf, and more.
If you want your own generator to live in this repo's generators/ collection with a real,
self-contained test suite (not just "does it compile" - actually generating, compiling, and
running the output), see generators/README.md for the
README.md/src//test/ convention and how the Kotlin generators' test/ directories are built.