diff --git a/src/NSwag.CodeGeneration/Models/OperationModelBase.cs b/src/NSwag.CodeGeneration/Models/OperationModelBase.cs index bd4393668..b6fc0b2cd 100644 --- a/src/NSwag.CodeGeneration/Models/OperationModelBase.cs +++ b/src/NSwag.CodeGeneration/Models/OperationModelBase.cs @@ -102,21 +102,28 @@ public string UnwrappedResultType { get { - var response = GetSuccessResponse(); - if (response.Value == null || response.Value.IsEmpty(_operation)) + TResponseModel response = GetSuccessResponseModel(); + if (response?.Response == null || response.Response.IsEmpty(_operation)) { return "void"; } - if (response.Value.IsBinary(_operation)) + if (response.Response.IsBinary(_operation)) { return _generator.GetBinaryResponseTypeName(); } - var isNullable = response.Value.IsNullable(_settings.CodeGeneratorSettings.SchemaType); - var schemaHasTypeNameTitle = response.Value.Schema?.HasTypeNameTitle; + bool isNullable = response.IsNullable; + + if (!isNullable) + { + // If one of the success types is nullable, we set the method return type to nullable as well. + isNullable = Responses.Any(r => r.IsSuccess && r.Type == response.Type + "?" && r.IsNullable); + } + + var schemaHasTypeNameTitle = response.Response.Schema?.HasTypeNameTitle; var hint = schemaHasTypeNameTitle != true ? "Response" : null; - return _generator.GetTypeName(response.Value.Schema, isNullable, hint); + return _generator.GetTypeName(response.Response.Schema, isNullable, hint); } } @@ -304,6 +311,24 @@ protected KeyValuePair GetSuccessResponse() return new KeyValuePair("default", _operation.ActualResponses.FirstOrDefault(r => r.Key == "default").Value); } + /// Gets the success response model, including type information. + /// The response model. + protected TResponseModel GetSuccessResponseModel() + { + if (Responses.Any(r => r.StatusCode == "200")) + { + return Responses.Single(r => r.StatusCode == "200"); + } + + var response = Responses.FirstOrDefault(r => HttpUtilities.IsSuccessStatusCode(r.StatusCode)); + if (response != null) + { + return response; + } + + return DefaultResponse; + } + /// Gets the name of the parameter variable. /// The parameter. /// All parameters. diff --git a/src/NSwag.CodeGeneration/Models/ResponseModelBase.cs b/src/NSwag.CodeGeneration/Models/ResponseModelBase.cs index f1a9b5838..ba05cca3d 100644 --- a/src/NSwag.CodeGeneration/Models/ResponseModelBase.cs +++ b/src/NSwag.CodeGeneration/Models/ResponseModelBase.cs @@ -15,7 +15,6 @@ namespace NSwag.CodeGeneration.Models public abstract class ResponseModelBase { private readonly IOperationModel _operationModel; - private readonly OpenApiResponse _response; private readonly OpenApiOperation _operation; private readonly JsonSchema _exceptionSchema; private readonly IClientGenerator _generator; @@ -37,7 +36,7 @@ protected ResponseModelBase(IOperationModel operationModel, string statusCode, OpenApiResponse response, bool isPrimarySuccessResponse, JsonSchema exceptionSchema, TypeResolverBase resolver, CodeGeneratorSettingsBase settings, IClientGenerator generator) { - _response = response; + Response = response; _operation = operation; _exceptionSchema = exceptionSchema; _generator = generator; @@ -50,6 +49,9 @@ protected ResponseModelBase(IOperationModel operationModel, ActualResponseSchema = response.Schema?.ActualSchema; } + /// The underlying response + internal OpenApiResponse Response { get; } + /// Gets the HTTP status code. public string StatusCode { get; } @@ -61,14 +63,14 @@ protected ResponseModelBase(IOperationModel operationModel, /// Gets the type of the response. public string Type => - _response.IsBinary(_operation) ? _generator.GetBinaryResponseTypeName() : + Response.IsBinary(_operation) ? _generator.GetBinaryResponseTypeName() : _generator.GetTypeName(ActualResponseSchema, IsNullable, "Response"); /// Gets a value indicating whether the response has a type (i.e. not void). public bool HasType => ActualResponseSchema != null; /// Gets or sets the expected child schemas of the base schema (can be used for generating enhanced typings/documentation). - public ICollection ExpectedSchemas => _response.ExpectedSchemas; + public ICollection ExpectedSchemas => Response.ExpectedSchemas; /// Gets a value indicating whether the response is of type date. public bool IsDate => ActualResponseSchema != null && @@ -77,21 +79,21 @@ protected ResponseModelBase(IOperationModel operationModel, _generator.GetTypeName(ActualResponseSchema, IsNullable, "Response") != "string"; /// Gets a value indicating whether the response requires a text/plain content. - public bool IsPlainText => !_response.Content.ContainsKey("application/json") && _response.Content.ContainsKey("text/plain"); + public bool IsPlainText => !Response.Content.ContainsKey("application/json") && Response.Content.ContainsKey("text/plain"); /// Gets a value indicating whether this is a file response. - public bool IsFile => IsSuccess && _response.IsBinary(_operation); + public bool IsFile => IsSuccess && Response.IsBinary(_operation); /// Gets the response's exception description. - public string ExceptionDescription => !string.IsNullOrEmpty(_response.Description) ? - ConversionUtilities.ConvertToStringLiteral(_response.Description) : + public string ExceptionDescription => !string.IsNullOrEmpty(Response.Description) ? + ConversionUtilities.ConvertToStringLiteral(Response.Description) : "A server side error occurred."; /// Gets the response schema. - public JsonSchema ResolvableResponseSchema => _response.Schema != null ? _resolver.GetResolvableSchema(_response.Schema) : null; + public JsonSchema ResolvableResponseSchema => Response.Schema != null ? _resolver.GetResolvableSchema(Response.Schema) : null; /// Gets a value indicating whether the response is nullable. - public bool IsNullable => _response.IsNullable(_settings.SchemaType); + public bool IsNullable => Response.IsNullable(_settings.SchemaType); /// Gets a value indicating whether the response type inherits from exception. public bool InheritsExceptionSchema => ActualResponseSchema?.InheritsSchema(_exceptionSchema) == true; @@ -110,9 +112,11 @@ public bool IsSuccess } var primarySuccessResponse = _operationModel.Responses.FirstOrDefault(r => r.IsPrimarySuccessResponse); + + // We should ignore nullability when evaluating if both responses have the same return type. return HttpUtilities.IsSuccessStatusCode(StatusCode) && ( primarySuccessResponse == null || - primarySuccessResponse.Type == Type + primarySuccessResponse.Type.TrimEnd('?') == Type.TrimEnd('?') ); } } @@ -121,9 +125,9 @@ public bool IsSuccess public bool ThrowsException => !IsSuccess; /// Gets the response extension data. - public IDictionary ExtensionData => _response.ExtensionData; + public IDictionary ExtensionData => Response.ExtensionData; /// Gets the produced mime type of this response if available. - public string Produces => _response.Content.Keys.FirstOrDefault(); + public string Produces => Response.Content.Keys.FirstOrDefault(); } } \ No newline at end of file diff --git a/src/NSwag.Generation.AspNetCore/Processors/OperationResponseProcessor.cs b/src/NSwag.Generation.AspNetCore/Processors/OperationResponseProcessor.cs index d2e757388..c217e45f4 100644 --- a/src/NSwag.Generation.AspNetCore/Processors/OperationResponseProcessor.cs +++ b/src/NSwag.Generation.AspNetCore/Processors/OperationResponseProcessor.cs @@ -7,6 +7,7 @@ //----------------------------------------------------------------------- using System.Globalization; +using System.Net; using System.Reflection; using Namotion.Reflection; using NJsonSchema; @@ -74,9 +75,9 @@ public bool Process(OperationProcessorContext operationProcessorContext) httpStatusCode = apiResponse.StatusCode.ToString(CultureInfo.InvariantCulture); } + var returnTypeAttributes = context.MethodInfo?.ReturnParameter?.GetCustomAttributes(false).OfType(); if (!IsVoidResponse(returnType)) { - var returnTypeAttributes = context.MethodInfo?.ReturnParameter?.GetCustomAttributes(false).OfType(); var contextualReturnType = returnType.ToContextualType(returnTypeAttributes); var nullableXmlAttribute = GetResponseXmlDocsElement(context.MethodInfo, httpStatusCode)?.Attribute("nullable"); @@ -84,10 +85,30 @@ public bool Process(OperationProcessorContext operationProcessorContext) nullableXmlAttribute.Value.Equals("true", StringComparison.OrdinalIgnoreCase) : _settings.SchemaSettings.ReflectionService.GetDescription(contextualReturnType, _settings.DefaultResponseReferenceTypeNullHandling, _settings.SchemaSettings).IsNullable; + if (int.TryParse(httpStatusCode, out int statusCodeResult) && + _settings.ResponseStatusCodesToTreatAsNullable.Any(code => + code == (HttpStatusCode)statusCodeResult)) + { + // If the response code of this response type is in the settings list, we treat is a nullable. + isResponseNullable = true; + } + response.IsNullableRaw = isResponseNullable; response.Schema = context.SchemaGenerator.GenerateWithReferenceAndNullability( contextualReturnType, isResponseNullable, context.SchemaResolver); } + else + { + if (int.TryParse(httpStatusCode, out int statusCodeResult) && + _settings.ResponseStatusCodesToTreatAsNullable.Any(code => + code == (HttpStatusCode)statusCodeResult)) + { + // If the response code of this response type is in the settings list, we treat is a nullable. + response.IsNullableRaw = true; + response.Schema = context.SchemaGenerator.Generate(typeof(void)); + response.Schema.IsNullableRaw = true; + } + } context.OperationDescription.Operation.Responses[httpStatusCode] = response; } diff --git a/src/NSwag.Generation/OpenApiDocumentGeneratorSettings.cs b/src/NSwag.Generation/OpenApiDocumentGeneratorSettings.cs index 6330bd9e7..720d2ca45 100644 --- a/src/NSwag.Generation/OpenApiDocumentGeneratorSettings.cs +++ b/src/NSwag.Generation/OpenApiDocumentGeneratorSettings.cs @@ -6,6 +6,7 @@ // Rico Suter, mail@rsuter.com //----------------------------------------------------------------------- +using System.Net; using Namotion.Reflection; using Newtonsoft.Json; using NJsonSchema; @@ -50,6 +51,13 @@ public OpenApiDocumentGeneratorSettings() /// Gets or sets the default response reference type null handling when no nullability information is available (if NotNullAttribute and CanBeNullAttribute are missing, default: NotNull). public ReferenceTypeNullHandling DefaultResponseReferenceTypeNullHandling { get; set; } + /// + /// Gets or sets a value indicating that the api method/action response should be considered nullable if this response type is documented, even if it is a void response. + /// Allows for things like a 204 No Content to be treated as nullable without decorating with the + /// If the action is decorated with the this setting will be ignored for that action. + /// + public HttpStatusCode[] ResponseStatusCodesToTreatAsNullable { get; set; } = []; + /// Gets or sets a value indicating whether to generate x-originalName properties when parameter name is different in .NET and HTTP (default: true). public bool GenerateOriginalParameterNames { get; set; } = true;