Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Change property type as exported by Swagger/Swashbuckle

I have a fairly complex object with nested objects; please note that in the example below I have simplified this object greatly.

Assume the following example object:

public class Result {
    public string Name { get; set; }
    public IpAddress IpAddress { get; set; }
}

I have implemented a JsonConverter<IPAddress> than (de)serializes the Ip as a string:

public class IPAddressConverter : JsonConverter<IPAddress>
{
    public override IPAddress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => IPAddress.Parse(reader.GetString());

    public override void Write(Utf8JsonWriter writer, IPAddress value, JsonSerializerOptions options)
        => writer.WriteStringValue(value.ToString());
}

The IPAddressConverter was then 'registered' as a converter in the AddJsonOptions(...) method. This nicely returns results as:

{ "Name": "Foo", "IpAddress": "198.51.100.1" }

And, vice versa, my controller "understands" IP addresses specified as string:

public IEnumerable<Result> FindByIp(IpAddress ip) {
    // ...
}

However, SwashBuckle exports this as:

{
  "openapi": "3.0.1",
  "info": {
    "title": "Example",
    "version": "v1"
  },
  "paths": {
    "/FindByIp": {
      "get": {
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "$ref": "#/components/schemas/Result"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AddressFamily": {
        "enum": [
          0,
          1,
          2,
          3,
          4,
          5,
          6,
          6,
          7,
          7,
          8,
          9,
          10,
          11,
          12,
          13,
          14,
          15,
          16,
          17,
          18,
          19,
          21,
          22,
          23,
          24,
          25,
          26,
          28,
          29,
          65536,
          65537,
          -1
        ],
        "type": "integer",
        "format": "int32"
      },
      "IPAddress": {
        "type": "object",
        "properties": {
          "addressFamily": {
            "$ref": "#/components/schemas/AddressFamily"
          },
          "scopeId": {
            "type": "integer",
            "format": "int64"
          },
          "isIPv6Multicast": {
            "type": "boolean",
            "readOnly": true
          },
          "isIPv6LinkLocal": {
            "type": "boolean",
            "readOnly": true
          },
          "isIPv6SiteLocal": {
            "type": "boolean",
            "readOnly": true
          },
          "isIPv6Teredo": {
            "type": "boolean",
            "readOnly": true
          },
          "isIPv4MappedToIPv6": {
            "type": "boolean",
            "readOnly": true
          },
          "address": {
            "type": "integer",
            "format": "int64"
          }
        },
        "additionalProperties": false
      },
      "Result": {
        "type": "object",
        "properties": {
          "ip": {
            "$ref": "#/components/schemas/IPAddress"
          },
          "name": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      }
    }
  }
}

Which, for the more visually inclined, looks like:

Screenshot

What I'd like to achieve, however, is this:

{
  "openapi": "3.0.1",
  "info": {
    "title": "Example",
    "version": "v1"
  },
  "paths": {
    "/FindByIp": {
      "get": {
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "$ref": "#/components/schemas/Result"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Result": {
        "type": "object",
        "properties": {
          "ip": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      }
    }
  }
}

Again, visualized:

Screenshot

I was hoping to be able to add an annotation / attribute on some properties (so I looked at Swashbuckle.AspNetCore.Annotations) but that doesn't seem to be possible.

Also, because the object is fairly complex and comes from a 3rd party library it's hard for me to actually add annotations / attributes on properties because I can't change the model (easily).

I could resort to AutoMapper (or alike) to create another model with a string for IP adresses but that would mean having to model all objects in the original model. Besides, it requires extra code and maintenance when the model changes. I'd rather tell Swashbuckle, somehow, that IP adresses (and, so, the type IPAddress will be represented as a string (in- and outgoing to my API). I'm looking for options on how to accomplish this the best way possible within given limitations (preferably not introducing new models to map to, preferably no annotations/attributes because I can't easily access the 3rd party library). Is there a way to register a "type-converter-something" for Swashbuckle to handle this?

Update: Solved!

This is what I ended up with:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services
        .AddResponseCompression()
        .AddMemoryCache()
        .AddControllers()
        // etc...
        // etc...

    // Here's the interesting part:
    services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo { Title = "Example", Version = "v1" });
            c.MapType<IPAddress>(() => new OpenApiSchema { Type = typeof(string).Name });
            // ...
        });
}

Thank you strickt01

like image 294
RobIII Avatar asked Oct 21 '19 14:10

RobIII


People also ask

How do you use swashbuckle AspNetCore Swagger?

Add and configure Swagger middlewareLaunch the app and navigate to https://localhost:<port>/swagger/v1/swagger.json . The generated document describing the endpoints appears as shown in OpenAPI specification (openapi. json). The Swagger UI can be found at https://localhost:<port>/swagger .

What is AddSwaggerGen?

AddSwaggerGen is an extension method to add swagger services to the collection. To configure Swagger, you invoke the method SwaggerDoc. Passing an Info object, you can define the title, description, contact information, and more in code file Startup. cs. Next open Startup.


1 Answers

As you are converting to a non-complex type you should be able to use MapType for this IPAddress example:

swagger.MapType<IPAddress>(() => new Schema { Type = "string" });

If you are converting to a complex type then you'd need to use SchemaFilter.

like image 200
strickt01 Avatar answered Nov 03 '22 10:11

strickt01