Geekn
Geekn

Reputation: 2882

Should auto generated swagger.json from swashbuckle be valid in swagger editor?

I have a .NET core 2.1 web API which uses Swashbuckle.AspNetCore v3.0.0 to generate the swagger documentation. I can access the swagger UI from the API which works perfectly and I don't receive any errors when using it from the API. I cannot, however, use the swagger.json to publish this API to Azure API Management. When I load the swagger.json in the swagger editor for validation, it complains about a bunch of errors (same errors that Azure complains about). Since the swagger.json is generated from the swagger configured in my API (it's a link right at the top of the swagger UI within my API), why can't it be successfully loaded into the swagger editor or to Azure?

enter image description here

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new Info { Title = settings.AppSettings.ApplicationName + " API", Version = "v1" });
        var commentPath = String.Format(@"{0}\{1}.xml", AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName);
        c.IncludeXmlComments(commentPath);
});



{"swagger":"2.0","info":{"version":"v1","title":"Route Manager API"},"paths":{"/api/account/Register":{"post":{"tags":["Account"],"summary":"Allows for registering a customer or vendor.","operationId":"ApiAccountRegisterPost","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"UserName","in":"query","description":"Unique name of user being registered.","required":true,"type":"string"},{"name":"Password","in":"query","description":"Password for new user.","required":true,"type":"string"},{"name":"ConfirmPassword","in":"query","description":"Confirmation of password that must match.","required":true,"type":"string"},{"name":"Email","in":"query","description":"Email address of registring user.","required":true,"type":"string"}],"responses":{"200":{"description":"Successfully registered customer or vendor.","schema":{"$ref":"#/definitions/IdentityResult"}},"400":{"description":"The RegisterUserDto model was invalid.","schema":{"$ref":"#/definitions/RegisterUserDto"}}}}},"/api/account/ChangePassword":{"post":{"tags":["Account"],"summary":"Allows for changing the users password.","operationId":"ApiAccountChangePasswordPost","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"OldPassword","in":"query","required":true,"type":"string","format":"password"},{"name":"NewPassword","in":"query","required":true,"type":"string","format":"password","maxLength":100,"minLength":6},{"name":"ConfirmPassword","in":"query","required":true,"type":"string","format":"password"},{"name":"StatusMessage","in":"query","required":false,"type":"string"}],"responses":{"400":{"description":"The ChangePasswordDto model was invalid.","schema":{"$ref":"#/definitions/ChangePasswordDto"}},"200":{"description":"Password successfully changed"}}}},"/api/account/Logout":{"post":{"tags":["Account"],"summary":"Logs the current user out of the system.","operationId":"ApiAccountLogoutPost","consumes":[],"produces":[],"parameters":[],"responses":{"200":{"description":"Logout successfully performed"}}}},"/api/account/ExternalLogin":{"get":{"tags":["Account"],"summary":"Initiates the external login process for the specified authentication provider.","description":"When an external login is not already associated with a local account, an attempt is made to find one local account with the same email \r\naddress.  If one is found, an account is created and associated with the external login.  If an account is not found, this method will \r\nreturn the external login email to the caller so they can decide if they want to present an option for the user to provide an \r\nexisting email account or create a new one based off the external login email.  If we always created a new account with the external \r\nemail then that would mean we could only associate external logins that have the same email address of an account stored in our system\r\nwhich would essentially allow duplicate accounts to be created for the same physical person.","operationId":"ApiAccountExternalLoginGet","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"provider","in":"query","description":"The code for the external login provider that is being used to login.","required":false,"type":"string"},{"name":"returnUrl","in":"query","description":"The return URL that will be called upon a successfully external login.","required":false,"type":"string"}],"responses":{"400":{"description":"Error occured during the external login process.  Response should contain error description.","schema":{"$ref":"#/definitions/ExternalLoginConfirmationDto"}},"200":{"description":"Indicate a token was issued and the user has been signed in."},"202":{"description":"Indicates the user was successfully signed in but an account does not exist in our system in which case the email \r\n            of the external login is returned in a ExternalLoginModel that should be used to call the ExternalLoginConfirmation action method."}}}},"/api/account/ExternalLoginCallback":{"get":{"tags":["Account"],"summary":"Attempts to sign in a user based on an external sign-in attempt.","description":"This action is called from an external login provider after a client calls ExternalLogin.  The results of this call are \r\ndocumented in the ExternalLogin method since the client never calls this method directly.","operationId":"ApiAccountExternalLoginCallbackGet","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"returnUrl","in":"query","required":false,"type":"string"},{"name":"remoteError","in":"query","required":false,"type":"string"}],"responses":{"202":{"description":"Indicates the user was successfully signed in but an account does not exist in our system in which case the email \r\n            of the external login is returned in a ExternalLoginModel that should be used to call the ExternalLoginConfirmation action method.","schema":{"$ref":"#/definitions/ExternalLoginConfirmationDto"}},"400":{"description":"Error occured during the external login process.  Response should contain error description.","schema":{"$ref":"#/definitions/ExternalLoginConfirmationDto"}},"200":{"description":"Indicate a token was issued and the user has been signed in."}}}},"/api/account/ExternalLoginConfirmation":{"post":{"tags":["Account"],"summary":"Associates an external login with an existing user or new user.","description":"In order to successfully associate an email for an existing user to an external credentials such as \r\n            facebook, the user must already be authenticated via the external login provider.","operationId":"ApiAccountExternalLoginConfirmationPost","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"Email","in":"query","description":"The external email address associated with the external provider login.","required":true,"type":"string"},{"name":"LoginProvider","in":"query","description":"The external provider that was used to authenticate.","required":false,"type":"string"},{"name":"ReturnUrl","in":"query","description":"The return URL that was provided by the client application when requesting an external login request.","required":false,"type":"string"}],"responses":{"400":{"description":"The model was found to be invalid. Response should contain model errors.","schema":{"$ref":"#/definitions/ExternalLoginConfirmationDto"}},"200":{"description":"Successfully associates the account to the external login and redirects to the client return URL."}}}},"/connect/authorize":{"get":{"tags":["Authorization"],"summary":"Provides the authorization endpoint for supporting openid connect code flow","operationId":"ConnectAuthorizeGet","consumes":[],"produces":["text/plain","application/json","text/json"],"parameters":[{"name":"AccessToken","in":"query","required":false,"type":"string"},{"name":"AcrValues","in":"query","required":false,"type":"string"},{"name":"Assertion","in":"query","required":false,"type":"string"},{"name":"Claims","in":"query","required":false,"type":"object"},{"name":"ClaimsLocales","in":"query","required":false,"type":"string"},{"name":"ClientAssertion","in":"query","required":false,"type":"string"},{"name":"ClientAssertionType","in":"query","required":false,"type":"string"},{"name":"ClientId","in":"query","required":false,"type":"string"},{"name":"ClientSecret","in":"query","required":false,"type":"string"},{"name":"Code","in":"query","required":false,"type":"string"},{"name":"CodeChallenge","in":"query","required":false,"type":"string"},{"name":"CodeChallengeMethod","in":"query","required":false,"type":"string"},{"name":"CodeVerifier","in":"query","required":false,"type":"string"},{"name":"Display","in":"query","required":false,"type":"string"},{"name":"GrantType","in":"query","required":false,"type":"string"},{"name":"IdentityProvider","in":"query","required":false,"type":"string"},{"name":"IdTokenHint","in":"query","required":false,"type":"string"},{"name":"LoginHint","in":"query","required":false,"type":"string"},{"name":"MaxAge","in":"query","required":false,"type":"integer","format":"int64"},{"name":"Nonce","in":"query","required":false,"type":"string"},{"name":"Password","in":"query","required":false,"type":"string"},{"name":"PostLogoutRedirectUri","in":"query","required":false,"type":"string"},{"name":"Prompt","in":"query","required":false,"type":"string"},{"name":"RedirectUri","in":"query","required":false,"type":"string"},{"name":"RefreshToken","in":"query","required":false,"type":"string"},{"name":"Request","in":"query","required":false,"type":"string"},{"name":"RequestId","in":"query","required":false,"type":"string"},{"name":"RequestUri","in":"query","required":false,"type":"string"},{"name":"Resource","in":"query","required":false,"type":"string"},{"name":"ResponseMode","in":"query","required":false,"type":"string"},{"name":"ResponseType","in":"query","required":false,"type":"string"},{"name":"Scope","in":"query","required":false,"type":"string"},{"name":"State","in":"query","required":false,"type":"string"},{"name":"Token","in":"query","required":false,"type":"string"},{"name":"TokenTypeHint","in":"query","required":false,"type":"string"},{"name":"Registration","in":"query","required":false,"type":"object"},{"name":"UiLocales","in":"query","required":false,"type":"string"},{"name":"Username","in":"query","required":false,"type":"string"}],"responses":{"200":{"description":"Successfully authenticated via the authorize flow and returns the appropriate access/identity tokens.","schema":{"$ref":"#/definitions/SignInResult"}},"400":{"description":"Indicates the user was authenticated, but the user manager was unable to obtain the user due to a server error."},"403":{"description":"Indicates the authorization request is not allowed because the user is not authenticated."}}}},"/connect/token":{"post":{"tags":["Authorization"],"summary":"Provides the token endpoint for supporting openidconnect token generation.","operationId":"ConnectTokenPost","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"AccessToken","in":"query","required":false,"type":"string"},{"name":"AcrValues","in":"query","required":false,"type":"string"},{"name":"Assertion","in":"query","required":false,"type":"string"},{"name":"Claims","in":"query","required":false,"type":"object"},{"name":"ClaimsLocales","in":"query","required":false,"type":"string"},{"name":"ClientAssertion","in":"query","required":false,"type":"string"},{"name":"ClientAssertionType","in":"query","required":false,"type":"string"},{"name":"ClientId","in":"query","required":false,"type":"string"},{"name":"ClientSecret","in":"query","required":false,"type":"string"},{"name":"Code","in":"query","required":false,"type":"string"},{"name":"CodeChallenge","in":"query","required":false,"type":"string"},{"name":"CodeChallengeMethod","in":"query","required":false,"type":"string"},{"name":"CodeVerifier","in":"query","required":false,"type":"string"},{"name":"Display","in":"query","required":false,"type":"string"},{"name":"GrantType","in":"query","required":false,"type":"string"},{"name":"IdentityProvider","in":"query","required":false,"type":"string"},{"name":"IdTokenHint","in":"query","required":false,"type":"string"},{"name":"LoginHint","in":"query","required":false,"type":"string"},{"name":"MaxAge","in":"query","required":false,"type":"integer","format":"int64"},{"name":"Nonce","in":"query","required":false,"type":"string"},{"name":"Password","in":"query","required":false,"type":"string"},{"name":"PostLogoutRedirectUri","in":"query","required":false,"type":"string"},{"name":"Prompt","in":"query","required":false,"type":"string"},{"name":"RedirectUri","in":"query","required":false,"type":"string"},{"name":"RefreshToken","in":"query","required":false,"type":"string"},{"name":"Request","in":"query","required":false,"type":"string"},{"name":"RequestId","in":"query","required":false,"type":"string"},{"name":"RequestUri","in":"query","required":false,"type":"string"},{"name":"Resource","in":"query","required":false,"type":"string"},{"name":"ResponseMode","in":"query","required":false,"type":"string"},{"name":"ResponseType","in":"query","required":false,"type":"string"},{"name":"Scope","in":"query","required":false,"type":"string"},{"name":"State","in":"query","required":false,"type":"string"},{"name":"Token","in":"query","required":false,"type":"string"},{"name":"TokenTypeHint","in":"query","required":false,"type":"string"},{"name":"Registration","in":"query","required":false,"type":"object"},{"name":"UiLocales","in":"query","required":false,"type":"string"},{"name":"Username","in":"query","required":false,"type":"string"}],"responses":{"200":{"description":"Successfully generates the appropriate access/identity tokens.","schema":{"$ref":"#/definitions/SignInResult"}},"400":{"description":"Indicates an error while attempting to generate an authentication token.","schema":{"$ref":"#/definitions/OpenIdConnectResponse"}},"403":{"description":"Indicates the authorization request is not allowed because the user is not authenticated."}}}},"/api/EntityActions":{"get":{"tags":["EntityAction"],"summary":"List endpoints for all enabled entity actions.","operationId":"ApiEntityActionsGet","consumes":[],"produces":[],"parameters":[],"responses":{"200":{"description":"Success"}}}},"/api/Environments":{"get":{"tags":["Environments"],"summary":"Returns all available environments.","description":"This method return all public environments.  If the caller has been authenticated, additional environments configured\r\nfor the identity will also be returned.","operationId":"ApiEnvironmentsGet","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[],"responses":{"200":{"description":"Environments successfully returned","schema":{"uniqueItems":false,"type":"array","items":{"$ref":"#/definitions/ApplicationEnvironmentDto"}}},"500":{"description":"Unexpected error while retrieving available environments"}}}},"/api/Environments/{clientID}":{"get":{"tags":["Environments"],"summary":"Returns the set of environment configured for a given clientID","description":"This method return all public environments.  If the caller has been authenticated, additional environments configured\r\nfor the identity will also be returned.","operationId":"ApiEnvironmentsByClientIDGet","consumes":[],"produces":["text/plain","application/json","text/json","application/xml","text/xml"],"parameters":[{"name":"clientID","in":"path","description":"The clientID assigned to the application requesting environments.","required":true,"type":"string"}],"responses":{"200":{"description":"Environments successfully returned","schema":{"uniqueItems":false,"type":"array","items":{"$ref":"#/definitions/ApplicationEnvironmentDto"}}},"400":{"description":"The client ID was not provided."},"500":{"description":"Unexpected error while retrieving available environments"}}}}},"definitions":{"IdentityResult":{"type":"object","properties":{"succeeded":{"type":"boolean","readOnly":true},"errors":{"uniqueItems":false,"type":"array","items":{"$ref":"#/definitions/IdentityError"},"readOnly":true}}},"IdentityError":{"type":"object","properties":{"code":{"type":"string"},"description":{"type":"string"}}},"RegisterUserDto":{"required":["userName","password","confirmPassword","email"],"type":"object","properties":{"userName":{"description":"Unique name of user being registered.","type":"string"},"password":{"description":"Password for new user.","type":"string"},"confirmPassword":{"description":"Confirmation of password that must match.","type":"string"},"email":{"description":"Email address of registring user.","type":"string"}}},"ChangePasswordDto":{"required":["oldPassword","newPassword","confirmPassword"],"type":"object","properties":{"oldPassword":{"format":"password","type":"string"},"newPassword":{"format":"password","maxLength":100,"minLength":6,"type":"string"},"confirmPassword":{"format":"password","type":"string"},"statusMessage":{"type":"string"}}},"ExternalLoginConfirmationDto":{"required":["email"],"type":"object","properties":{"email":{"description":"The external email address associated with the external provider login.","type":"string"},"loginProvider":{"description":"The external provider that was used to authenticate.","type":"string"},"returnUrl":{"description":"The return URL that was provided by the client application when requesting an external login request.","type":"string"}}},"SignInResult":{"type":"object","properties":{"succeeded":{"type":"boolean","readOnly":true},"isLockedOut":{"type":"boolean","readOnly":true},"isNotAllowed":{"type":"boolean","readOnly":true},"requiresTwoFactor":{"type":"boolean","readOnly":true}}},"OpenIdConnectResponse":{"type":"object","properties":{"accessToken":{"type":"string"},"code":{"type":"string"},"error":{"type":"string"},"errorDescription":{"type":"string"},"errorUri":{"type":"string"},"expiresIn":{"format":"int64","type":"integer"},"idToken":{"type":"string"},"refreshToken":{"type":"string"},"resource":{"type":"string"},"scope":{"type":"string"},"state":{"type":"string"},"tokenType":{"type":"string"}}},"ApplicationEnvironmentDto":{"type":"object","properties":{"applicationDisplayName":{"type":"string"},"environmentName":{"type":"string"},"baseUrl":{"type":"string"}}}}}

Here is an example function where the swagger is breaking down the properties of the Openiddict type within this function.

[HttpPost("~/connect/token")]
[ProducesResponseType(typeof(Microsoft.AspNetCore.Identity.SignInResult), 200)]
[ProducesResponseType(typeof(OpenIdConnectResponse), 400)]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
{
    return Ok(null);
}

That function generates the following swagger.json from swashbuckle

/connect/token:
    post:
      tags:
        - Authorization
      summary: Provides the token endpoint for supporting openidconnect token generation.
      operationId: ConnectTokenPost
      consumes: []
      produces:
        - text/plain
        - application/json
        - text/json
        - application/xml
        - text/xml
      parameters:
        - name: AccessToken
          in: query
          required: false
          type: string
        - name: AcrValues
          in: query
          required: false
          type: string
        - name: Assertion
          in: query
          required: false
          type: string
        - name: Claims
          in: query
          required: false
          type: object
        - name: ClaimsLocales
          in: query
          required: false
          type: string
        - name: ClientAssertion
          in: query
          required: false
          type: string
        - name: ClientAssertionType
          in: query
          required: false
          type: string
        - name: ClientId
          in: query
          required: false
          type: string
        - name: ClientSecret
          in: query
          required: false
          type: string
        - name: Code
          in: query
          required: false
          type: string
        - name: CodeChallenge
          in: query
          required: false
          type: string
        - name: CodeChallengeMethod
          in: query
          required: false
          type: string
        - name: CodeVerifier
          in: query
          required: false
          type: string
        - name: Display
          in: query
          required: false
          type: string
        - name: GrantType
          in: query
          required: false
          type: string
        - name: IdentityProvider
          in: query
          required: false
          type: string
        - name: IdTokenHint
          in: query
          required: false
          type: string
        - name: LoginHint
          in: query
          required: false
          type: string
        - name: MaxAge
          in: query
          required: false
          type: integer
          format: int64
        - name: Nonce
          in: query
          required: false
          type: string
        - name: Password
          in: query
          required: false
          type: string
        - name: PostLogoutRedirectUri
          in: query
          required: false
          type: string
        - name: Prompt
          in: query
          required: false
          type: string
        - name: RedirectUri
          in: query
          required: false
          type: string
        - name: RefreshToken
          in: query
          required: false
          type: string
        - name: Request
          in: query
          required: false
          type: string
        - name: RequestId
          in: query
          required: false
          type: string
        - name: RequestUri
          in: query
          required: false
          type: string
        - name: Resource
          in: query
          required: false
          type: string
        - name: ResponseMode
          in: query
          required: false
          type: string
        - name: ResponseType
          in: query
          required: false
          type: string
        - name: Scope
          in: query
          required: false
          type: string
        - name: State
          in: query
          required: false
          type: string
        - name: Token
          in: query
          required: false
          type: string
        - name: TokenTypeHint
          in: query
          required: false
          type: string
        - name: Registration
          in: query
          required: false
          type: object
        - name: UiLocales
          in: query
          required: false
          type: string
        - name: Username
          in: query
          required: false
          type: string
      responses:
        '200':
          description: Successfully generates the appropriate access/identity tokens.
          schema:
            $ref: '#/definitions/SignInResult'
        '400':
          description: Indicates an error while attempting to generate an authentication token.
          schema:
            $ref: '#/definitions/OpenIdConnectResponse'
        '403':
          description: Indicates the authorization request is not allowed because the user is not authenticated.


[HttpGet("~/connect/authorize")]
[ProducesResponseType(typeof(Microsoft.AspNetCore.Identity.SignInResult), 200)]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
{
    return Ok(null);
}

Upvotes: 1

Views: 1172

Answers (1)

Helder Sepulveda
Helder Sepulveda

Reputation: 17594

Here is what I can tell you from the code you shared:

      {
        "name": "Claims",
        "in": "query",
        "required": false,
        "type": "object"
      }

That does not follow the Open Api Specification:
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#fixed-fields-7

If in is any value other than "body"...

Since the parameter is not located at the request body, it is limited to simple types (that is, not an object). The value MUST be one of "string", "number", "integer", "boolean", "array" or "file".

If you manually change those "type": "object" to "type": "string" in the editor the errors will go away.


Change your action to include FromBody:

public async Task<IActionResult> Exchange([FromBody] OpenIdConnectRequest request)

Somehow swashbuckle is getting confused and thinking that is a query param when that should be in the body

Upvotes: 1

Related Questions