RicoSuter / NSwag

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

Issue with _mappings variable in the process method not found for some generated api calls #3505

Open glennwillems opened 3 years ago

glennwillems commented 3 years ago

Using NSwag.MsBuild 13.10.8 in a .net Core Library project. I have an issue generating my typescript client where some process methods do not have the _mappings variable initialized.

I have selected 2 routes where one has the correct _mappings variable and the other that is missing it. Both are HttpPost without any fancy setup in Asp.net Core Web Api.

Open api spec:

{ "x-generator": "NSwag v13.10.8.0 (NJsonSchema v10.3.11.0 (Newtonsoft.Json v12.0.0.0))", "openapi": "3.0.0", "info": { "title": "My Title", "version": "1.0.0" }, "paths": { "/api/backoffice/orders/cancel": { "post": { "tags": [ "Order" ], "operationId": "Order_CancelOrder", "requestBody": { "x-name": "parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CancelOrderParameters" } } }, "required": true, "x-position": 1 }, "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CancelOrderResult" } } } } } } }, "/api/backoffice/orders/betalen": { "post": { "tags": [ "Order" ], "summary": "Start payment procedure for the specified order. For test purposes only because this will only be used throught the Public API.", "operationId": "Order_BetaalOrder", "requestBody": { "x-name": "parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BetaalOrderPublicParameters" } } }, "required": true, "x-position": 1 }, "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BetaalOrderResult" } } } } } } }, }, "components": { "schemas": { "CancelOrderResult": { "type": "object", "additionalProperties": false }, "CancelOrderParameters": { "type": "object", "additionalProperties": false, "properties": { "order": { "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/OrderSelector" } ] } } }, "BetaalOrderResult": { "type": "object", "additionalProperties": false, "properties": { "redirectionUrl": { "type": "string", "nullable": true }, "redirectionVersion": { "type": "string", "nullable": true }, "redirectionData": { "type": "string", "nullable": true } } }, "BetaalOrderPublicParameters": { "type": "object", "additionalProperties": false, "properties": { "order": { "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/OrderSelector" } ] }, "betalingOkUri": { "type": "string", "nullable": true }, "betalingMisluktUri": { "type": "string", "nullable": true }, "language": { "type": "string", "nullable": true } } } } } }

