Why cannot sent XML example request via Swagger UI?
Most of the web apps written in .NET Core supports application/json as request body content type just like response content type. This is the default configuration when creating a new app via Visual Studio or dotnet new command.
But it may happen that we will need to enable XML support for body / response (f.e. to support a legacy system). In this article, we will show how to do it and what problems it causes.
To enable XML handling, we need to register formatter - a class, that will handle (de)serialization of body/response based on the Content-Type header value.
The formatters are not registered by default, so we need to add them (one handles serialization and the second - deserialization):
public void ConfigureServices(IServiceCollection services)
            {
                //...
                services.AddControllers()
                    .AddXmlSerializerFormatters();
                //...
            }
            ⚠️ Warning! In this article we are omitting other XML (de)serialization method: by using
DataContractSerializer(viaAddDataContractsSerializerFormatters). As you can imagine,AddXmlSerializerFormatterswill useXmlSerializerclass while performing parsing step. You can find the differences between both serializers herej
Unfortunately, it is not enough. Our app will ignore Accept header value by default. In this case, our XML body will be deserialized properly but a response will be sent as json:
public class TestModel
            {
                public string TestProp { get; set; }
            }
            
            [HttpPost]
            public TestModel Post(TestModel model)
            {
                return model;
            }
            curl -X POST "https://localhost:5001/WeatherForecast" -H  "accept: text/plain" -H  "Content-Type: application/xml" -d "<?xml version=\"1.0\" encoding=\"UTF-8\"?><TestModel>\t<testProp>testVal</testProp></TestModel>" | jq .
            {
              "testProp": null
            }
            To fix this issue, we need to tweak the MVC configuration a little bit:
public void ConfigureServices(IServiceCollection services)
            {
                //...
                services.AddControllers(options => options.RespectBrowserAcceptHeader = true)
                    .AddXmlSerializerFormatters();
                //...
            }
            Great! Everything is working as expected and what's more, Swagger UI will display all of the supported content types:

JSON is case-insensitive by design. XML is not. What is more, Swagger UI prepares sample requests using camelCase but XmlSerializer expects PascalCase convention.
That is why in the request example before, the property value was set to null in the response - XmlDeserializer omitted  TestVal as request contained testVal...
The first solution for our problem is proper property decoration with an attribute that will specify XML node name:
public class TestModel
            {
                [XmlElement("testProp")]
                public string TestProp { get; set; }
            }
            XmlElement does the job.
⚠️ Attention! We need to remember that
PascalCaseis no longer supported in this approach as XML is case-sensitive.
To enforce PascalCase we need to set one property:
services.AddControllers()
                .AddXmlSerializerFormatters()
                .AddJsonOptions(options =>
                    options.JsonSerializerOptions.PropertyNamingPolicy = null);
            Yup! For some reason, the XML serializer looks into json configuration. This approach comes with the one main drawback, however. As you will see, the json examples will also use PascalCase convention. Our application will work w/o any problems, but it is not how the JSON standard looks like.
To overcome this problem we need to manually fix the way that schema (and examples based on schema) is generated for the XML content type:
public class XmlPropertyNamingFilter : ISchemaFilter
            {
                public void Apply(OpenApiSchema model, SchemaFilterContext context)
                {
                    if (model.Properties == null) 
                        return;
                    foreach (var entry in model.Properties)
                    {
                        var name = entry.Key;
                        entry.Value.Xml = new OpenApiXml
                        {
                            Name = string.Concat(name[0].ToString().ToUpper(), name.AsSpan(1))
                        };
                    }
                }
            }
            We need just add the newly-created class to the schema filters of Swagger:
services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication1", Version = "v1" });
                c.SchemaFilter<XmlPropertyNamingFilter>();
            });
            ⚠️ Warning! Remember to remove
options.JsonSerializerOptions.PropertyNamingPolicy = nullif set before.
There is another issue for XML schema while using Swagger: For the DTO:
public class TestModel
            {
                [XmlElement("testProp")]
                public string TestProp { get; set; }
            
                public TestModelNested[] NestedChildren { get; set; }
            }
            
            public class TestModelNested
            {
                [XmlElement("otherProp")]
                public string OtherProp { get; set; }
            }
            we got the following example:
<?xml version="1.0" encoding="UTF-8"?>
            <TestModel>
                <testProp>string</testProp>
                <nestedChildren>
                    <otherProp>string</otherProp>
                </nestedChildren>
            </TestModel>
            Improperly serialized collection is the issue here, it's obvious. Unfortunately, it is a known bug in Swagger: Github Issue
To solve the issue, we need to tweak schema generation just like in PascalCase case:
internal class XmlSchemaFilter : ISchemaFilter
            {
                public void Apply(OpenApiSchema schema, SchemaFilterContext context)
                {
                    if (TypeHasAttribute(context.Type, typeof(XmlRootAttribute)))
                    {
                        schema.Xml = new OpenApiXml
                        {
                            Name = GetElementNameFromAttribute(context.Type, typeof(XmlRootAttribute))
                        };
                    }
                    else if (TypeHasAttribute(context.Type, typeof(XmlTypeAttribute)))
                    {
                        schema.Xml = new OpenApiXml
                        {
                            Name = GetElementNameFromAttribute(context.Type, typeof(XmlTypeAttribute))
                        };
                    }
                    else if (TypeHasAttribute(context.Type, typeof(XmlArrayAttribute)))
                    {
                        schema.Xml = new OpenApiXml
                        {
                            Name = GetElementNameFromAttribute(context.Type, typeof(XmlArrayAttribute)), 
                            Wrapped = true
                        };
                    }
                    else if (context.MemberInfo is not null && TypeHasAttribute(context.MemberInfo, typeof(XmlTypeAttribute)))
                    {
                        schema.Xml = new OpenApiXml
                        {
                            Name = GetElementNameFromAttribute(context.MemberInfo, typeof(XmlTypeAttribute))
                        };
                    }
                    else if (context.MemberInfo is not null && TypeHasAttribute(context.MemberInfo, typeof(XmlArrayAttribute)))
                    {
                        schema.Xml = new OpenApiXml
                        {
                            Name = GetElementNameFromAttribute(context.MemberInfo, typeof(XmlArrayAttribute)),
                            Wrapped = true
                        };
                    }
                }
            
                private static string GetElementNameFromAttribute(MemberInfo type, Type attributeType) =>
                    type.CustomAttributes.Single(a => a.AttributeType == attributeType)
                        .ConstructorArguments.First(a => a.ArgumentType == typeof(string))
                        .Value?
                        .ToString();
            
                private static bool TypeHasAttribute(MemberInfo type, Type attributeType) =>
                    type.CustomAttributes.Any(a => a.AttributeType == attributeType);
            }
            ⚠️ Warning! The code above was found somewhere on StackOverflow. Contact us if you know the source answer on SO site, please.
As you can see, the fairly easy thing like XML support can burn time to configure it properly. Especially for developer, who worked the wgole time with JSON content-type 😊.
 
                
                              
PON. - PT. 10:00 - 18:00
                        
office@knsdata.com