microsoft / OpenAPI.NET

The OpenAPI.NET SDK contains a useful object model for OpenAPI documents in .NET along with common serializers to extract raw OpenAPI JSON and YAML documents from the model.
MIT License
1.38k stars 231 forks source link

SerializeAsV2 produces invalid swagger (when SerializeAsV3 is valid) #425

Open pechkarus opened 4 years ago

pechkarus commented 4 years ago

When creating Open API document and specifying schema reference for enum in query parameter (as oppose to define schema in place) invalid Swagger is generated for OpenAPI/Swagger v2, on the other hand v3 document is perfectly valid. Correct me if I am wrong, but schema of parameters in v2 should be defined in place and cannot be referenced. However it adds additional burden on the authors of Open API documents who want to support V2 and V3 simultaneously. Should the generator be smart enough to follow the reference and inline the schema definition when generated for v2, while supporting v3 features at the same time? Related issue in Swashbuckle.AspNetCore

Version

Microsoft.OpenApi 1.1.4.0

Program

Slightly modified default example to show the issue:

  [JsonConverter(typeof(StringEnumConverter))]
  public enum JustAnotherEnum
  {
      A = 0,
      B = 1,
      C = 2,
      D = 10,
      E = 11,
  }
  var json = new OpenApiDocument
  {
      Info = new OpenApiInfo
      {
          Version = "1.0.0",
          Title = "Just a Swagger ",
      },
      Servers = new List<OpenApiServer>
      {
          new OpenApiServer { Url = "http://just.swagger.io/api" }
      },
      Components = new OpenApiComponents
      {
          Schemas =
          {
              ["JustAnotherEnum"] = new OpenApiSchema
              {
                  Type = "string",
                  Enum = typeof(JustAnotherEnum)
                          .GetEnumValues()
                          .Cast<object>()
                          .Select(value => (IOpenApiAny)new OpenApiString(value.ToString()))
                          .ToList()
              },            
          }
      },
      Paths = new OpenApiPaths
      {
          ["/justCall"] = new OpenApiPathItem
          {
              Operations = new Dictionary<OperationType, OpenApiOperation>
              {
                  [OperationType.Get] = new OpenApiOperation
                  {
                      Description = "Just returns",
                      Parameters =
                      {
                          new OpenApiParameter()
                          {
                              In = ParameterLocation.Query,
                              Name = "justString",
                              Schema = new OpenApiSchema
                              {
                                  Type = "string",                                            
                              }
                         },
                         new OpenApiParameter()
                         {
                             In = ParameterLocation.Query,
                             Name = "justEnum",
                             Schema = new OpenApiSchema
                             {
                                 Reference = new OpenApiReference()
                                 {
                                     Id = "JustAnotherEnum",
                                     Type = ReferenceType.Schema
                                 }
                             }  
                          }
                      },  
                      Responses = new OpenApiResponses
                      {
                          ["200"] = new OpenApiResponse
                          {
                              Description = "OK"
                          }
                      }
                  }
              }
          }
      }
  }.Serialize(OpenApiSpecVersion.OpenApi2_0, OpenApiFormat.Json);

Expected

Valid OpenAPI/Swagger v2 document

Actual

Invalid OpenAPI/Swagger v2 document

{
  "swagger": "2.0",
  "info": {
    "title": "Just a Swagger ",
    "version": "1.0.0"
  },
  "host": "just.swagger.io",
  "basePath": "/api",
  "schemes": [
    "http"
  ],
  "paths": {
    "/justCall": {
      "get": {
        "description": "Just returns",
        "parameters": [
          {
            "in": "query",
            "name": "justString",
            "type": "string"
          },
          {
            "in": "query",
            "name": "justEnum"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "definitions": {
    "JustAnotherEnum": {
      "enum": [
        "A",
        "B",
        "C",
        "D",
        "E"
      ],
      "type": "string"
    }
  }
}
darrelmiller commented 4 years ago

Hey @pechkarus . Sorry for the extremely slow response. In cases like these, please don't hesitate to ping me on twitter. My GitHub notifications are a mess.

It turns out that this issue lead me to quite the bug in our current release that I have been able to resolve.

However, you can easily avoid the bug in 1.1.4 by constructing the OpenApiDocument a slightly different way. Instead of creating the Schema object in components and then trying to reference it in the enum, you can just use the same object in both places. The only reason there exists two forms of references (resolved and unresolved) is make it easier to parse documents. References are parsed as unresolved and then we walk the document and remove the "unresolved" objects, and replace them with the "resolved" objects.

When you are constructing an OpenApiDocument, you can create the "resolved" object in the first place and just use the same object in multiple places. e.g.

            var justAnotherEnum = new OpenApiSchema
            {
                Type = "string",
                Enum = typeof(JustAnotherEnum)
                        .GetEnumValues()
                        .Cast<object>()
                        .Select(value => (IOpenApiAny)new OpenApiString(value.ToString()))
                        .ToList(),
                Reference = new OpenApiReference()
                {
                    Id = "JustAnotherEnum",
                    Type = ReferenceType.Schema
                },
                UnresolvedReference = false
            };

            var doc = new OpenApiDocument
            {
                Info = new OpenApiInfo
                {
                    Version = "1.0.0",
                    Title = "Just a Swagger ",
                },
                Servers = new List<OpenApiServer>
                {
                    new OpenApiServer { Url = "http://just.swagger.io/api" }
                },
                Components = new OpenApiComponents
                {
                    Schemas =
                    {
                        ["JustAnotherEnum"] = justAnotherEnum,
                    }
                },
                Paths = new OpenApiPaths
                {
                    ["/justCall"] = new OpenApiPathItem
                    {
                        Operations = new Dictionary<OperationType, OpenApiOperation>
                        {
                            [OperationType.Get] = new OpenApiOperation
                            {
                                Description = "Just returns",
                                Parameters =
                    {
                        new OpenApiParameter()
                        {
                            In = ParameterLocation.Query,
                            Name = "justString",
                            Schema = new OpenApiSchema
                            {
                                Type = "string",
                            }
                       },
                       new OpenApiParameter()
                       {
                           In = ParameterLocation.Query,
                           Name = "justEnum",
                           Schema = justAnotherEnum
                        }
                    },
                                Responses = new OpenApiResponses
                                {
                                    ["200"] = new OpenApiResponse
                                    {
                                        Description = "OK"
                                    }
                                }
                            }
                        }
                    }
                }
            };

            var json = doc.Serialize(OpenApiSpecVersion.OpenApi2_0, OpenApiFormat.Json);

This will work fine in 1.1.4. However, in hunting down this issue I discovered that we were not properly resolving references within OpenApiParameter objects. So, Schema and Examples that used references in a parameter were not be resolved.

korygin commented 3 years ago

Hi Darrel,

I have a similar but slightly different issue. Attached is a valid v3 doc that when serialized as v2 produces an invalid document with the following errors:

openapi.txt