Swagger.json isn't recognizing Newtonsoft JsonProperty(PropertyName=...) attributes, despite AddSwaggerGenNewtonsoftSupport call
I've just recently upgraded from Swashbuckle.AspNetCore.SwaggerGen 4.0.1 to 6.4.0, and now the Newtonsoft Json attributes ([JsonProperty(...)] and the like) are being ignored. In particular, [JsonProperty(PropertyName = "key")] is used all over my codebase to rename properties, and those new names are not reflected in the swagger.json.
This is a .Net 6.0 project.
Relevant code snippets
Packages installed
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUi" Version="6.4.0" />
Startup.cs
I'll admit I just started spamming AddNewtonsoftJson() once I saw there was more than one place I could/should put it. The AddControllers().AddNewtonsoftJson() and AddMvcCore().AddNewtonsoftJson() calls are the latest flailing I've tried, the builder.AddNewtonsoftJson() was the first attempt.
var builder = services.AddMvcCore(x => {
x.EnableEndpointRouting = false;
});
builder.AddNewtonsoftJson(x => {
x.AllowInputFormatterExceptionMessages = false;
// Default enum serialization: as a string, not a number.
x.SerializerSettings.Converters.Add(new StringEnumConverter());
// don't serialize nulls
x.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
x.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
});
builder.Services.AddControllers().AddNewtonsoftJson()
.AddJsonOptions(options => { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); });
// Homemade all-Swagger-init function, see SwaggerConfig.cs
services.AddSwaggerPage();
SwaggerConfig.cs
You can see services.AddSwaggerGenNewtonsoftSupport() is called (probably correctly?) here.
public static void AddSwaggerPage(this IServiceCollection services)
{
services.AddSwaggerGen();
services.ConfigureSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = ...,
Title = ...,
Description = ...
});
services.AddSwaggerGenNewtonsoftSupport();
...and a bunch of auth stuff...
}
}
Code that generates the wrong property name in swagger.json, from an in-house Nuget package
namespace Thingy
{
public class Document
{
// This right here gets ignored. The generated swagger.json has this as "eTag", not "_etag".
[JsonProperty(PropertyName = "_etag", NullValueHandling = NullValueHandling.Ignore)]
public string ETag { get; set; }
}
}
So: what error am I making in making Swagger recognize the Newtonsoft attributes?
Hey this can happen when mixing Newtonsoft and Microsoft serialization. The only thing I've found that always works is to attribute your properties with both JsonPropertyName (Microsoft) and JsonProperty (Newtonsoft) and then it's properly serialized from the API and in Swagger for example:
[JsonPropertyName("_created_at")]
[JsonProperty("_created_at")]
public DateTime? CreatedAt { get; set; }
Swashbuckle typically uses the serialization options associated with MVC - if you changed MVC to use System.Text.Json, then so will Swashbuckle.
There might be a bug where that clobbers your explicit intention to not do that though.
@martincostello this seems to be the first warning in the package readme. Swashbuckle by default in the version 4 used Newtonsoft whereas since version 5 it uses STJ unless the AddSwaggerGenNewtonsoftSupport is called
So I've just run into the same issue, however I am calling AddSwaggerGenNewtonsoftSupport much like int he OP's example. The double decorator solutions would work but that feals a little dirty and really annoying. I guess you might be able to write your own decorator? However I did come across a different solution, which is to make the swagger have a look at the DTO's and overwrite them on the fly. It uses the Schema filter which is built into SwashBuckle. I did have a little problem when reading/reflecting the property names ont he DTO as reflection uses the actual name but the sotred property name depends on the NamingStrategy set in the NewtonSoft ContractResolver settings.
Anyway here is the code for my solution:
Startup.cs
// Add services here
services.AddControllers(options =>
{
options.Filters.Add<GlobalExceptionFilter>();
})
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy() // Or your preferred strategy, but this will afect the NewtonsoftJsonSchemaFilter
};
});
Swagger implementation change:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", info);
c.AddSecurityDefinition("Bearer", scheme);
c.AddSecurityRequirement(security);
c.SchemaFilter<NewtonsoftJsonSchemaFilter>();
});
NewtonsoftJsonSchemaFilter.cs to handle the decorator with swagger
using System.Reflection;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Swashbuckle.AspNetCore.SwaggerGen;
/// <summary>
/// Custom schema filter implementation for Newtonsoft.Json
/// Where you have DTOs with JsonProperty attributes, this filter will ensure that the schema properties are correctly named
/// in swagger documentation.
/// </summary>
public class NewtonsoftJsonSchemaFilter : ISchemaFilter
{
// Define a static naming strategy to be used
private static readonly NamingStrategy NamingStrategy = new CamelCaseNamingStrategy();
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (context.Type == null || schema?.Properties == null)
return;
foreach (var property in context.Type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
// Get the JsonProperty attribute if it exists
var jsonPropertyAttribute = property.GetCustomAttribute<JsonPropertyAttribute>();
var propertyName = property.Name;
// Determine the property name as per the naming strategy
var schemaPropertyName = NamingStrategy.GetPropertyName(propertyName, false);
// Use the JSON property name from the attribute if it exists, otherwise default to naming strategy
var jsonPropertyName = jsonPropertyAttribute?.PropertyName ?? schemaPropertyName;
// Check if the schema contains the property and update it
if (schema.Properties.ContainsKey(schemaPropertyName))
{
var schemaProperty = schema.Properties[schemaPropertyName];
schema.Properties.Remove(schemaPropertyName);
schema.Properties[jsonPropertyName] = schemaProperty;
}
}
}
}
Could you create a repro to look at it?. I have worked with Swashbuckle for a long time and there must be something wrong on the Program/StartUp.cs
I haven't been able to get back to this code, but I think the error is in having the AddSwaggerGenNewtonsoftSupport inside the ConfigureSwaggerGen() call. At one point, I moved that call to outside the ConfigureSwaggerGen, and got about twenty errors on annotations that had, at some point in the distant past, worked.
I suspect that moving AddSwaggerGenNewtonsoftSupport outside ConfigureSwaggerGen, and then running around and fixing the ~20 annotations, should fix the problem. But, alas, here are hotter fires burning elsewhere at the moment.