Modify SwaggerGenerator so that it takes DefaultValueAttribute into account.
Given following operation:
/*
* Matches following urls:
* /api/myresource/
* /api/myresource?skip=10
* /api/myresource?skip=10?take=20
*/
[HttpGet]
public IActionResult Index(MyQueryParams query)
{
}
public class MyQueryParams
{
[DefaultValue(0)]
public int Skip { get; set; } = 0;
[DefaultValue(10)]
public int Top { get; set; } = 10;
}
Swashbucle should generate the parameters as Optional and with Default value:
"get": {
"parameters": [
{
"name": "skip",
"in": "query",
"required": false,
"default": 0,
"type": "integer",
"format": "int32"
},
{
"name": "top",
"in": "query",
"required": false,
"default": 10,
"type": "integer",
"format": "int32"
}
]
}
@Liero you can add an operation filter as a quick fix, I wrote one that supports both property DefaultValue attributes and default values in method parameters:
// Adds Swagger support for optional parameters with default values
public class UpdateOptionalParamatersWithDefaultValues : IOperationFilter
{
private readonly MvcJsonOptions _mvcJsonOptions;
public UpdateOptionalParamatersWithDefaultValues(IOptions<MvcJsonOptions> mvcJsonOptions)
{
_mvcJsonOptions = mvcJsonOptions.Value;
}
public void Apply(Operation operation, OperationFilterContext context)
{
if (operation.Parameters == null || !operation.Parameters.Any())
{
return;
}
var parameterValuePairs = context.ApiDescription.ParameterDescriptions
.Where(parameter => GetDefaultValueAttribute(parameter) != null || GetParameterInfo(parameter).HasDefaultValue)
.ToDictionary(parameter => parameter.Name, GetDefaultValue);
foreach (var parameter in operation.Parameters)
{
if (parameterValuePairs.TryGetValue(parameter.Name, out var defaultValue))
{
parameter.Extensions.Add("default", defaultValue);
parameter.Required = false;
}
}
}
private DefaultValueAttribute GetDefaultValueAttribute(ApiParameterDescription parameter)
{
if (!(parameter.ModelMetadata is DefaultModelMetadata metadata) || metadata.Attributes.PropertyAttributes == null)
{
return null;
}
return metadata.Attributes.PropertyAttributes
.OfType<DefaultValueAttribute>()
.FirstOrDefault();
}
public ParameterInfo GetParameterInfo(ApiParameterDescription parameter)
{
return ((ControllerParameterDescriptor) parameter.ParameterDescriptor).ParameterInfo;
}
private object GetDefaultValue(ApiParameterDescription parameter)
{
var parameterInfo = GetParameterInfo(parameter);
if (parameterInfo.HasDefaultValue)
{
if (parameter.Type.IsEnum)
{
var stringEnumConverter = _mvcJsonOptions.SerializerSettings.Converters
.OfType<StringEnumConverter>()
.FirstOrDefault();
if (stringEnumConverter != null)
{
var defaultValue = parameterInfo.DefaultValue.ToString();
return stringEnumConverter.CamelCaseText ? ToCamelCase(defaultValue) : defaultValue;
}
}
return parameterInfo.DefaultValue;
}
var defaultValueAttribute = GetDefaultValueAttribute(parameter);
return defaultValueAttribute.Value;
}
private string ToCamelCase(string name)
{
return char.ToLowerInvariant(name[0]) + name.Substring(1);
}
}
@whyleee I've created somthing similar that allows to define examples without the need of modifying controler code. No I can defined examples at the configuration level in the following manner:
services.AddSwaggerGen(c =>
{
c.UseExampleFilter(e =>
{
e.DefineExample<AuthenticationController>(con => con.Login(new LoginCredentials
{
Login = "cezarypiatek",
Password = "secret"
}));
});
});
All required code is here:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Mvc.Controllers;
using Newtonsoft.Json;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace SwaggerHelpers.ExampleDescriptor
{
public static class ExmpleOperationFilterExtensions
{
public static void UseExampleFilter([NotNull] this SwaggerGenOptions app, [NotNull] Action<ExampleOperationFilterConfig> configure)
{
var configuration = new ExampleOperationFilterConfig();
configure(configuration);
app.OperationFilter<ExampleOperationFilter>(configuration);
}
}
public class ExampleOperationFilterConfig
{
private readonly IDictionary<string, Dictionary<string,string>> invocationExamples = new Dictionary<string, Dictionary<string,string>>();
[NotNull, Pure]
private static Dictionary<string, object> BuildParameterValuesFromExpression([NotNull] MethodCallExpression call)
{
var parameterValues = new Dictionary<string, object>();
var parameters = call.Method.GetParameters();
if (parameters.Length > 0)
{
for (int index = 0; index < parameters.Length; index++)
{
var parameterExpression = call.Arguments[index];
var value = Expression.Lambda(parameterExpression)
.Compile()
.DynamicInvoke();
parameterValues.Add(parameters[index].Name, value);
}
}
return parameterValues;
}
public void DefineExample<TController>([NotNull] Expression<Action<TController>> sampleInvocation)
{
var methodCall= (MethodCallExpression) sampleInvocation.Body;
var actionName = methodCall.Method.Name;
var controllerName = typeof(TController).Name.Replace("Controller", "");
var key = GetActionKey(controllerName, actionName);
this.invocationExamples[key] = BuildParameterValuesFromExpression(methodCall)
.ToDictionary(x=>x.Key, x=> JsonConvert.SerializeObject(x.Value, Formatting.Indented));
}
[NotNull, Pure]
private static string GetActionKey(string controllerName, string actionName)
{
return $"{controllerName}_{actionName}";
}
[Pure]
public bool TryGetActionParametersExamples([NotNull] ControllerActionDescriptor c, out Dictionary<string, string> parametersValues)
{
var actionKey = GetActionKey(c.ControllerName, c.ActionName);
return this.invocationExamples.TryGetValue(actionKey, out parametersValues);
}
}
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
public class ExampleOperationFilter: IOperationFilter
{
private readonly ExampleOperationFilterConfig config;
/// <inheritdoc />
public ExampleOperationFilter(ExampleOperationFilterConfig config)
{
this.config = config;
}
/// <inheritdoc />
public void Apply([NotNull] Operation operation, [NotNull] OperationFilterContext context)
{
if (context.ApiDescription.ActionDescriptor is ControllerActionDescriptor actionDescriptor && operation.Parameters != null)
{
if (this.config.TryGetActionParametersExamples(actionDescriptor, out var parametersValues))
{
foreach (var parameter in operation.Parameters.OfType<BodyParameter>())
{
if (parametersValues.TryGetValue(parameter.Name, out var parameterValue))
{
parameter.Extensions.Add("default", parameterValue);
}
}
}
}
}
}
}
Published also as gist https://gist.github.com/cezarypiatek/d7cd41a0e7bf484e07a5ea9ea119c757
@whyleee:
I've modified your UpdateOptionalParamatersWithDefaultValues so that it better respects JsonSerializer settings when converting default value:
_jsonSerializer = JsonSerializer.CreateDefault(_mvcJsonOptions.Value.SerializerSettings);
if (parameterValuePairs.TryGetValue(parameter.Name, out var defaultValue))
{
parameter.Required = false;
if (defaultValue != null)
{
var jValue = (JValue)JValue.FromObject(defaultValue, _jsonSerializer);
parameter.Extensions.Add("default", jValue.Value);
}
}
in that case you don't need special care for enums in GetDefaultValue
entire code:
// Adds Swagger support for optional parameters with default values
public class UpdateOptionalParamatersWithDefaultValues : IOperationFilter
{
JsonSerializer _jsonSerializer;
public UpdateOptionalParamatersWithDefaultValues(IOptions<MvcJsonOptions> mvcJsonOptions)
{
_jsonSerializer = JsonSerializer.CreateDefault(mvcJsonOptions.Value.SerializerSettings);
}
public void Apply(Operation operation, OperationFilterContext context)
{
if (operation.Parameters == null) return;
var parameterValuePairs = context.ApiDescription.ParameterDescriptions
.Where(parameter => GetDefaultValueAttribute(parameter) != null || GetParameterInfo(parameter).HasDefaultValue)
.ToDictionary(parameter => parameter.Name, GetDefaultValue);
foreach (var parameter in operation.Parameters)
{
if (parameterValuePairs.TryGetValue(parameter.Name, out var defaultValue))
{
parameter.Required = false;
if (defaultValue != null)
{
var jValue = (JValue)JValue.FromObject(defaultValue, _jsonSerializer);
parameter.Extensions.Add("default", jValue.Value);
}
}
}
}
private DefaultValueAttribute GetDefaultValueAttribute(ApiParameterDescription parameter)
{
return (parameter.ModelMetadata as DefaultModelMetadata)?
.Attributes.PropertyAttributes?
.OfType<DefaultValueAttribute>()
.FirstOrDefault();
}
public ParameterInfo GetParameterInfo(ApiParameterDescription parameter)
{
return ((ControllerParameterDescriptor)parameter.ParameterDescriptor).ParameterInfo;
}
private object GetDefaultValue(ApiParameterDescription parameter)
{
var parameterInfo = GetParameterInfo(parameter);
if (parameterInfo.HasDefaultValue)
{
return parameterInfo.DefaultValue;
}
else
{
return GetDefaultValueAttribute(parameter)?.Value;
}
}
}
Can it use the "default" value as the example ? Like:
public class Product
{
public int Id { get; set; }
[DefaultValue("descript something")]
public string Description { get; set; }
}
and at the doc show:

