openapi-typescript
openapi-typescript copied to clipboard
v7 Preview đ
v7 Preview
openapi-typescript@7 is now in preview! đ The major additions to this library are:
- ⨠Built-in validation using the Redoc CLI â no more needing an external tool
- ⨠Respecting
redocly.yamlfor API config (define your schemas there in one place, and useopenapi-typescriptto generate TypeScript without having to manage a separate config) - ⨠Addition of the
--enumflag for generating true TypeScript enums instead of unions (default behavior still unions) - đ A mini-rewrite to use the TypeScript AST instead of string mashing
Full Changelog
Features
-
⨠Feature: automatically validate schemas with Redocly CLI (docs). No more need for external tools to report errors! đ
- By default, it will only throw on actual schema errors (uses Redoclyâs default settings)
- For stricter linting or custom rules, you can create a redocly.yaml config
-
⨠Feature: bundle schemas with Redocly CLI
- Any options passed into your redocly.yaml config are respected
-
⨠Feature: add
enumoption to export top-level enums from schemas (#941) -
⨠Feature: add
formatOptionsto allow formatting TS output -
⨠Feature: header responses add
[key: string]: unknownindex type to allow for additional untyped headers -
⨠Feature: allow configuration of schemas via
apiskey in redocly.config.yaml. See docs for more info.
Breaking Changes
-
â ď¸ Breaking: The Node.js API now returns the TypeScript AST for the main method as well as
transform()andpostTransform(). To migrate, youâll have to use thetypescriptcompiler API:+ import ts from "typescript"; + const DATE = ts.factory.createIdentifier("Date"); + const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); const ast = await openapiTS(mySchema, { transform(schemaObject, metadata) { if (schemaObject.format === "date-time") { - return schemaObject.nullable ? "Date | null" : "Date"; + return schemaObject.nullable + ? ts.factory.createUnionTypeNode([DATE, NULL]) + : DATE; } }, };Though itâs more verbose, itâs also more powerful, as now you have access to additional properties of the generated code you didnât before (such as injecting comments).
For example syntax, search this codebae to see how the TypeScript AST is used.
Also see AST Explorerâs
typescriptparser to inspect how TypeScript is interpreted as an AST. -
â ď¸ Breaking: Changing of several CLI flags and Node.js API options
- The
--auth,--httpHeaders,--httpMethod, andfetch(Node.js-only) options were all removed from the CLI and Node.js API- To migrate, youâll need to create a redocly.yaml config that specifies your auth options in the http setting
- You can also set your fetch client in redocly.yaml as well.
--immutable-typeshas been renamed to--immutable--support-array-lengthhas been renamed to--array-length
- The
-
â ď¸ Breaking: Most optional objects are now always present in types, just typed as
:never. This includes keys of the Components Object as well as HTTP methods. -
â ď¸ Breaking: No more
externalexport in schemas anymore. Everything gets flattened into thecomponentsobject instead (if referencing a schema object from a remote partial, note it may have had a minor name change to avoid conflict). -
â ď¸ Breaking
defaultNonNullableoption now defaults totrue. Youâll now need to manually setfalseto return to old behavior. -
â ď¸ Breaking: Remove globbing schemas in favor of
redocly.yamlconfig. Specify multiple schemas with outputs in there instead. See Multiple schemas for more info.
And dozens of small performance improvements and quality fixes.
Give it a try today with the @next flag:
npm i openapi-typescript@next
Testers wanted đ
Will pin this issue for a while while people kick the tires and report bugs while a release candidate is developed. For many people, it should be a drop-in replacement; for others, it will only require a few minor changes.
If people could report any errors/issues they face while trying this, thatâd be much appreciated. Any issues posted here or as separate issues will help patch holes in the docs site.
Great to hear that openapi-typescript will support external refs! Thanks a lot for that :)
I did quick test with our project and ran into a typing issue.
When extending a model using allOf...
PurchasableProduct:
type: object
properties:
price:
$ref: '#/components/schemas/Price'
required:
- price
allOf:
- $ref: '#/components/schemas/Product'
... the generated code does not compile:
Our backend colleagues use the same OpenAPI file using the openapi-generator kotlin-spring plugin, so it seems to a be valid spec.
Here's an example repo to reproduce the issue:
https://github.com/luchsamapparat/openapi-typescript-7-bug
@luchsamapparat the generated code seems to be working correctly; the error seems to be coming from the fact your schema specifies price as a required property but no such property exists. Does removing that fix the error?
If you have another look at my example, the price property is not only defined as required, but also as a property directly below the properties: line.
You can copy and paste the full example OpenAPI YAML from my repo into the Swagger Editor which parses it correctly:
Would be nice to support x-enum-varnames property.
UPD: Well, I think I did it. Check the PR https://github.com/drwpow/openapi-typescript/pull/1374
Thanks to @joostme who pointed out that there's a workaround for my problem:
https://github.com/luchsamapparat/openapi-typescript-7-bug/commit/638870cbd25bcb6bfc775b781bfc18721ba52c89
running with --immutable flag seems to generate invalid ts: export readonly interface paths {}
see:
npx openapi-typescript@next https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml --immutable | head
error:
'readonly' modifier can only appear on a property declaration or index signature.ts(1024)
@stefanprobst good catch đ! Will add some immutable tests to make sure the types are valid.
Sidenote: I did learn through the v7 rewrite that TypeScriptâs AST does let you generate invalid TS without stopping you. Which is fun đ
Upgraded to v7 without issues. Generated file looks neat and tidy - especially when using external file refs. Thank you for this awesome job!
@drwpow may it be the case that there were some changes in handling of the | undefined portion of additionalProperties typings? I think, I am noticing now something of it.
Related issues: #1018, #1267
Input:
CityLinks:
type: object
required: [socials]
properties:
socials:
type: object
additionalProperties:
type: string
Output
CityLinks: {
socials: {
// There was no `| undefined` in later v6 versions
[key: string]: string | undefined;
};
};
Why is it that we have a ton of never now in v7 whereas they did not exist in v6? it makes the generated types a fair bit more verbose to read.
Why is it that we have a ton of
nevernow in v7 whereas they did not exist in v6?
Great question! That actually has to do with how TS handles discriminated unions within other tools like openapi-fetch. For example, In v6, comparing the response objects between 2 or more endpoints would yield a very complex union because each would have different combinations of request methodsâoften each would be a completely different permutation of keys. And to TS, comparing objects with different keys are âapples to orangesâ even if we donât intend for them to be.
But when we add never in v7 and TS sees every request object has the same keys, it now knows itâs comparing âapples to applesâ and can make better inference decisions (example). And as a result, writing type utilities becomes easier and safer to do (some edge cases were fixed in openapi-fetch by this change).
So it not only helps TS do its job better, and makes more advanced utilities built off openapi-typescript better, but I think the never additions are a more âcorrectâ type in generalâthey make it clear where there could be an addition to your schema according to the spec, but it has been decidedly opted out of (or is not used yet).
Gotcha, this is super helpful to know, thanks for the link to the reference article too! does this result also in a speed up in the auto complete type hints as a result?
hey i think i have found an issue with v7. i've dug into it and i have found the source of the issue...
a number of my endpoints can return a 204 status, which has no content. for context, my API is following the JSON:API standard: https://jsonapi.org/format/#crud-creating-responses-204.
so the generated types look like this:
responses: {
201: {
headers: {
[name: string]: unknown;
};
content: {
// ...
};
};
204: {
headers: {
[name: string]: unknown;
};
content?: never; // <-- this is the source of the issue
};
// ...
};
the potentially undefined content for 204 trips up the SuccessResponse type utility, causing it to resolve to never.
Here's a typescript playground which contains an abridged version of my generated schema and some code at the bottom to demonstrate the issue. if you remove the ? from the 204 content you will see that the you get the correct type. I am guessing a NonNullable somewhere in the type utilities will fix this
I do not have this issue with v6.7.0
Edit: for completeness, the 204 response looks like this when generated with 6.7.0. here it is not potentially undefined:
204: {
content: never;
};
Another issue sorry!
In v7, if a request body parameter has a default value, it is treated as required/non-optional in the generated types.
requestBody:
content:
application/vnd.api+json:
schema:
required:
- data
properties:
data:
type: object
required:
- type
additionalProperties: false
properties:
type:
type: string
id:
type: string
attributes:
type: object
properties:
first_name:
type: string
maxLength: 100
last_name:
type: string
maxLength: 100
email:
type: string
format: email
maxLength: 100
language:
type: string
nullable: true
default: en
maxLength: 20
required:
- email
- initials
this will generate a type like:
requestBody?: {
content: {
"application/vnd.api+json": {
data: {
type: string;
id?: string;
attributes?: {
first_name?: string;
last_name?: string;
/** Format: email */
email: string;
/** @default en */
language: string | null; // <-- â not optional?
};
};
};
};
};
i believe this is incorrect, given the openapi spec on default (emphasis mine):
Use the default keyword in the parameter schema to specify the default value for an optional parameter. The default value is the one that the server uses if the client does not supply the parameter value in the request
in v6, the generated type is as expected:
requestBody?: {
content: {
"application/vnd.api+json": {
data: {
type: string;
id?: string;
attributes?: {
first_name?: string;
last_name?: string;
/** Format: email */
email: string;
/** @default en */
language?: string | null;
};
};
};
};
};
here's a stackblitz demonstrating the issue. it will output to schema.d.ts when it starts up https://stackblitz.com/edit/stackblitz-starters-dh6zug
@WickyNilliams thanks for flagging! I will investigate that specifically. There are other breaking changes Iâm tracking that will require a breaking version change for openapi-fetch (but fortunately the breaking change is less code since 7.x makes inference simpler/safer). Right now itâs known openapi-fetch only works with 6.x, and an upcoming breaking version of openapi-fetch will be released for 7.x (as a fast follow)
@WickyNilliams also for the default issue, that seems like a bug. 7.x enables --default-non-nullable by default so itâs intended to have that behavior. But is just missing some relevant tests it seems.
Right now itâs known openapi-fetch only works with 6.x,
is this mentioned in the docs? i looked in multiple before trying out v7, and since i didn't spot anything i assumed that they were compatible. might be worth sticking a big banner somewhere :D or perhaps list openapi-typescript as a peer dependency with a v6 range? but i guess peer deps can be their own can of worms
i have pinned to v6 in the meanwhile. glad to hear v7 enables some simplification of the fetcher!
7.x enables
--default-non-nullableby default so itâs intended to have that behavior. But is just missing some relevant tests it seems.
ah i see that in the full changelog above now. the docs site itself still says it defaults to false by the way. is there some explanation somewhere for that change? in my (lacking-in-context and likely uninformed) opinion anything that diverges from what the schema explicitly defines should be opt-in. so i'd be curious on the background.
might be worth sticking a big banner somewhere :D
A note on the docs would be a good idea! Will add that.
is there some explanation somewhere for that change?
Mostly from this issue, and if you search youâll find other instances where it seemed like more users were expecting this behavior than not. And it seems like the spec suggests this should be the default behavior? Open to feedback if Iâm misunderstanding something; I had just flagged this as a deviation from the specification (and with a spec deviation + multiple issues raised about the default behavior seems like a good hint to change it).
in my reading i think there is a sutble difference between inputs and outputs. for outputs/responses, the set of non-optional fields should be the union of required and those with defaults. for inputs/requests, it is only those which are required.
reading the linked issue, i think that's what they're asking also. see the second bullet point (emphasis theirs):
But, when the consumer is writing the value can be provided or not so that the generated type should also be optional instead of required
i found this related issue https://github.com/drwpow/openapi-typescript/issues/584 which i think is talking about default values in responses being required.
there's also this comment which says:
So if a default value is set on a request parameter, it cannot be interpreted as a required parameter.
Ah thank you that is a really great distinction. Will also add that to the test suite. That may mean some nuances of --default-non-nullable may change (maybe; maybe not). But breaking versions are the opportunities to make these improvements and dig deeper on how everything works.
For sure! Please post back here as and when something changes
I need to to create types from multiple remote sources but I keep getting TypeError [ERR_INVALID_FILE_URL_HOST]: File URL host must be "localhost" or empty on linux error. Tried with local files and still the same issue. This is the redocly.yaml :
@ogjonixyz Thank you! Will track that as well. Appreciate all the testing and reporting issues while itâs still in beta.
Hello. Got some issue. In yaml we have refs with schema field. And lib try to parse schema as a valid key to get data, but should ignore it in response
responses:
'200':
description: Info about foo
content:
application/json:
schema:
$ref: '../../schemas/foo-api/foo
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
content?: paths["/foo/{fooId}"]["get"]["responses"]["200"]["content"]["application/json"]["schema"][];
} & {
count?: number;
};
};
};
}
@omgpiu could you make a separate issue please? Ideally with your full schema, version of openapi-typescript, and the full generated response. Just from the info provided Iâm not sure whatâs going on.
@ogjonixyz I canât say for sure, but it looks like in your redocly.yaml config, youâre not matching the correct letter case for the file? In many Linux filesystems, theyâre case-sensitive. And that error message usually happens when youâre pointing to a nonexistent file
Released a new version of @next with a few bugfixes. Still going to hunt down some remaining bugs and add a few more tests before releasing a RC. But itâs looking close!
Hello! I noticed that with v7 you can end up with an invalid ts file if you have multiple paths that use the same operationId. For example using the following schema:
{
"openapi": "3.0.0",
"paths": {
"/v1/path": {
"post": {
"operationId": "Operation1",
"parameters": [],
"responses": { "200": { "description": "" } },
"tags": ["tag"]
}
},
"/v2/path": {
"post": {
"operationId": "Operation1",
"parameters": [],
"responses": { "200": { "description": "" } },
"tags": ["tag"]
}
}
},
"components": {
"schemas": {}
}
}
you get an additional duplicated Operation1 prop in the operations interface. I managed to remove the duplicates by filtering them out from the ts.Node[] that the default export function returns. Are there any plans on changing this behaviour?
FYI the swagger docs have this to say
operationIdis an optional unique string used to identify an operation. If provided, these IDs must be unique among all operations described in your API.
https://swagger.io/docs/specification/paths-and-operations/
@ogjonixyz I canât say for sure, but it looks like in your
redocly.yamlconfig, youâre not matching the correct letter case for the file? In many Linux filesystems, theyâre case-sensitive. And that error message usually happens when youâre pointing to a nonexistent file
Ah sorryâI encountered the bug. That bug is when you donât explicitly pass in a --redoc flag and it tries to locate one (and when it does, it only returns a partial path and not a full one, hence the URL error), and we werenât testing for that. Adding a test for that and pushing a fix.