My code generator config: "codeGenerators": { "openApiToTypeScriptClient": { "className": "{controller}Api", "moduleName": "", "namespace": "", "typeScriptVersion": 4.0, "template": "Angular", "promiseType": "Promise", "httpClass": "HttpClient", "useSingletonProvider": false, "injectionTokenType": "InjectionToken", "rxJsVersion": 6.0, "dateTimeType": "Date", "nullValue": "Undefined", "generateClientClasses": true, "generateClientInterfaces": true, "generateOptionalParameters": true, "exportTypes": true, "wrapDtoExceptions": false, "exceptionClass": "ApiException", "clientBaseClass": null, "wrapResponses": false, "wrapResponseMethods": [], "generateResponseClasses": true, "responseClass": "ApiResponse", "protectedMethods": [], "configurationClass": null, "useTransformOptionsMethod": false, "useTransformResultMethod": false, "generateDtoTypes": true, "operationGenerationMode": "MultipleClientsFromOperationId", "markOptionalProperties": false, "generateCloneMethod": false, "typeStyle": "Class", "enumStyle": "Enum", "useLeafType": false, "classTypes": [], "extendedClasses": [], "extensionCode": null, "generateDefaultValues": true, "excludedTypeNames": [], "excludedParameterNames": [], "handleReferences": true, "generateConstructorInterface": true, "convertConstructorInterfaceData": false, "importRequiredTypes": true, "useGetBaseUrlMethod": false, "baseUrlTokenName": "API_BASE_URL", "queryNullValue": "", "inlineNamedDictionaries": false, "inlineNamedAny": false, "templateDirectory": null, "typeNameGeneratorType": null, "propertyNameGeneratorType": null, "enumNameGeneratorType": null, "serviceHost": null, "serviceSchemes": null, "output": "../dist/javascript/d09.avgpv.backoffice.client.ts", "newLineBehavior": "Auto" }

The actual generated typescript client looks like the following. The first one is missing the _mappings variabled, the 2nd one actually has it initialized.

cancelOrder(parameters: CancelOrderParameters): Observable<CancelOrderResult> {
        let url_ = this.baseUrl + "/api/backoffice/orders/cancel";
        url_ = url_.replace(/[?&]$/, "");

        const content_ = JSON.stringify(parameters);

        let options_ : any = {
            body: content_,
            observe: "response",
            responseType: "blob",
            headers: new HttpHeaders({
                "Content-Type": "application/json",
                "Accept": "application/json"
            })
        };

        return this.http.request("post", url_, options_).pipe(_observableMergeMap((response_ : any) => {
            return this.processCancelOrder(response_);
        })).pipe(_observableCatch((response_: any) => {
            if (response_ instanceof HttpResponseBase) {
                try {
                    return this.processCancelOrder(<any>response_);
                } catch (e) {
                    return <Observable<CancelOrderResult>><any>_observableThrow(e);
                }
            } else
                return <Observable<CancelOrderResult>><any>_observableThrow(response_);
        }));
    }

    protected processCancelOrder(response: HttpResponseBase): Observable<CancelOrderResult> {
        const status = response.status;
        const responseBlob =
            response instanceof HttpResponse ? response.body :
            (<any>response).error instanceof Blob ? (<any>response).error : undefined;

        let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }}
        if (status === 200) {
            return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
            let result200: any = null;
            let resultData200 = _responseText === "" ? null : jsonParse(_responseText, this.jsonParseReviver);
            result200 = CancelOrderResult.fromJS(resultData200, _mappings);
            return _observableOf(result200);
            }));
        } else if (status !== 200 && status !== 204) {
            return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
            }));
        }
        return _observableOf<CancelOrderResult>(<any>null);
    }
betaalOrder(parameters: BetaalOrderPublicParameters): Observable<BetaalOrderResult> {
        let url_ = this.baseUrl + "/api/backoffice/orders/betalen";
        url_ = url_.replace(/[?&]$/, "");

        const content_ = JSON.stringify(parameters);

        let options_ : any = {
            body: content_,
            observe: "response",
            responseType: "blob",
            headers: new HttpHeaders({
                "Content-Type": "application/json",
                "Accept": "application/json"
            })
        };

        return this.http.request("post", url_, options_).pipe(_observableMergeMap((response_ : any) => {
            return this.processBetaalOrder(response_);
        })).pipe(_observableCatch((response_: any) => {
            if (response_ instanceof HttpResponseBase) {
                try {
                    return this.processBetaalOrder(<any>response_);
                } catch (e) {
                    return <Observable<BetaalOrderResult>><any>_observableThrow(e);
                }
            } else
                return <Observable<BetaalOrderResult>><any>_observableThrow(response_);
        }));
    }

    protected processBetaalOrder(response: HttpResponseBase): Observable<BetaalOrderResult> {
        const status = response.status;
        const responseBlob =
            response instanceof HttpResponse ? response.body :
            (<any>response).error instanceof Blob ? (<any>response).error : undefined;

        let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }}
        let _mappings: { source: any, target: any }[] = [];
        if (status === 200) {
            return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
            let result200: any = null;
            let resultData200 = _responseText === "" ? null : jsonParse(_responseText, this.jsonParseReviver);
            result200 = BetaalOrderResult.fromJS(resultData200, _mappings);
            return _observableOf(result200);
            }));
        } else if (status !== 200 && status !== 204) {
            return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
            }));
        }
        return _observableOf<BetaalOrderResult>(<any>null);
    }
glennwillems commented 3 years ago

We found out that if your responsemodel is an empty class it will not generate the let _mappings: { source: any, target: any }[] = []; line. So would this be regarded as a bug or works as inteded?

gregfullman commented 3 years ago

This is definitely a bug. I'm seeing the same, but with a empty class that inherits from a non-empty base class.

alexdresko commented 1 year ago

Confirmed that adding a simple public string bool Anything { get; set; } = true to my response class fixed the problem.