We are using Swashbuckle to document our WebAPI project (using Owin) and are trying to modify the generated Swagger file of Swashbuckle.
With the DescribeAllEnumsAsStrings() and an enum property like below, we get an expected result:
class MyResponseClass {
public Color color;
}
enum Color {
LightBlue,
LightRed,
DarkBlue,
DarkRed
}
Swagger generated result:
"color": {
"enum": [
"LightBlue",
"LightRed",
"DarkBlue",
"DarkRed"
],
"type": "string"
},
The challenge for us is that we have some properties that are of type string but we are actually treating them as enum types. For example:
class MyResponseClass {
public string color;
}
The only possible values for this property are dark-blue, dark-red, light-blue, light-red.
So, we want something like below as the result:
"color": {
"enum": [
"light-blue",
"light-red",
"dark-blue",
"dark-red"
],
"type": "string"
},
We have lots of these properties with different values in different classes. It would be great to have a custom attribute like below to make it generic. I can't figure out how to create such an attribute and use it in Swashbuckle DocumentFilters or OperationFilters:
public MyEndpointResponseClass {
[StringEnum("booked", "confirmed", "reserved")]
public string status;
// Other properties
}
public MyEndpointRequestClass {
[StringEnum("dark-blue", "dark-red", "light-blue", "light-red")]
public string color;
// Other properties
}
Instead of a custom attribute (StringEnum) use an attribute that swagger already knows about, a little know attribute (I've never used it before):
[RegularExpression("^(dark-blue|dark-red|light-blue|light-red)")]
That will inject into the parameter.pattern and then we can read it from the IDocumentSchema and transform it into an enum, here is my code:
private class StringEnumDocumentFilter : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry s, IApiExplorer a)
{
if (swaggerDoc.paths != null)
{
foreach (var path in swaggerDoc.paths)
{
ProcessOperation(path.Value.get);
ProcessOperation(path.Value.put);
ProcessOperation(path.Value.post);
ProcessOperation(path.Value.delete);
ProcessOperation(path.Value.options);
ProcessOperation(path.Value.head);
ProcessOperation(path.Value.patch);
}
}
}
private void ProcessOperation(Operation op)
{
if (op != null)
{
foreach (var param in op.parameters)
{
if (param.pattern != null)
{
param.#enum = param.pattern
.Replace("^", "")
.Replace("(", "")
.Replace(")", "")
.Split('|');
}
}
}
}
}
Here is a working example:
http://swashbuckletest.azurewebsites.net/swagger/ui/index?filter=TestStringEnum#/TestStringEnum/TestStringEnum_Post
And the code behind that is on GitHub:
TestStringEnumController.cs
SwaggerConfig.cs#L389
Related
I needed to add enum description for schema of a request in swagger, so I defined this filter :
public class EnumSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
if (context.Type.IsEnum)
{
model.Enum.Clear();
var names = Enum.GetNames(context.Type);
names
.ToList()
.ForEach(n => model.Enum.Add(new OpenApiString($"{n} : {(int)Enum.Parse(context.Type, n)}")));
model.Example = new OpenApiInteger((int)Enum.Parse(context.Type, names[0]));
}
}
}
However the issue here is that when I want to try that enum in a get request, I see the following option :
Is there a way to change this to only show enum integer values when user want to select ?
I could solve the issue by defining a custom ParametersFilter :
public class SchemaParametersFilter : IParameterFilter
{
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
{
var type = context.ParameterInfo?.ParameterType;
if (type == null)
return;
if (type.IsEnum && parameter.In == ParameterLocation.Query)
{
var names = Enum.GetNames(type);
parameter.Schema.Enum = names.OfType<string>().Select(p => new OpenApiInteger((int)Enum.Parse(type, p))).ToList<IOpenApiAny>();
}
}
}
It seems swashbuckle\swagger-ui (5.6 - using swagger-ui) does not generate example XML correctly when the model has a property that is a list.
To see this issue:
1 - Create an empty webapi project (I'm using asp.net)
2 - Add a couple of example models (I went with Customer + Order for testing)
public class Customer
{
public string AccountNumber { get; set; }
[XmlArray("Orders"),XmlArrayItem("Order")]
public List<Order> Orders { get;set; }
}
public class Order
{
public string OrderNumber { get;set; }
}
3 - Create a controller using FromBody to bind to model
public class CustomerController : ApiController
{
public void Post([FromBody]Customer customer)
{
customer.ToString();
}
}
4 - Change web api config to allow simple XML
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.XmlFormatter.UseXmlSerializer = true; //ADD THIS
}
}
5 - Run site and using /swagger ui change parameter content type to xml and select example model. You will find the example is as follows.
<?xml version="1.0"?>
<Customer>
<AccountNumber>string</AccountNumber>
<Orders>
<OrderNumber>string</OrderNumber>
</Orders>
</Customer>
6 - Submit this with a breakpoint on the customer.ToString() line in the controller and you will find the Orders collection is empty
7 - Modify the XML in swagger-ui to the following and submit:
<?xml version="1.0"?>
<Customer>
<AccountNumber>string</AccountNumber>
<Orders>
<Order><OrderNumber>string</OrderNumber></Order>
</Orders>
</Customer>
8 - The Customer.Orders collection is now correctly populated.
What is the best way to fix or workaround this in Swashbuckle?
(There are a few discussions around this and whether it's a bug in swagger-ui or Swashbuckle, but I'm specifically interested in working around it using Swashbuckle)
I have found the following works:
1 - Add an implementation of ISchemaFilter
internal class ApplySchemaVendorExtensions : ISchemaFilter
{
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
// Fix issues with xml array examples not generating correctly
if (!type.IsValueType)
{
schema.xml = new Xml { name = type.Name };
if(schema.properties != null)
{
foreach (var property in schema.properties)
{
//Array property, which wraps its elements
if (property.Value.type == "array")
{
property.Value.xml = new Xml
{
name = $"{property.Key}",
wrapped = true
};
}
}
}
}
}
}
2 - Comment this line into SwaggerConfig.cs
c.SchemaFilter<ApplySchemaVendorExtensions>();
Repeat the test in the Question and the example XML now works directly. As always I'm curious if there's a better solution...
EDIT: Actually this oddly works in the original project I have this issue, but in the small reproduction project for this Quesion it behaves slightly differently! I will edit this answer when I find why...
Thanks to #mutex, but I found that I needed to make another adjustment to it:
internal class SwaggerFixArraysInXmlFilter : Swashbuckle.Swagger.ISchemaFilter
{
// this fixes a Swagger bug that wasn't generating correct XML elements around List<> or array[] types
public void Apply(Swashbuckle.Swagger.Schema schema, Swashbuckle.Swagger.SchemaRegistry schemaRegistry, System.Type type)
{
// Fix issues with xml array examples not generating correctly
if (!type.IsValueType)
{
schema.xml = new Swashbuckle.Swagger.Xml { name = type.Name };
if (schema.properties != null)
{
foreach (var property in schema.properties)
{
//Array property, which wraps its elements
if (property.Value.type == "array")
{
property.Value.xml = new Swashbuckle.Swagger.Xml
{
name = $"{property.Key}",
wrapped = true
};
property.Value.items.xml = new Swashbuckle.Swagger.Xml
{
name = $"{property.Value.items.type}",
wrapped = true
};
}
}
}
}
}
}
Thanks to #Abacus, but I found that I needed to make another adjustment to it. (String is not a ValueType, so it was renaming any string value to <String>string</String>... May have something to do with .NET Core 3.1)
internal class SwaggerFixArraysInXmlFilter : Swashbuckle.Swagger.ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
Type type = context.Type;
// Fix issues with xml array examples not generating correctly
if (!type.IsValueType && type.Name != "String")
{
schema.Xml = new OpenApiXml { Name = type.Name };
if (schema.Properties != null)
{
foreach (var property in schema.Properties)
{
//Array property, which wraps its elements
if (property.Value.Type == "array")
{
property.Value.Xml = new OpenApiXml
{
Name = $"{property.Key}",
Wrapped = true
};
property.Value.Items.Xml = new OpenApiXml
{
Name = $"{property.Value.Items.Type}",
Wrapped = true
};
}
}
}
}
}
If you are using .Net Core 2.2 with Swagger v5, you will need the below code set
internal class SwaggerFixArraysInXmlFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
Type type = context.Type;
// Fix issues with xml array examples not generating correctly
if (!type.IsValueType)
{
schema.Xml = new OpenApiXml { Name = type.Name };
if (schema.Properties != null)
{
foreach (var property in schema.Properties)
{
//Array property, which wraps its elements
if (property.Value.Type == "array")
{
property.Value.Xml = new OpenApiXml
{
Name = $"{property.Key}",
Wrapped = true
};
property.Value.Items.Xml = new OpenApiXml
{
Name = $"{property.Value.Items.Type}",
Wrapped = true
};
}
}
}
}
}
}
While Json Serialization I got below result:
{ "Action": 0 }
But I want to change the result as string instead of 0
{ "Action": "Include" }
public enum FAction
{
Include = 0,
}
I have property using this Enum.
public FAction Action { get; set; }
//...
Action = FAction.Include;
//...
I have used like this. To get the following result
{ "Action": "Include" }
what I have to do? Please, suggest any.
I began learn Groovy, and faced the challenge.
I have this code, that stores meta-data to object:
class Meta {
final MetaItem name
final MetaItem description
// ...
// And more fields with type MetaItem
// ...
Meta() {
name = new MetaItem("name")
description = new MetaItem("description")
}
void setName(String name) {
this.name.value = name
}
String getName() {
return this.name.value
}
void setDescription(String description) {
this.description.value = description
}
String getDescription() {
return this.description.value
}
// ...
// And more methods. Two for each field
// ...
}
class MetaItem {
private final def id
def value
MetaItem(String id) {
this.id = id
}
}
// Validating
def meta = new Meta()
assert meta.name == null
assert meta.description == null
meta.with {
name = "Name"
description = "Desc"
}
assert meta.name == "Name"
assert meta.description == "Desc"
print "Success!"
As you can see from the code, it increases quicly in volumes when new fields are added, because for each field you need to add two methods. Can this somehow be optimized? Redirect the assignment operation from object to his member. I've looked on Delegate, but this is not what I need.
P.S. I can't use access by .value because this class is used in Gradle extension and I need to configure it like this:
myExtension {
meta {
name = "Name"
description = "Desc"
// And many others
}
}
P.P.S. Sorry for my bad english, it's not my first language
Suppose I have a MvxViewModel with a string array property:
using System;
using Cirrious.MvvmCross.ViewModels;
namespace Foo {
public class FooViewModel : MvxViewModel {
private string[] mTexts;
public string[] Texts {
get { return mTexts; }
set {
mTexts = value;
RaisePropertyChanged(() => Texts);
}
}
public void Init()
{
Texts = new string[] { "foo", "bar" };
}
}
}
Suppose I also have an iOS MvxViewController with a single string property:
using System;
using Cirrious.MvvmCross.Touch.Views;
using Cirrious.MvvmCross.Binding.BindingContext;
namespace Foo {
public class FooView : MvxViewController {
public string Text { get; set; }
public override void ViewDidLoad () {
base.ViewDidLoad();
var bindset = this.CreateBindingSet<FooView, FooViewModel>();
bindset.Bind(Text).To("Texts[0]");
bindset.Apply();
}
}
}
How can I bind this string property, Text, to one position (say, 0) of the Texts array?
I have found sources (here and here) which indicate that this would be possible by what was done int the ViewDidLoad() method above, however, I get a warning: Unable to bind: source property source not found IndexedProperty:0 on String[].
What am I missing?
You can use a List or ObservableCollection instead of an Array:
private List<string> mTexts;
public List<string> Texts
{
get { return mTexts; }
set
{
mTexts = value;
RaisePropertyChanged(() => Texts);
}
}
I'm not to sure on Mvx support for Array type properties. I always tend to use an ObservableCollection or List. The error message you get when trying to bind to an Arrayseems to suggest it's struggling to do an index lookup on the Array.
Unable to bind: source property source not found IndexedProperty:0 on String[]
One approach that I tried and seemed to work was to implement an Indexer in the ViewModel and then bind to that.
ViewModel
string[] mTexts;
public string this[int index] => mTexts[index];
public void Init()
{
mTexts = new string[] { "foo", "bar" };
}
View Bindings
var bindset = this.CreateBindingSet<FooView, FooViewModel>();
bindset.Bind(Text).To(".[1]");
bindset.Apply();
As noted by xleon in the comments below, the period is optional. Using "[1]" will work as well.
Alternative with Lambda:
var bindset = this.CreateBindingSet<FooView, FooViewModel>();
bindset.Bind(Text).To(vm => vm[1]);
bindset.Apply();