Search code examples
c#openapinswag

nswag is generating invallid swagger.json with $type properties


I am using NSwag to generate and serve an OpenAPI document. for the most part it generates just fine. however, sometimes when my coworker tries to serve the page locally, the JSON created includes $type properties (but only sometimes). Here is an partial sample JSON snippet showing what I mean:

"paths": {
    "$type": "NSwag.Collections.ObservableDictionary`2[[System.String, System.Private.CoreLib],[NSwag.OpenApiPathItem, NSwag.Core]], NSwag.Core",
    "/api/Export/delimited/fetch/{fileName}": {
      "post": {
        "$type": "NSwag.OpenApiOperation, NSwag.Core",
        "tags": {
          "$type": "System.Collections.Generic.List`1[[System.String, System.Private.CoreLib]], System.Private.CoreLib",
          "$values": [
            "Export"
          ]
        },
        "operationId": "Export_ExportClaimsToDelimitedTextFile",
        "parameters": {
          "$type": "System.Collections.Generic.List`1[[NSwag.OpenApiParameter, NSwag.Core]], System.Private.CoreLib",
          "$values": [
            {
              "$type": "NSwag.OpenApiParameter, NSwag.Core",
              "name": "fileName",
              "in": "path",
              "required": true,
              "schema": {
                "$type": "NJsonSchema.JsonSchema, NJsonSchema",
                "type": "string"
              },
              "x-position": 1
            }
          ]
        },
        "requestBody": {
          "$type": "NSwag.OpenApiRequestBody, NSwag.Core",
          "x-name": "requestWithFriends",
          "content": {
            "$type": "NSwag.Collections.ObservableDictionary`2[[System.String, System.Private.CoreLib],[NSwag.OpenApiMediaType, NSwag.Core]], NSwag.Core",
            "application/json": {
              "$type": "NSwag.OpenApiMediaType, NSwag.Core",
              "schema": {
                "$type": "NJsonSchema.JsonSchema, NJsonSchema",
                "oneOf": {
                  "$type": "System.Collections.ObjectModel.ObservableCollection`1[[NJsonSchema.JsonSchema, NJsonSchema]], System.ObjectModel",
                  "$values": [
                    {
                      "$type": "NJsonSchema.JsonSchema, NJsonSchema",
                      "$ref": "#/components/schemas/ExportRequest"
                    }
                  ]
                }
              }
            }
          },
          "required": true,
          "x-position": 2
        },
        "responses": {
          "$type": "NSwag.Collections.ObservableDictionary`2[[System.String, System.Private.CoreLib],[NSwag.OpenApiResponse, NSwag.Core]], NSwag.Core",
          "200": {
            "$type": "NSwag.OpenApiResponse, NSwag.Core",
            "description": "",
            "content": {
              "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[NSwag.OpenApiMediaType, NSwag.Core]], System.Private.CoreLib",
              "application/octet-stream": {
                "$type": "NSwag.OpenApiMediaType, NSwag.Core",
                "schema": {
                  "$type": "NJsonSchema.JsonSchema, NJsonSchema",
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          }
        }
      }
    },

I expected Json more like this:

"paths": {
    "/api/Export/delimited/fetch/{fileName}": {
      "post": {
        "tags": [
          "Export"
        ],
        "operationId": "Export_ExportClaimsToDelimitedTextFile",
        "parameters": [
          {
            "name": "fileName",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "x-position": 1
          }
        ],
        "requestBody": {
          "x-name": "requestWithFriends",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportClaimsRequestWithDealers"
              }
            }
          },
          "required": true,
          "x-position": 2
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          }
        }
      }
    },

My intention is to use the Swagger.json to generate typescript files using NSwag's tools. However these $type properties break that process.

Ihis is a screen shot of the error it produced when I attempt to parse the first JSON: Required property "responses" not found

My attempts to correct this issue have not been very fruitful. ChatGPT seems to indicate that it has something to do with Newtonsoft.Json to specify the type of the object and that I can disable it using this bit of code:

services.AddOpenApiDocument(config =>
{
    // Disable the $type properties in the Swagger JSON
    config.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.None;
});

However when I try that I discover that SerializerSettings is null at runtime. Regardless, such a thing wouldn't explain why it only occurred sometimes. I thought it might have something to do with different package versions but that's hard to verify.

I am looking for any advice as to how to diagnose and correct this issue. Even a consistent way to temporarily reset the app to escape this state would be helpful.


Solution

  • The appearance of $type properties in the JSON is indeed related to Newtonsoft.Json's settings for TypeNameHandling. By default, these settings should not be generating the $type properties, but sometimes the settings can be globally overridden, causing the unexpected behavior.

    Here's a multi-step approach to diagnose and correct this issue:

    1. Explicitly Set SerializerSettings: If SerializerSettings is null at runtime, then you need to initialize it before setting properties on it. You can do this as follows:

      services.AddOpenApiDocument(config =>
      {
          if (config.SerializerSettings == null)
          {
              config.SerializerSettings = new Newtonsoft.Json.JsonSerializerSettings();
          }
          config.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.None;
      });
      
    2. Ensure Consistency Across Environments: You mentioned that this issue is intermittent, particularly between your environment and your coworker's. Here are some steps to ensure consistency:

      • Make sure you both are using the exact same version of NSwag and Newtonsoft.Json.
      • Check if there are any differences in Startup.cs or other configuration files.
      • Ensure you both have a consistent development environment, such as using the same operating system, .NET Core/.NET 5+ version, etc.
    3. Global Configuration: It's possible that there's some global configuration or another service in your application that modifies the default JsonSerializerSettings. Look for any other code in your project that modifies JsonConvert.DefaultSettings or sets TypeNameHandling properties.

    4. Refresh & Rebuild: Sometimes the issues can be related to build caches. You can:

      • Delete the bin and obj directories from your project and rebuild it.
      • Restart your development environment or IDE.
      • If you're using Docker, ensure that you rebuild your images without using cache.
    5. Logging & Diagnostics: If you're still facing the issue, consider adding diagnostic logs. For instance, right before your OpenAPI document is generated, log the value of config.SerializerSettings.TypeNameHandling. This will help you understand if the setting is being changed somewhere else.

    6. Fallback: Manual Cleanup: As a temporary measure (not a permanent solution), you can write a middleware or a post-processing step that intercepts the generated Swagger JSON and removes all $type properties. But this should be your last resort.

    Remember, the key is to ensure that the environment and settings are consistent across all development machines. By following these steps, you should be able to identify the root cause and rectify the issue.