but not description: "string"
I found that is hard code in swagger-ui : https://github.com/swagger-api/swagger-ui/blob/12a9fbc0ca6470ac6437cfd6eaa56ccf576d40dd/src/core/plugins/samples/fn.js#L5
@Liero - the specific issue that you posted _should_ be resolved with the following commit - 390027d6e44094d27c2fe537c653c069070a3eee. This will be generally available with the 3.0.0 release (due in a week or two). In the meantime, you are encouraged to try out the following preview package - https://www.myget.org/feed/domaindrivendev/package/nuget/Swashbuckle.AspNetCore.
i'm finding that using DefaultValueAttribute on properties is not propagating values to swagger.
It should work - can you post an example of your action signature and model class?
I'm seeing the same thing. In my case, if I bind using FromBody the default values are present, but if I bind FromQuery they aren't.
public class LocationQuery{
[DefaultValue(52.000)]
public double latitude {get;set;} = 52.00;
[DefaultValue(105.000)]
public double longitude {get;set;} = 105.00;
}
If my action looks like this, the default values are present:
public async Task<ActionResult<LocationSet>> Locations([FromBody]LocationQuery query)
If my action looks like this, the default values are not present:
public async Task<ActionResult<LocationSet>> Locations([FromQuery]LocationQuery query)
Same result if I annotate the individual properties with [FromQuery] instead of the action parameter.
I also have a SchemaFilter applied to fix up the example json blob in Swagger UI. I notice that if I'm binding using FromQuery there is never anything with a SystemType of LocationQuery passed in. Might not be related but I noticed it at the same time.
public class AddExamples : ISchemaFilter
{
public void Apply(Schema schema, SchemaFilterContext context)
{
if(context.SystemType == typeof(LocationQuery ))
{
schema.Example = new LocationQuery ()
{
};
}
}
}
my use is essentially the OP's example. I have a GET with a [FromQuery] model to house all the query params.
are there any other notes i can provide on this to assist in a repro?
bumping. was troubleshooting this issue again only to find my comments here in researching the problem...
use
public class Product
{
public int Id { get; set; }
///<summary>
///</summary>
///<example>example value</example>
public string Description { get; set; }
}
as a short term bypass to the problem, I've added @whyleee's class to my project so that the DefaultValueAttribute value lands in the swagger doc. @zhaokuohaha, while that may make a comment land in the swagger doc, i'm looking to specifically populate the default field and not the description.
Default Value Attribute is still ignored when being used as FromQuery in versions 4.0.1 and 5.0.0. @whyleee's fix is very appreciated.
@zhaokuohaha
Relating to using DefaultValue to populate the examples in the UI, this is working for me
Decorate the desired properties in a model:
public class Test {
[DefaultValue("Hello")]
public string Text { get; set; }
}
Main filter:
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Project.Swashbuckle {
public class SchemaFilter : ISchemaFilter {
public void Apply(Schema schema, SchemaFilterContext context) {
if (schema.Properties == null) {
return;
}
foreach (PropertyInfo propertyInfo in context.SystemType.GetProperties()) {
// Look for class attributes that have been decorated with "[DefaultAttribute(...)]".
DefaultValueAttribute defaultAttribute = propertyInfo
.GetCustomAttribute<DefaultValueAttribute>();
if (defaultAttribute != null) {
foreach (KeyValuePair<string, Schema> property in schema.Properties) {
// Only assign default value to the proper element.
if (ToCamelCase(propertyInfo.Name) == property.Key) {
property.Value.Example = defaultAttribute.Value;
break;
}
}
}
}
}
private string ToCamelCase(string name) {
return char.ToLowerInvariant(name[0]) + name.Substring(1);
}
}
}
Usage (Startup.js):
services.AddSwaggerGen(c => {
// ...
c.SchemaFilter<SchemaFilter>();
});
@scottcwilliams511 馃憤 鉂わ笍 !
Commit 192aa2d70f057fc2805f58d51b6e859f1f6b07ce adds out-of-the-box support for this
@zhaokuohaha
Relating to using DefaultValue to populate the examples in the UI, this is working for me
Decorate the desired properties in a model:
public class Test { [DefaultValue("Hello")] public string Text { get; set; } }Main filter:
using System.Collections.Generic; using System.ComponentModel; using System.Reflection; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; namespace Project.Swashbuckle { public class SchemaFilter : ISchemaFilter { public void Apply(Schema schema, SchemaFilterContext context) { if (schema.Properties == null) { return; } foreach (PropertyInfo propertyInfo in context.SystemType.GetProperties()) { // Look for class attributes that have been decorated with "[DefaultAttribute(...)]". DefaultValueAttribute defaultAttribute = propertyInfo .GetCustomAttribute<DefaultValueAttribute>(); if (defaultAttribute != null) { foreach (KeyValuePair<string, Schema> property in schema.Properties) { // Only assign default value to the proper element. if (ToCamelCase(propertyInfo.Name) == property.Key) { property.Value.Example = defaultAttribute.Value; break; } } } } } private string ToCamelCase(string name) { return char.ToLowerInvariant(name[0]) + name.Substring(1); } } }Usage (Startup.js):
services.AddSwaggerGen(c => { // ... c.SchemaFilter<SchemaFilter>(); });
If your API doesn't return camel cased values this may not work for some of your properties. Update the if condition to
if (propertyInfo.Name.ToLower() == property.Key.Replace("_", "").ToLower()) { . property.Value.Example = defaultAttribute.Value; break; }
Commit 192aa2d adds out-of-the-box support for this
Is this going to be backported to 4.x, since (for obvious reasons) my employer doesn't allow the use of RC software in production projects?
I鈥檓 afraid not. This is a one way train!
@domaindrivendev
i've been keeping an eye on the status of that last build. any thoughts on when that train will be arriving? looking forward to getting off my fork
i am on 5.0.0-rc5 version and default values are not described in swagger doc.
example controller:
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/currencies")]
public class CurrencyController : BaseController
{
public CurrencyController(IMediator mediator)
: base(mediator)
{
}
[HttpGet]
[ProducesResponseType(typeof(PagedResultsDto<CurrencyDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public Task<IActionResult> GetCurrenciesAsync(GetCurrenciesQuery request, CancellationToken cancelToken = default) => HandleAsync(request, cancelToken);
}
example request type that has the query and default value attributes:
public class GetCurrenciesQuery : PagedQueryBase<CurrencyDto>
{
[FromQuery]
public string Search { get; set; }
[FromQuery]
[DefaultValue(nameof(CurrencyDto.Id))]
[ValidPropertyName(typeof(CurrencyDto), StringComparison.InvariantCultureIgnoreCase)]
public override string SortBy { get; set; } = nameof(CurrencyDto.Id).ToCamelCase();
[FromQuery]
[DefaultValue(SortOrderValue.Asc)]
[EnumDataType(typeof(SortOrderValue))]
public override SortOrderValue? SortOrder { get; set; } = SortOrderValue.Asc;
[FromQuery]
[DefaultValue(1)]
[Range(1, int.MaxValue)]
public override int? PageIndex { get; set; } = 1;
[FromQuery]
[DefaultValue(50)]
[Range(1, 50)]
public override int? PageSize { get; set; } = 50;
}
swagger doc:
{
"openapi": "3.0.1",
"info": {
"title": "v1",
"version": "v1"
},
"paths": {
"/api/v1/currencies": {
"get": {
"tags": [
"Currency"
],
"parameters": [
{
"name": "search",
"in": "query",
"schema": {
"type": "string",
"nullable": true
}
},
{
"name": "sortBy",
"in": "query",
"schema": {
"type": "string",
"nullable": true
}
},
{
"name": "sortOrder",
"in": "query",
"schema": {
"$ref": "#/components/schemas/SortOrderValueNullable"
}
},
{
"name": "pageIndex",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"nullable": true
}
},
{
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"nullable": true
}
}
],
....
This is broken again in 6.0.7.
In 5.6.3 works fine tho.
using net 5
Reopen this issue or should I make a new one?
[Route("api/[controller]/[action]")]
[ApiController]
[Produces(MediaTypeNames.Application.Json, Type = typeof(void))]
public class ExampleController : ControllerBase {
[HttpGet]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult TestBadRequestAction([FromQuery, DefaultValue("CorrectValue")] string input) {
if (input == "CorrectValue")
return Ok();
return BadRequest();
}
}
@SirGordon - if the parameter is optional, shouldn't it be expressed in the action signature as follows?
public IActionResult TestBadRequestAction([FromQuery]string input = "CorrectValue")
This is the de facto way to implement optional parameters and their default values, and SB will pick this up without issue. Is there a reason why your using the DefaultValue attribute instead?
@domaindrivendev the parameter is not optional, it is required. In version 5.6.3 it behaves as expected.
Only works with [ApiController] tho. Without this attribute, asp.net core uses [DefaultValue] value when not provided, instead of returning ProblemDetails with BadRequest.
I'm a bit confused by your last comment as you're describing a scenario where a value is _not provided_ but you're also saying the parameter is required! If the implementation auto-populates the parameter when _not provided_, then by definition that parameter is "optional".
So, I believe the "optional parameter" approach I described above will accomplish exactly the same behavior (and is more succinct IMO) that you've described, and SB will honor it too. Have you tried it?
Of course, you're correct in pointing out the regression bug, and I'll look into getting that fixed, but in the meantime I suggest you use the approach I suggested as it's a perfectly valid approach to accomplishing the same thing, unless I'm missing something.
Just pushed c9eb9ddc89be507d762a0af08734fb90cdcde812 to fix the regression, available in 6.1.0 nuget.
To make the goal clear: have a mandatory parameter while providing a default value in swagger-ui only, so it affects only swagger-ui but not the api itself.
Most helpful comment
@Liero you can add an operation filter as a quick fix, I wrote one that supports both property
DefaultValueattributes and default values in method parameters: