NSwag
NSwag copied to clipboard
Wildcard property added to all classes after upgrading to v13.20.0 (still like that in v14.0.2)
Me and my collegues have been using NSwag for 3 years in a project to generate TypeScript clients from API specs. Some months ago we stopped updating this dependency in our project because when we generate our clients using v13.20.0 all the classes in the generated clients had a wildcard property added to them.
[key: string]: any;
Since this would mean that we wouldn't get any compilation error when a property name is changed in the spec we stopped updating this dependency. I didn't find any notes about this change, so I was kind of in "wait and see" mode in case it was a mistake, but I see it remains like this in the latest v14.0.2 release.
Was this an intended change?
Is there some setting in the .nswag file I can configure to make it generate without the wildcard properties the way it does in v13.16.1 which is the version I am stuck on?
Or any other way to "globally" configure this, I wouldn't want to have to specify this per object in the spec, that would be a lot of refactoring for us, but more importantly it would be easy for our backend devs to make the mistake of not including that in the spec for new objects, and for our frontend devs to not notice the wildcard property. Then you end up with the situation where the property might be renamed by the backend devs but when our frontend devs re-generate the clients there is no compilation error.
Thanks!
Can you provide your settings from which you are generating TS clients? Which template are you using?
(And example model from API spec)
@krzyhan Yeah sure, here is everything from our most simple API:
.nswag file
{
"runtime": "Net60",
"defaultVariables": null,
"documentGenerator": {
"fromDocument": {
"json": "",
"url": "../../../../feature-toggle/shared/src/main/resources/toggle-api.yaml",
"output": null,
"newLineBehavior": "Auto"
}
},
"codeGenerators": {
"openApiToTypeScriptClient": {
"className": "FeatureToggleClient",
"moduleName": "",
"namespace": "",
"typeScriptVersion": 2.7,
"template": "Fetch",
"promiseType": "Promise",
"httpClass": "HttpClient",
"withCredentials": false,
"useSingletonProvider": false,
"injectionTokenType": "OpaqueToken",
"rxJsVersion": 6.0,
"dateTimeType": "Date",
"nullValue": "Undefined",
"generateClientClasses": true,
"generateClientInterfaces": false,
"generateOptionalParameters": false,
"exportTypes": true,
"wrapDtoExceptions": false,
"exceptionClass": "ApiException",
"clientBaseClass": "MyClient",
"wrapResponses": false,
"wrapResponseMethods": [],
"generateResponseClasses": true,
"responseClass": "SwaggerResponse",
"protectedMethods": [],
"configurationClass": null,
"useTransformOptionsMethod": false,
"useTransformResultMethod": false,
"generateDtoTypes": true,
"operationGenerationMode": "SingleClientFromOperationId",
"markOptionalProperties": true,
"generateCloneMethod": false,
"typeStyle": "Class",
"enumStyle": "Enum",
"useLeafType": false,
"classTypes": [],
"extendedClasses": [],
"extensionCode": "../feature-toggle-api.extensions.ts",
"generateDefaultValues": true,
"excludedTypeNames": [],
"excludedParameterNames": [],
"handleReferences": false,
"generateConstructorInterface": true,
"convertConstructorInterfaceData": true,
"importRequiredTypes": true,
"useGetBaseUrlMethod": false,
"baseUrlTokenName": "API_BASE_URL",
"queryNullValue": "",
"useAbortSignal": false,
"inlineNamedDictionaries": false,
"inlineNamedAny": false,
"templateDirectory": null,
"typeNameGeneratorType": null,
"propertyNameGeneratorType": null,
"enumNameGeneratorType": null,
"serviceHost": null,
"serviceSchemes": null,
"output": "../feature-toggle-api.ts",
"newLineBehavior": "Auto"
}
}
}
.yaml
openapi: 3.0.3
info:
title: My Feature Toggle API
description: >
This feature flag api:
- List check if a feature is active
version: 1.0.0
tags:
- name: Toggle
paths:
/toggle/active/{feature-key}:
get:
tags:
- Toggle
summary: Check if an toggle is active
description: >
This endpoint will:
* return a true or false Toggle depending the configuration for the current user.
operationId: isToggleActive
parameters:
- in: path
name: feature-key
required: true
schema:
type: string
maxLength: 200
responses:
'200':
description: successful operation
content:
'application/json':
schema:
$ref: '#/components/schemas/Toggle'
'401':
$ref: '#/components/responses/Unauthorized'
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
components:
schemas:
Toggle:
type: object
properties:
active:
type: boolean
required:
- active
responses:
NotFound:
description: The specified resource was not found
Unauthorized:
description: Unauthorized
Forbidden:
description: Forbidden (jwt was valid, but user was not found)
Conflict:
description: Conflict e.g. data violates a constraint such as unique email addresses
BadRequest:
description: Bad Request - an issue with request data
InternalServerError:
description: Internal Server Error - a generic 'something bad' that we couldn't recover from happened
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: [ ]
Generated client with v.13.16.1
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v13.16.1.0 (NJsonSchema v10.7.2.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
/* tslint:disable */
/* eslint-disable */
// ReSharper disable InconsistentNaming
import { MyClient } from "./my-client"
export class FeatureToggleClient extendsMyClient {
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
super();
this.http = http ? http : window as any;
this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : "";
}
/**
* Check if an toggle is active
* @return successful operation
*/
isToggleActive(feature_key: string): Promise<Toggle> {
let url_ = this.baseUrl + "/toggle/active/{feature-key}";
if (feature_key === undefined || feature_key === null)
throw new Error("The parameter 'feature_key' must be defined.");
url_ = url_.replace("{feature-key}", encodeURIComponent("" + feature_key));
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processIsToggleActive(_response);
});
}
protected processIsToggleActive(response: Response): Promise<Toggle> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result200 = Toggle.fromJS(resultData200);
return result200;
});
} else if (status === 401) {
return response.text().then((_responseText) => {
return throwException("Unauthorized", status, _responseText, _headers);
});
} else if (status === 400) {
return response.text().then((_responseText) => {
return throwException("Bad Request - an issue with request data", status, _responseText, _headers);
});
} else if (status === 404) {
return response.text().then((_responseText) => {
return throwException("The specified resource was not found", status, _responseText, _headers);
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<Toggle>(null as any);
}
}
export class Toggle implements IToggle {
active!: boolean;
constructor(data?: IToggle) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.active = _data["active"];
}
}
static fromJS(data: any): Toggle {
data = typeof data === 'object' ? data : {};
let result = new Toggle();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["active"] = this.active;
return data;
}
}
export interface IToggle {
active: boolean;
}
export class ApiException extends Error {
message: string;
status: number;
response: string;
headers: { [key: string]: any; };
result: any;
constructor(message: string, status: number, response: string, headers: { [key: string]: any; }, result: any) {
super();
this.message = message;
this.status = status;
this.response = response;
this.headers = headers;
this.result = result;
}
protected isApiException = true;
static isApiException(obj: any): obj is ApiException {
return obj.isApiException === true;
}
}
function throwException(message: string, status: number, response: string, headers: { [key: string]: any; }, result?: any): any {
if (result !== null && result !== undefined)
throw result;
else
throw new ApiException(message, status, response, headers, null);
}
/* tslint:disable */
/* eslint-disable */
Generated client with v13.20.0
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v13.20.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
/* tslint:disable */
/* eslint-disable */
// ReSharper disable InconsistentNaming
import { MyClient } from "./my-client"
export class FeatureToggleClient extends MyClient {
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
super();
this.http = http ? http : window as any;
this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : "";
}
/**
* Check if an toggle is active
* @return successful operation
*/
isToggleActive(feature_key: string): Promise<Toggle> {
let url_ = this.baseUrl + "/toggle/active/{feature-key}";
if (feature_key === undefined || feature_key === null)
throw new Error("The parameter 'feature_key' must be defined.");
url_ = url_.replace("{feature-key}", encodeURIComponent("" + feature_key));
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processIsToggleActive(_response);
});
}
protected processIsToggleActive(response: Response): Promise<Toggle> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result200 = Toggle.fromJS(resultData200);
return result200;
});
} else if (status === 401) {
return response.text().then((_responseText) => {
return throwException("Unauthorized", status, _responseText, _headers);
});
} else if (status === 400) {
return response.text().then((_responseText) => {
return throwException("Bad Request - an issue with request data", status, _responseText, _headers);
});
} else if (status === 404) {
return response.text().then((_responseText) => {
return throwException("The specified resource was not found", status, _responseText, _headers);
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<Toggle>(null as any);
}
}
export class Toggle implements IToggle {
active!: boolean;
[key: string]: any;
constructor(data?: IToggle) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
for (var property in _data) {
if (_data.hasOwnProperty(property))
this[property] = _data[property];
}
this.active = _data["active"];
}
}
static fromJS(data: any): Toggle {
data = typeof data === 'object' ? data : {};
let result = new Toggle();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
for (var property in this) {
if (this.hasOwnProperty(property))
data[property] = this[property];
}
data["active"] = this.active;
return data;
}
}
export interface IToggle {
active: boolean;
[key: string]: any;
}
export class ApiException extends Error {
message: string;
status: number;
response: string;
headers: { [key: string]: any; };
result: any;
constructor(message: string, status: number, response: string, headers: { [key: string]: any; }, result: any) {
super();
this.message = message;
this.status = status;
this.response = response;
this.headers = headers;
this.result = result;
}
protected isApiException = true;
static isApiException(obj: any): obj is ApiException {
return obj.isApiException === true;
}
}
function throwException(message: string, status: number, response: string, headers: { [key: string]: any; }, result?: any): any {
if (result !== null && result !== undefined)
throw result;
else
throw new ApiException(message, status, response, headers, null);
}
/* tslint:disable */
/* eslint-disable */
Did I provide what you need @krzyhan ?
Can you - or anyone else - share some knowledge on what changed between v.13.16.1 and v.v13.20.0 to make this wildcard property appear (as seen in the example above)?
I'm wondering if it will be possible for me to upgrade and generate client code without this wildcard property?