Open Closed

nswag error on null optional param #8409


0
bdadmin created

After upgrading to 8.1 we are getting an error with optional params in our service proxies. As you can see in my screen shot the param is marked as optional but nswag has code to throw an error if null. What changed?


13 Answer(s)
  • 0
    ismcagdas created
    Support Team

    Hi @bdadmin

    We haven't faced such problem. Could you also share your Swashbuckle.AspNetCore version ?

    Thanks,

  • 0
    morindo created

    I'm having the same issue. Should optional parameters accept null value?

    Thanks,

  • 0
    maliming created
    Support Team

    hi @morindo The zero demo project should not have the problem you mentioned. Can you compare the relevant code and package version?

  • 0
    morindo created

    Thanks @maliming for the quick reply. So, for Swashbuckle.AspNetCore nuget package the version is 5.0.0-rc4. Just so you know, it's a new solution, started from ASP.NET Zero (Angular) version 8.1.0 and I didn't update any nuget or npm packages.


    Here is my service.config.nswag configuration file.

    {
      "runtime": "Default",
      "defaultVariables": null,
      "documentGenerator": {
        "fromDocument": {
          "url": "http://localhost:22742/swagger/v1/swagger.json",
          "output": null
        }
      },
      "codeGenerators": {
        "openApiToTypeScriptClient": {
          "className": "{controller}ServiceProxy",
          "moduleName": "",
          "namespace": "",
          "typeScriptVersion": 2.7,
          "template": "Angular",
          "promiseType": "Promise",
          "httpClass": "HttpClient",
          "useSingletonProvider": false,
          "injectionTokenType": "InjectionToken",
          "rxJsVersion": 6.0,
          "dateTimeType": "MomentJS",
          "nullValue": "Undefined",
          "generateClientClasses": true,
          "generateClientInterfaces": false,
          "generateOptionalParameters": false,
          "exportTypes": true,
          "wrapDtoExceptions": false,
          "exceptionClass": "ApiException",
          "clientBaseClass": null,
          "wrapResponses": false,
          "wrapResponseMethods": [],
          "generateResponseClasses": true,
          "responseClass": "SwaggerResponse",
          "protectedMethods": [],
          "configurationClass": null,
          "useTransformOptionsMethod": false,
          "useTransformResultMethod": false,
          "generateDtoTypes": true,
          "operationGenerationMode": "MultipleClientsFromPathSegments",
          "markOptionalProperties": false,
          "generateCloneMethod": false,
          "typeStyle": "Class",
          "classTypes": [],
          "extendedClasses": [],
          "extensionCode": "service.extensions.ts",
          "generateDefaultValues": true,
          "excludedTypeNames": [],
          "excludedParameterNames": [],
          "handleReferences": false,
          "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": "../src/shared/service-proxies/service-proxies.ts"
        },
        "openApiToCSharpClient": {
          "clientBaseClass": null,
          "configurationClass": null,
          "generateClientClasses": true,
          "generateClientInterfaces": false,
          "injectHttpClient": false,
          "disposeHttpClient": true,
          "protectedMethods": [],
          "generateExceptionClasses": true,
          "exceptionClass": "SwaggerException",
          "wrapDtoExceptions": true,
          "useHttpClientCreationMethod": false,
          "httpClientType": "System.Net.Http.HttpClient",
          "useHttpRequestMessageCreationMethod": false,
          "useBaseUrl": true,
          "generateBaseUrlProperty": true,
          "generateSyncMethods": false,
          "exposeJsonSerializerSettings": false,
          "clientClassAccessModifier": "public",
          "typeAccessModifier": "public",
          "generateContractsOutput": false,
          "contractsNamespace": null,
          "contractsOutputFilePath": null,
          "parameterDateTimeFormat": "s",
          "generateUpdateJsonSerializerSettingsMethod": true,
          "serializeTypeInformation": false,
          "queryNullValue": "",
          "className": "{controller}Client",
          "operationGenerationMode": "MultipleClientsFromOperationId",
          "additionalNamespaceUsages": [],
          "additionalContractNamespaceUsages": [],
          "generateOptionalParameters": false,
          "generateJsonMethods": true,
          "enforceFlagEnums": false,
          "parameterArrayType": "System.Collections.Generic.IEnumerable",
          "parameterDictionaryType": "System.Collections.Generic.IDictionary",
          "responseArrayType": "System.Collections.ObjectModel.ObservableCollection",
          "responseDictionaryType": "System.Collections.Generic.Dictionary",
          "wrapResponses": false,
          "wrapResponseMethods": [],
          "generateResponseClasses": true,
          "responseClass": "SwaggerResponse",
          "namespace": "MyNamespace",
          "requiredPropertiesMustBeDefined": true,
          "dateType": "System.DateTime",
          "jsonConverters": null,
          "anyType": "object",
          "dateTimeType": "System.DateTime",
          "timeType": "System.TimeSpan",
          "timeSpanType": "System.TimeSpan",
          "arrayType": "System.Collections.ObjectModel.ObservableCollection",
          "arrayInstanceType": "System.Collections.ObjectModel.Collection",
          "dictionaryType": "System.Collections.Generic.Dictionary",
          "dictionaryInstanceType": "System.Collections.Generic.Dictionary",
          "arrayBaseType": "System.Collections.ObjectModel.ObservableCollection",
          "dictionaryBaseType": "System.Collections.Generic.Dictionary",
          "classStyle": "Inpc",
          "generateDefaultValues": true,
          "generateDataAnnotations": true,
          "excludedTypeNames": [],
          "excludedParameterNames": [],
          "handleReferences": false,
          "generateImmutableArrayProperties": false,
          "generateImmutableDictionaryProperties": false,
          "jsonSerializerSettingsTransformationMethod": null,
          "inlineNamedArrays": false,
          "inlineNamedDictionaries": false,
          "inlineNamedTuples": true,
          "inlineNamedAny": false,
          "generateDtoTypes": true,
          "generateOptionalPropertiesAsNullable": false,
          "templateDirectory": null,
          "typeNameGeneratorType": null,
          "propertyNameGeneratorType": null,
          "enumNameGeneratorType": null,
          "serviceHost": null,
          "serviceSchemes": null,
          "output": null
        },
        "openApiToCSharpController": {
          "controllerBaseClass": null,
          "controllerStyle": "Partial",
          "controllerTarget": "AspNet",
          "useCancellationToken": false,
          "useActionResultType": false,
          "generateModelValidationAttributes": false,
          "routeNamingStrategy": "None",
          "className": "{controller}",
          "operationGenerationMode": "MultipleClientsFromOperationId",
          "additionalNamespaceUsages": [
            "System.Web.Http"
          ],
          "additionalContractNamespaceUsages": [],
          "generateOptionalParameters": false,
          "generateJsonMethods": true,
          "enforceFlagEnums": false,
          "parameterArrayType": "System.Collections.Generic.IEnumerable",
          "parameterDictionaryType": "System.Collections.Generic.IDictionary",
          "responseArrayType": "System.Collections.ObjectModel.ObservableCollection",
          "responseDictionaryType": "System.Collections.Generic.Dictionary",
          "wrapResponses": false,
          "wrapResponseMethods": [],
          "generateResponseClasses": true,
          "responseClass": "SwaggerResponse",
          "namespace": "MyNamespace",
          "requiredPropertiesMustBeDefined": true,
          "dateType": "System.DateTime",
          "jsonConverters": null,
          "anyType": "object",
          "dateTimeType": "System.DateTime",
          "timeType": "System.TimeSpan",
          "timeSpanType": "System.TimeSpan",
          "arrayType": "System.Collections.Generic.IEnumerable",
          "arrayInstanceType": "System.Collections.ObjectModel.Collection",
          "dictionaryType": "System.Collections.Generic.Dictionary",
          "dictionaryInstanceType": "System.Collections.Generic.Dictionary",
          "arrayBaseType": "System.Collections.ObjectModel.ObservableCollection",
          "dictionaryBaseType": "System.Collections.Generic.Dictionary",
          "classStyle": "Inpc",
          "generateDefaultValues": true,
          "generateDataAnnotations": true,
          "excludedTypeNames": [],
          "excludedParameterNames": [],
          "handleReferences": false,
          "generateImmutableArrayProperties": false,
          "generateImmutableDictionaryProperties": false,
          "jsonSerializerSettingsTransformationMethod": null,
          "inlineNamedArrays": false,
          "inlineNamedDictionaries": false,
          "inlineNamedTuples": true,
          "inlineNamedAny": false,
          "generateDtoTypes": true,
          "generateOptionalPropertiesAsNullable": false,
          "templateDirectory": null,
          "typeNameGeneratorType": null,
          "propertyNameGeneratorType": null,
          "enumNameGeneratorType": null,
          "serviceHost": null,
          "serviceSchemes": null,
          "output": null
        }
      }
    }
    

    When I run refresh.bat, I get the following result.

    R:\Dev\Repos\BakeSimple\angular\nswag>"..\node_modules\.bin\nswag" run
    NSwag NPM CLI
    NSwag command line tool for .NET Core NetCore21, toolchain v13.0.6.0 (NJsonSchema v10.0.23.0 (Newtonsoft.Json v11.0.0.0))
    Visit http://NSwag.org for more information.
    NSwag bin directory: R:\Dev\Repos\BakeSimple\angular\node_modules\nswag\bin\binaries\NetCore21
    
    Executing file 'R:\Dev\Repos\BakeSimple\angular\nswag\service.config.nswag' with variables ''...
    Done.
    
    Duration: 00:00:05.3494464
    

    Finally, here is part of my service-proxies.ts generated file.

    /**
         * @param filter (optional) 
         * @param nameFilter (optional) 
         * @param sorting (optional) 
         * @param skipCount (optional) 
         * @param maxResultCount (optional) 
         * @return Success
         */
        getAll(filter: string | undefined, nameFilter: string | undefined, sorting: string | undefined, skipCount: number | undefined, maxResultCount: number | undefined): Observable<PagedResultDtoOfGetProductCategoryForViewDto> {
            let url_ = this.baseUrl + "/api/services/app/ProductCategories/GetAll?";
            if (filter === null)
                throw new Error("The parameter 'filter' cannot be null.");
            else if (filter !== undefined)
                url_ += "Filter=" + encodeURIComponent("" + filter) + "&"; 
            if (nameFilter === null)
                throw new Error("The parameter 'nameFilter' cannot be null.");
            else if (nameFilter !== undefined)
                url_ += "NameFilter=" + encodeURIComponent("" + nameFilter) + "&"; 
            if (sorting === null)
                throw new Error("The parameter 'sorting' cannot be null.");
            else if (sorting !== undefined)
                url_ += "Sorting=" + encodeURIComponent("" + sorting) + "&"; 
            if (skipCount === null)
                throw new Error("The parameter 'skipCount' cannot be null.");
            else if (skipCount !== undefined)
                url_ += "SkipCount=" + encodeURIComponent("" + skipCount) + "&"; 
            if (maxResultCount === null)
                throw new Error("The parameter 'maxResultCount' cannot be null.");
            else if (maxResultCount !== undefined)
                url_ += "MaxResultCount=" + encodeURIComponent("" + maxResultCount) + "&"; 
            url_ = url_.replace(/[?&]$/, "");
    
            let options_ : any = {
                observe: "response",
                responseType: "blob",
                headers: new HttpHeaders({
                    "Accept": "text/plain"
                })
            };
    
            return this.http.request("get", url_, options_).pipe(_observableMergeMap((response_ : any) => {
                return this.processGetAll(response_);
            })).pipe(_observableCatch((response_: any) => {
                if (response_ instanceof HttpResponseBase) {
                    try {
                        return this.processGetAll(<any>response_);
                    } catch (e) {
                        return <Observable<PagedResultDtoOfGetProductCategoryForViewDto>><any>_observableThrow(e);
                    }
                } else
                    return <Observable<PagedResultDtoOfGetProductCategoryForViewDto>><any>_observableThrow(response_);
            }));
        }
    
        protected processGetAll(response: HttpResponseBase): Observable<PagedResultDtoOfGetProductCategoryForViewDto> {
            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 : JSON.parse(_responseText, this.jsonParseReviver);
                result200 = PagedResultDtoOfGetProductCategoryForViewDto.fromJS(resultData200);
                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<PagedResultDtoOfGetProductCategoryForViewDto>(<any>null);
        }
        ```
    
  • 0
    maliming created
    Support Team

    hi This is like a problem with net core 3.0, I am checking it.

  • 0
    maliming created
    Support Team

    This is a change in Swashbuckle.AspNetCore 5.x version. https://github.com/aspnetzero/aspnet-zero-core/pull/2750/files#diff-ca7c70741c6778bcc051a0512c7b60a9R28 https://github.com/aspnetzero/aspnet-zero-core/pull/2750/files#diff-95b60e9b66067387865beebdd219fadb

    But I searched for a long time and didn't find any information about these.

    I'm not sure if custom SwaggerNullableParameterFilter may cause other problems. However But for now, SwaggerNullableParameterFilter can solve it.

    After applying SwaggerNullableParameterFilter

    public class SwaggerNullableParameterFilter : IParameterFilter
    {
    	public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
    	{
    		if (!parameter.Schema.Nullable &&
    			(context.ApiParameterDescription.Type.IsNullable() || !context.ApiParameterDescription.Type.IsValueType))
    		{
    			parameter.Schema.Nullable = true;
    		}
    	}
    }
    
    
  • 0
    maliming created
    Support Team

    https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1528

  • 0
    morindo created

    Thank you very much @maliming, it just works!

    /**
     * @param filter (optional) 
     * @param nameFilter (optional) 
     * @param sorting (optional) 
     * @param skipCount (optional) 
     * @param maxResultCount (optional) 
     * @return Success
     */
    getAll(filter: string | null | undefined, nameFilter: string | null | undefined, sorting: string | null | undefined, skipCount: number | undefined, maxResultCount: number | undefined): Observable&lt;PagedResultDtoOfGetProductCategoryForViewDto&gt; {
        let url_ = this.baseUrl + "/api/services/app/ProductCategories/GetAll?";
        if (filter !== undefined)
            url_ += "Filter=" + encodeURIComponent("" + filter) + "&"; 
        if (nameFilter !== undefined)
            url_ += "NameFilter=" + encodeURIComponent("" + nameFilter) + "&"; 
        if (sorting !== undefined)
            url_ += "Sorting=" + encodeURIComponent("" + sorting) + "&"; 
        if (skipCount === null)
            throw new Error("The parameter 'skipCount' cannot be null.");
        else if (skipCount !== undefined)
            url_ += "SkipCount=" + encodeURIComponent("" + skipCount) + "&"; 
        if (maxResultCount === null)
            throw new Error("The parameter 'maxResultCount' cannot be null.");
        else if (maxResultCount !== undefined)
            url_ += "MaxResultCount=" + encodeURIComponent("" + maxResultCount) + "&"; 
        url_ = url_.replace(/[?&]$/, "");
    
        let options_ : any = {
            observe: "response",
            responseType: "blob",			
            headers: new HttpHeaders({
                "Accept": "text/plain"
            })
        };
    
  • 0
    morindo created

    So adding SwaggerNullableParameterFilter class is wokring, it remove the null check.

    Now, on the nswag (refresh.bat) generation process, how can we opt out null parameter in query string or replace null value by empty string so encodeURIComponent does not encode null to "null". Is there a way to do this? I've been searching this morning and can't find anything to fix it.

    Example:

    getAll(filter: string | null | undefined, productCategoryIdFilter: string | null | undefined, taxRateIdFilter: string | null | undefined, sorting: string | null | undefined, skipCount: number | undefined, maxResultCount: number | undefined): Observable<PagedResultDtoOfGetProductForViewDto> { let url_ = this.baseUrl + "/api/services/app/Products/GetAll?"; if (filter !== undefined) url_ += "Filter=" + encodeURIComponent("" + filter) + "&"; if (productCategoryIdFilter !== undefined) url_ += "ProductCategoryIdFilter=" + encodeURIComponent("" + productCategoryIdFilter) + "&"; if (taxRateIdFilter !== undefined) url_ += "TaxRateIdFilter=" + encodeURIComponent("" + taxRateIdFilter) + "&"; if (sorting !== undefined) url_ += "Sorting=" + encodeURIComponent("" + sorting) + "&"; if (skipCount === null) throw new Error("The parameter 'skipCount' cannot be null."); else if (skipCount !== undefined) url_ += "SkipCount=" + encodeURIComponent("" + skipCount) + "&"; if (maxResultCount === null) throw new Error("The parameter 'maxResultCount' cannot be null."); else if (maxResultCount !== undefined) url_ += "MaxResultCount=" + encodeURIComponent("" + maxResultCount) + "&"; url_ = url_.replace(/[?&]$/, "");

    The line:

    if (productCategoryIdFilter !== undefined) url_ += "ProductCategoryIdFilter=" + encodeURIComponent("" + productCategoryIdFilter) + "&";

    Should be:

    if (productCategoryIdFilter !== undefined) url_ += "ProductCategoryIdFilter=" + encodeURIComponent(productCategoryIdFilter ? "" + productCategoryIdFilter : "") + "&";

    Thank you,

  • 0
    maliming created
    Support Team

    hi @morindo I also didn't find any solution, you can pass the parameter as undefined

  • 0
    morindo created

    Thank you very much maliming. Cheers!

  • 0
    maliming created
    Support Team

    You're welcome, let's wait for the issue of Swashbuckle.

  • 0
    ismcagdas created
    Support Team

    This issue is closed because of no recent activity. Please create a new issue if you are still having this problem.