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,AddXmlSerializerFormatters
will useXmlSerializer
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:
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
PascalCase
is 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 = null
if 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