[BUG][typescript-*] Bad behavior when trying to map generics
Bug Report Checklist
- [x] Have you provided a full/minimal spec to reproduce the issue?
- [x] Have you validated the input using an OpenAPI validator?
- [x] Have you tested with the latest master to confirm the issue still exists?
- [x] Have you searched for related issues/PRs?
- [x] What's the actual output vs expected output?
- [ ] [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description
I have an OpenAPI file where one of the components has a data type of "object." In TypeScript, the object data type is very generic and doesn't help with data typing but I do think it's an appropriate default for most cases. However, for my use case on a particular project, I would like to map object to Record<string, unknown> but this leads to generation problems.
The first problem is that Record is a built-in type, so there's no need to import it. This results in bad imports being generated.
import type { Recordstringunknown } from './Recordstringunknown';
import {
RecordstringunknownFromJSON,
RecordstringunknownFromJSONTyped,
RecordstringunknownToJSON,
RecordstringunknownToJSONTyped,
} from './Recordstringunknown';
The second problem (also seen in the import above) is that the <> for generics are stripped out of what's generated.
export interface User {
// ...
/**
*
* @type {Recordstringunknown}
* @memberof User
*/
metadata?: Recordstringunknown;
}
The above generated code should be Record<string,unknown> as specified in the config.yml file I have listed below. Related also, even though my coding style preference is to have a space after the comma (i.e., Record<string, unknown>), that leads to even worse generated results.
Note: the Java template can handle the <> (generic syntax) and spaces without issue.
import type { Recordstring unknown } from './Recordstring unknown';
import {
Recordstring unknownFromJSON,
Recordstring unknownFromJSONTyped,
Recordstring unknownToJSON,
Recordstring unknownToJSONTyped,
} from './Recordstring unknown';
// ...
export interface User {
// ...
/**
*
* @type {Recordstring unknown}
* @memberof User
*/
metadata?: Recordstring unknown;
}
openapi-generator version
7.13.0
OpenAPI declaration file content or url
openapi: 3.1.0
info:
title: Sample API
description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
version: 1.0.0
servers:
- url: http://api.example.com/v1
description: Optional server description, e.g. Main (production) server
- url: http://staging-api.example.com
description: Optional server description, e.g. Internal staging server for testing
paths:
/users:
get:
summary: Returns a list of users.
description: Optional extended description in CommonMark or HTML.
responses:
"200": # status code
description: A JSON array of user names
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: integer
format: int64
description: The user ID.
name:
type: string
description: The user name.
email:
type: string
format: email
description: The user email address.
metadata:
type: object
Generation Details
# config.yml
typeMappings:
object: Record<string,unknown>
Steps to reproduce
docker run --rm \
-v ".:/local" \
openapitools/openapi-generator-cli:latest generate \
-c "/local/config.yml" \
-g "typescript-fetch" \
-i "/local/openapi.yml" \
-o /local/src
Related issues/PRs
None that I've been able to find.
Suggest a fix
I believe this is a problem with the typescript-* templates; I've tested this with typescript-fetch and typescript-axios and both have the same problem. But if I switch the template to java, it correctly handles generics/spaces
@joscha @mkusaka , hello, please let me know what you think of this:
https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type
I want to add these all of types, but without the <>, to languageSpecificPrimitives:
https://github.com/OpenAPITools/openapi-generator/blob/12fa2c0032a12c29efd8cf359e1e62cfdc0f0d1a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java#L307
And override the defaultIncludes property in the same fashion:
https://github.com/OpenAPITools/openapi-generator/blob/12fa2c0032a12c29efd8cf359e1e62cfdc0f0d1a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java#L1735
And then over here:
https://github.com/OpenAPITools/openapi-generator/blob/12fa2c0032a12c29efd8cf359e1e62cfdc0f0d1a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java#L808
I'll add an else-if to match the the substring of type before the <
This should take care of the import issue and the name sanitization issue.
Also, @allejo
Note: the Java template can handle the <> (generic syntax) and spaces without issue.
Please can you confirm something? I see that even though the type name worked as expected, there is still a funny-looking import statement:
import org.openapitools.client.model.Record<string,unknown>;
As an update, I've done and tested it and gotten seemingly satisfactory results:
import { mapValues } from '../runtime';
/**
*
* @export
* @interface User
*/
export interface User {
/**
* The user ID.
* @type {number}
* @memberof User
*/
id?: number;
/**
* The user name.
* @type {string}
* @memberof User
*/
name?: string;
/**
* The user email address.
* @type {string}
* @memberof User
*/
email?: string;
/**
*
* @type {Record<string, unknown>}
* @memberof User
*/
metadata?: Record<string, unknown>;
}
/**
* Check if a given object implements the User interface.
*/
export function instanceOfUser(value: object): value is User {
return true;
}
export function UserFromJSON(json: any): User {
return UserFromJSONTyped(json, false);
}
export function UserFromJSONTyped(json: any, ignoreDiscriminator: boolean): User {
if (json == null) {
return json;
}
return {
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'] == null ? undefined : json['name'],
'email': json['email'] == null ? undefined : json['email'],
'metadata': json['metadata'] == null ? undefined : json['metadata'],
};
}
export function UserToJSON(json: any): User {
return UserToJSONTyped(json, false);
}
export function UserToJSONTyped(value?: User | null, ignoreDiscriminator: boolean = false): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'name': value['name'],
'email': value['email'],
'metadata': value['metadata'],
};
}
I contemplated the potential use of other utility types and tried out Pick on a new schema object, UserSummary, which filters out email from User. It's a "top-level" schema compared to metadata, so I ran into an issue and the only way I've gotten it to work so far is to use withoutRuntimeChecks: true
import * as runtime from '../runtime';
import type {
User,
} from '../models/index';
/**
*
*/
export class DefaultApi extends runtime.BaseAPI {
/**
* Optional extended description in CommonMark or HTML.
* Returns a list of users.
*/
async usersGetRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<User>> {
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
const response = await this.request({
path: `/users`,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response);
}
/**
* Optional extended description in CommonMark or HTML.
* Returns a list of users.
*/
async usersGet(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<User> {
const response = await this.usersGetRaw(initOverrides);
return await response.value();
}
/**
* Optional extended description in CommonMark or HTML.
* Returns a list of users.
*/
async usersSummaryGetRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Pick<User, "email">>> {
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
const response = await this.request({
path: `/users/summary`,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response);
}
/**
* Optional extended description in CommonMark or HTML.
* Returns a list of users.
*/
async usersSummaryGet(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Pick<User, "email">> {
const response = await this.usersSummaryGetRaw(initOverrides);
return await response.value();
}
}
I'll make the PR over this weekend, but I'd like to receive feedback if anyone has any