Search code examples
c#swaggeropenapiswashbucklenswagstudio

Using Swashbuckle 5.x specify nullable = true on a Generic T Parameter reference property


I recently upgraded my API to a .net core 3.1 server using Swashbuckle 5 with the newtonsoft json nuget, which produces an openapi 3 schema. I then use NSwag to generate a C# API. Previously I had a .net core 2.2 server with swashbuckle 4, producing a swagger 2.0 api schema.

I have a generic response class for all responses, containing some metadata about the response like status code and a message, plus a Payload property of Generic type T containing the meat of the response.

When the response is an error code, I set the payload property to null. I am struggling to find a way to define my api so that swashbuckle and NSwag combined produce a C# api that will allow the payload property to be null on deserialization. (swagger 2.0 / swashbuckle 4 worked without issue).

Try as I might, the Payload property always gets the annotation [Newtonsoft.Json.JsonProperty("payload", Required = Newtonsoft.Json.Required.DisallowNull...] and the [System.ComponentModel.DataAnnotations.Required] annotation.

As I understand it, open API 3 now allows "$ref" properties to have the "nullable": true attribute in the schema definition. If I add this manually to my definition after it is created, NSwag correctly removes the Required attribute in the CSharp api and crucially sets the JsonProperty Required attribute to be "Default" (not required) instead of "DisallowNull".
However, nothing that I mark up the payload property with causes the nullable: true to appear in my schema json definition.

What I want is this:

"properties": {
          "payload": {
            "nullable": true, 
            "$ref": "#/components/schemas/VisualService.Client.Models.MyResultClass"
          },

What I get is this:

"properties": {
          "payload": {
            "$ref": "#/components/schemas/VisualService.Client.Models.MyResultClass"
          },

What would also work is setting the "nullable"=true on the definition of the referenced $ref object itself. I can't find a way to do this either.

I have tried the following remedies, to no success.

  1. Marking up the property in the dto class with JsonProperty in different ways:

    [JsonProperty(Required = Required.AllowNull)]
    public T Payload { get; set; }
    
    [AllowNull]
    public T Payload { get; set; }
    
    [MaybeNull]
    public T Payload { get; set; }
    
  2. Trying to tell Swashbuckle / Newtonsoft to use my custom Json Resolver as described in this github issue- doesn't seem to obey

    services.AddControllers()
                        .AddNewtonsoftJson(options =>
                        {                        options.SerializerSettings.ContractResolver = MyCustomResolver();

  1. I created my own custom attribute and filter to try to set the property as nullable

    [NullableGenericProperty]
    public T Payload { get; set; }
    
   [AttributeUsage(AttributeTargets.Property)]
    public class NullableGenericPropertyAttribute : Attribute
    {

    }

    public class SwaggerNullablePayloadFilter : ISchemaFilter
    {
        public void Apply(OpenApiSchema schema, SchemaFilterContext context)
        {
            if (schema?.Properties == null || context?.Type == null)
                return;

            var nullableGenericProperties = context.Type.GetProperties()
                .Where(t =>
                    t.GetCustomAttribute<NullableGenericPropertyAttribute>()
                    != null);

            foreach (var excludedProperty in nullableGenericProperties)
            {
                if (schema.Properties.ContainsKey(excludedProperty.Name.ToLowerInvariant()))
                {
                    var prop = schema.Properties[excludedProperty.Name.ToLowerInvariant()];

                    prop.Nullable = true;
                    prop.Required = new HashSet<string>() { "false" };
                }
            }
        }
    }

I had minor success with this one, in that adding the prop.Nullable = true; caused the attribute[System.ComponentModel.DataAnnotations.Required] to be removed from the c# api. However, the [Newtonsoft.Json.JsonProperty("payload", Required = Newtonsoft.Json.Required.DisallowNull...] still remained, so it didn't help that much. I added prop.Required = new HashSet<string>() { "false" }; as an additional try, but it doesn't seem to do anything.

I could downgrade to .net core 2.2 / swashbuckle 4 again but 2.2 is out of long term support and I want to stay at 3.1 if at all possible. I could also do a find and replace on my generated API client every time but I don't want to have to manually remember to do it every time I regenerate the api which can be several times a day in development cycles.

I've got a hacky workaround - which is that I'm intercepting the json response and adding the "nullable" = true on my server where it's needed, by using a regex match on the response Body json string, before serving it to the client. It's really hacky though and I'd like a native way to do this if it exists.

Any and all help appreciated!


Solution

  • There is a setting that accomplishes this:

    UseAllOfToExtendReferenceSchemas 
    

    It changes the schema to this, which nswag can use to allow nulls for $ref properties.

      "payload": {
        "required": [
          "false"
        ],
        "allOf": [
          {
            "$ref": "#/components/schemas/MyResultClass"
          }
        ],
        "nullable": true
      },
    

    Use it like this:

            _ = services.AddSwaggerGen(setup =>
            {
                setup.SwaggerDoc("v1", new OpenApiInfo { Title = AppConst.SwaggerTitle, Version = "v1" });
    
                setup.UseAllOfToExtendReferenceSchemas();
                ...