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.

Enabling XML support for WebApi

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 (via AddDataContractsSerializerFormatters). As you can imagine, AddXmlSerializerFormatters will use XmlSerializer class 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:

Content Types

camelCase handling

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...

Handling camleCase in XmlSerializer

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 PascalCase is no longer supported in this approach as XML is case-sensitive.

Swagger UI - handling PascalCase

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 = null if set before.

XML Array problems

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.

Summary

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 😊.

Author:

Patryk Wąsiewicz


PON. - PT. 10:00 - 18:00

office@knsdata.com

KNS Data Sp. z o. o.
ul. Hoża 86 lok. 410
00-682 Warszawa
NIP 7010903351
REGON 382381463
KRS 0000767896