Open kollolsb opened 1 year ago
Currently, openapi-typescript ignores format
because in most instances it’s subjective how you’d want it interpreted. For example, for { "type": "string", "format": "date" }
, some may prefer it parsed as a Date
while others prefer it left as a string
because that’s what the JSON receives (there also can be a bit of ambiguity when it comes to a mutable Date
and timezones as well that a readonly string may make clearer).
In this example, I believe string
would be the correct default typing because that’s not the top-level response, right? Wouldn’t the object be initially parsed as JSON, which would make it impossible for File
to happen?
That’s why the transform API exists—to give openapi-typescript additional insight into how your APIs may be transformed as they’re consumed—whether or not you parse Date
, Blob
, File
, etc. Otherwise, this library may be at best overreaching telling you how to parse your schema, or at worst lying to you, giving incorrect types for things.
That said, if others prefer { "type": "string", "format": "file" }
to be typed as File
by default, with the default changeable with the transform API, I’d be open to that.
Thanks for pointing that out, I was able to use the transform API to switch the property type to File. I think what might help is mentioning this approach as a tip in the introduction section of the documentation; I had avoided the Node JS section because I thought it didn't apply for my use case. Also perhaps an inferType
flag could be provided that covers the most common use cases like binary
, date
etc.
Thanks again for the quick response. I really like your library, will definitely recommend it to others, great job!
I agree that there should be a prominent line in the docs noting that openapi-typescript
tries to be conservative in its schema interpretation, and it's expected that you may need to use the Node.js API for some things. Calling out date
and binary
as common cases would also make sense.
I spent the last 3 hours trying to figure this out. Part of it was the openapi-fetch
not supporting the proper file Content-Type, but the generated schema wasn't helping me figure this out either.
I'm going with the bodySerializer
+ transformApi
fix for now.
I'd love it if Blob
was the default file type though. I'm glad this made its way into the docs, but I didn't see it and spent a lot of time trying to figure out what was wrong.
@drwpow
in most instances it’s subjective how you’d want it interpreted
Is it true tho? This is what the docs say about content type for a part in multipart body:
For 3.0.3
:
When passing in
multipart
types, boundaries MAY be used to separate sections of the content being transferred — thus, the following defaultContent-Type
s are defined formultipart
:
- If the property is a primitive, or an array of primitive values, the default Content-Type is
text/plain
- If the property is complex, or an array of complex values, the default Content-Type is
application/json
- If the property is a
type: string
withformat: binary
orformat: base64
(aka a file object), the default Content-Type isapplication/octet-stream
The Content-Type for encoding a specific property. Default value depends on the property type: for
string
withformat
beingbinary
–application/octet-stream
; for other primitive types –text/plain
; forobject
-application/json
; forarray
– the default is defined based on the inner type. The value can be a specific media type (e.g.application/json
), a wildcard media type (e.g.image/*
), or a comma-separated list of the two types.
or 3.1.0
:
When passing in
multipart
types, boundaries MAY be used to separate sections of the content being transferred – thus, the following defaultContent-Type
s are defined formultipart
:
- If the property is a primitive, or an array of primitive values, the default Content-Type is
text/plain
- If the property is complex, or an array of complex values, the default Content-Type is
application/json
- If the property is a
type: string
with acontentEncoding
, the default Content-Type isapplication/octet-stream
The Content-Type for encoding a specific property. Default value depends on the property type: for
object
-application/json
; forarray
– the default is defined based on the inner type; for all other cases the default isapplication/octet-stream
. The value can be a specific media type (e.g.application/json
), a wildcard media type (e.g.image/*
), or a comma-separated list of the two types.
I think because they insist on using JSON Schema for validating things that aren't JSON, like form-data, and JSON Schema doesn't have type "binary", they reused the string type (stupid) and made this magical option which must be handled specially. From what I understand, this is how I would need to request such API using vanilla JS (which I'm doing because not a single OpenAPI lib for JS can do it and just work):
const formData = new FormData();
// prop with primitive value (strings, numbers, booleans, I guess?)
formData.append("prop1", String(value)); // same as sending a text/plain blob
// prop with object
formData.append(
"prop2",
new Blob([JSON.stringify(value)], { type: "application/json" }),
);
// for 3.0: prop with type "string", but format "binary"
// for 3.1: everything except object
formData.append(
"prop3",
new Blob([value], { type: "application/octet-stream" }),
);
// (and for arrays just call append many times for single property)
// send
fetch("https://example.com", { method: "POST", body: formData });
I think this lib should do similar thing.
text/plain
.For those that, like me, got stuck trying to make this work. Here's my complete solution for file uploads and downloads.
openapi-typescript
version 6.x and OpenAPI 3.0.3
generate-schema.mjs
import openapiTS from 'openapi-typescript';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const baseDir = path.dirname(fileURLToPath(import.meta.url));
const localPath = new URL(path.resolve(baseDir, '../apiSpec.json'), import.meta.url);
const output = await openapiTS(localPath, {
transform(schemaObject, metadata) {
if (schemaObject.format === 'binary') {
if (metadata.path.endsWith('multipart/form-data')) {
return schemaObject.nullable ? 'File | null' : 'File';
}
if (metadata.path.endsWith('application/octet-stream')) {
return schemaObject.nullable ? 'Blob | null' : 'Blob';
}
}
return undefined;
}
});
await fs.promises.writeFile(path.resolve(baseDir, '../src/schema.d.ts'), output);
This sets the type to File
if you are uploading something via multipart/form-data
and Blob
if you are downloading an application/octet-stream
. This will not work if the schema is a reference to a separate component. For example,
THIS WILL NOT WORK:
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/SomeFormSchema"
}
}
THIS WORKS:
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"file": {
"type": "string",
"format": "binary"
}
}
}
}
async function upload(file: File) {
await client.POST('/path/to/endpoint', {
body: {
file
},
bodySerializer: (body) => {
const formData = new FormData();
formData.set('file', body.file);
return formData;
}
});
}
async function download(): Blob | undefined {
const { data } = client.GET('/path/to/endpoint', {
parseAs: 'blob'
});
return data;
}
Also stumbled up on this. I think it would be great to change the default options or at least give a CLI option to apply less conservative parsing?
Also keep in mind that NextJS server actions cannot take objects with Blob data in them yet. If you're using server actions and for some reason don't use FormData
, you'll not be able to submit Blob data to server actions at this point, even with openapi-fetch
supporting it.
Just in case you're struggling with this, it might be fixed in an upcoming release based on this PR.
Not to add another, "me too" for the requested change, but I just spent an unhealthy amount of time trying to remedy this on my end. When trying to use the transform API, I kept getting errors because the schema didn't match the expected input for transformation, despite it being a valid schema otherwise. I started changing several parts of the schema, including adding fields that are not required according to the OpenAPI spec (e.g. variables
parameter in a server object is expected, though not required), but then gave up because it was cascading into a larger change set that didn't make much sense.
I understand the resistance to imposing opinion. I think an optional flag when generating the schema viaa CLI could work just as well. It appears that a vast majority of use-cases for "format": "binary"
are for Blob
type.
For those that, like me, got stuck trying to make this work. Here's my complete solution for file uploads and downloads.
openapi-typescript
version 6.x and OpenAPI 3.0.3openapi-typescript types
generate-schema.mjs
import openapiTS from 'openapi-typescript'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; const baseDir = path.dirname(fileURLToPath(import.meta.url)); const localPath = new URL(path.resolve(baseDir, '../apiSpec.json'), import.meta.url); const output = await openapiTS(localPath, { transform(schemaObject, metadata) { if (schemaObject.format === 'binary') { if (metadata.path.endsWith('multipart/form-data')) { return schemaObject.nullable ? 'File | null' : 'File'; } if (metadata.path.endsWith('application/octet-stream')) { return schemaObject.nullable ? 'Blob | null' : 'Blob'; } } return undefined; } }); await fs.promises.writeFile(path.resolve(baseDir, '../src/schema.d.ts'), output);
This sets the type to
File
if you are uploading something viamultipart/form-data
andBlob
if you are downloading anapplication/octet-stream
. This will not work if the schema is a reference to a separate component. For example,THIS WILL NOT WORK:
"multipart/form-data": { "schema": { "$ref": "#/components/schemas/SomeFormSchema" } }
THIS WORKS:
"multipart/form-data": { "schema": { "type": "object", "properties": { "file": { "type": "string", "format": "binary" } } } }
openapi-fetch client
Uploading a file
async function upload(file: File) { await client.POST('/path/to/endpoint', { body: { file }, bodySerializer: (body) => { const formData = new FormData(); formData.set('file', body.file); return formData; } }); }
Downloading a file
async function download(): Blob | undefined { const { data } = client.GET('/path/to/endpoint', { parseAs: 'blob' }); return data; }
Thank you so much 🥇
Based on what @fredsensibill did, I've implemented his solution using tsx
in our package where we convert multiple schemas to TS at once. Put it in a file transformer.ts
and just run it with tsx ./transformer.ts
.
How the given example works: it grabs, for example, service1.yaml
from the inputFolder
and generates service1.ts
in the outputFolder
and does the same for all *.yaml
files it fints in the inputFolder
.
To be able to use top-level await make sure to use "type": "module",
in your package.json
and have the following in your tsconfig.json
's compilerOptions
:
"compilerOptions": {
"target": "ES2017",
"module": "Preserve"
},
Here's transformer.ts
:
import fs from 'fs';
import openapiTS, { OpenAPITSOptions } from 'openapi-typescript';
const inputFolder = './specs';
const outputFolder = './schemas/specs';
const files = await fs.promises.readdir(inputFolder);
const schemaFilenames = files.filter(f => f.endsWith('.yaml')).map(f => f.split('.yaml')[0]);
// common options object for the AST
const options: OpenAPITSOptions = {
transform(schemaObject, metadata) {
if (schemaObject.format === 'binary') {
if (metadata.path.endsWith('multipart/form-data')) {
return schemaObject.nullable ? 'File | null' : 'File';
}
if (metadata.path.endsWith('application/octet-stream')) {
return schemaObject.nullable ? 'Blob | null' : 'Blob';
}
}
return undefined;
}
}
// grab all the outputs
const promises: Promise<string>[] = schemaFilenames.map((schemaFilename) =>
openapiTS(new URL(`${inputFolder}/${schemaFilename}.yaml`, import.meta.url), options)
);
const resolvedPromises = await Promise.all(promises);
// write to the filesystem
await fs.promises.mkdir(outputFolder, { recursive: true });
const fsPromises = schemaFilenames.map((schemaFilename, idx) =>
fs.promises.writeFile(`${outputFolder}/${schemaFilename}.ts`, resolvedPromises[idx], { flag: 'w+' })
);
await Promise.all(fsPromises);
This issue is stale because it has been open for 90 days with no activity. If there is no activity in the next 7 days, the issue will be closed.
This issue was closed because it has been inactive for 7 days since being marked as stale. Please open a new issue if you believe you are encountering a related problem.
Version 7+ of openapi-typescript requires transform
to return a TypeScript AST rather than a string. I have adjusted @fredsensibill's solution accordingly.
This is working for me, but I do not work with the TypeScript AST very often and parts of it look dodgy. Please consider I am just some dude in a comment section before using this for real.
import type { OpenAPITSOptions } from 'openapi-typescript';
import { factory } from 'typescript';
const options: OpenAPITSOptions = {
transform({ format, nullable }, { path }) {
if (format !== 'binary' || !path) {
return;
}
const typeName = path.includes('multipart~1form-data')
? 'File'
: path.includes('application~1octet-stream')
? 'Blob'
: null;
if (!typeName) {
return;
}
const node = factory.createTypeReferenceNode(typeName);
return nullable
? factory.createUnionTypeNode([
node,
factory.createTypeReferenceNode('null')
])
: node;
}
};
FWIW, also sharing my solution, same disclaimer as above (just a random comment guy). To be called directly with API spec as argument:
import openapiTS, { astToString } from 'openapi-typescript';
import fs from "node:fs";
import ts from "typescript";
if (process.argv.length === 2) {
console.error('Expected at least one argument!');
process.exit(1);
}
const BLOB = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Blob"));
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull());
const ast = await openapiTS(new URL(process.argv[2]), {
transform(schemaObject, metadata) {
if (schemaObject.format === 'binary') {
return schemaObject.nullable ? ts.factory.createUnionTypeNode([BLOB, NULL]) : BLOB;
}
}
});
const output = astToString(ast);
fs.writeFileSync('./src/lib/api/v1.d.ts', output);
The docs actually have an up-to-date example for the new AST-based API: https://openapi-ts.dev/node#example-blob-types
Description
In OpenAPI 3.0 we can describe the schema for files uploaded directly or as multipart:
The binary format indicates a
File
type object. However, the typescript interface generated by the library sets this property tostring
:Proposal
The type of the property should be set to
File
:NOTE: For reference the openapi-generator for typescript correctly sets the type to
File
but doesn't support OpenAPI 3.0Checklist
Similar to #1123