Swashbuckle.AspNetCore icon indicating copy to clipboard operation
Swashbuckle.AspNetCore copied to clipboard

Swagger.json isn't recognizing Newtonsoft JsonProperty(PropertyName=...) attributes, despite AddSwaggerGenNewtonsoftSupport call

Open pmarshall opened this issue 2 years ago • 6 comments

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?

pmarshall avatar Jun 20 '23 23:06 pmarshall

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; }

CodyBatt avatar Sep 22 '23 17:09 CodyBatt

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 avatar Apr 14 '24 09:04 martincostello

@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

jgarciadelanoceda avatar Jun 05 '24 20:06 jgarciadelanoceda

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;
            }
        }
    }
}

SJGCodeProjects avatar Aug 08 '24 08:08 SJGCodeProjects

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

jgarciadelanoceda avatar Sep 10 '24 19:09 jgarciadelanoceda

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.

pmarshall avatar Sep 12 '24 05:09 pmarshall