RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.67k stars 1.23k forks source link

Wildcard property added to all classes after upgrading to v13.20.0 (still like that in v14.0.2) #4718

Open johansunden opened 7 months ago

johansunden commented 7 months ago

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!

krzyhan commented 7 months ago

Can you provide your settings from which you are generating TS clients? Which template are you using?

(And example model from API spec)

johansunden commented 7 months ago

@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 */
johansunden commented 6 months ago

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?