json-api-dotnet/JsonApiDotNetCore

OpenAPI: Previously used application/vnd.api+json returns 415 - Unsupported Media Type

Closed this issue · 11 comments

SUMMARY

First of all Great work with Integrating Open.Api and forming Swagger UI. I have a question concerning the supported media type we used so far: application/vnd.api+json.

DETAILS

I noticed that when I add the Swashbuckle prerelease package and I include it in the UI I have a problem with all my existing integrations. The reason appears to be the Content-Type of the request.
In all my current integrations I used this request Header:
Content-Type: application/vnd.api+json
When I Swashbuckle in API I get Status Code: 415 - Unsupported Media Type
The only way it works is if I set:
Content-Type: application/vnd.api+json; ext=openapi
Am I missing any configuration in order not to break compatibility with previous integrations while using Swagger?

STEPS TO REPRODUCE

  1. Updated to JsonApiDotNetCore and JsonApiDotNetCore.OpenApi.Swashbuckle.
  2. Include it in the code as mentioned in the documentation (I will provide the full Extension Method I created)
  3. Run Solution, open Swagger Doc, try request (Executes successfully)
  4. Open Postman with the same request and authentication but use the following header: Content-Type: application/vnd.api+json
  5. Error Response: 415 - Unsupported Media Type
internal static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services, IConfiguration configuration)
{
    services.AddOpenApiForJsonApi(options =>
    {
        var currentNs = configuration.GetCurrentNamespace();
        currentNs = string.IsNullOrEmpty(currentNs) ? currentNs : $"/{currentNs}";
        options.SwaggerDoc("v1", new OpenApiInfo { Title = "External Services", Version = "v1" });
        options.AddServer(new OpenApiServer { Url = $"{currentNs}/api/externalservices" });
        options.AddSwaggerAuth();
        options.TagActionsBy(apiDesc =>
        {

            var controllerType = apiDesc.ActionDescriptor.EndpointMetadata
             .OfType<DisplayNameAttribute>()
             .FirstOrDefault()?.DisplayName;

            return new List<string> { controllerType ?? apiDesc.ActionDescriptor.RouteValues["controller"] };
        });

        options.DocInclusionPredicate((name, api) => true);

        options.OperationFilter<DefaultApiVersionFilter>();
    });

    return services;
}

internal static SwaggerGenOptions AddSwaggerAuth(this SwaggerGenOptions options)
{
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.Http,
        Scheme = "bearer",
        BearerFormat = "JWT",
        Description = "Use JWT provided by Identity Service."
    });

    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            Array.Empty<string>()
        }
    });

    return options;
}

VERSIONS USED

  • JsonApiDotNetCore version: 5.7.1
  • JsonApiDotNetCore.OpenApi.Swashbuckle version: 5.7.1-preview.2
  • .NET version: 8.0

We had to change the OpenAPI document structure and introduce the extension at https://www.jsonapi.net/ext/openapi/ to support resource inheritance. Our OpenAPI package cannot function without that anymore. It is explained at #1704.

Are your existing integrations based on an earlier version of our package, or something else?

@bkoelman thank you for your response.

We were not using JsonApiDotNetCore.Open.Swashbuckle at all before.
What we want is that our client applications which are configured to call our API that follows JsonApi specification and still use as content type for their body: Content-Type: application/vnd.api+json

I understand why we need it for OpenApi integration and swagger ui but do we need to put it everywhere? It seems that the moment we stop calling method: AddSwaggerDocumentation that is mentioned above this extension in no longer recognized by the code.

Unfortunately, this is how our solution works. I wish things were simpler. The goal is to enable usage of all JsonApiDotNetCore features, which requires a JSON:API extension due to limitations in the OpenAPI spec itself.

In theory, we could make this conditional. But we don't want the document structure to radically change, after an API developer decides to use one of our features they didn't use before. That silently breaks existing clients. And for us it means an explosion of the set of possible combinations to test for, which is unfeasible.

Even if your API project doesn't use resource inheritance, we rely on OpenAPI inheritance to express returned includes, which is a mixture of types.

I understand this is inconvenient because you have a custom implementation that works differently. We aim to provide a stable solution for the document structure in future releases.

I totally agree and understand.
What I would suggest to consider is that since openapi is used as an extension I would also hope that unless it's necessary to be used like for example when I'm on a Swagger UI then to continue to work as it used to just not to support all the fixes and correction you've made for OpenApi. Again this is merely a suggestion which is completely with no Knowledge of the issues and Challenges you've faced so far with the OpenApi Integration.

So if I understand your use case right, that would be: only offer a documentation website and not care about client generators. This makes it annoying that there's a different content type now. Users may be hand-crafting JSON requests and never use client generators, so why do we need the change?

I wonder if we could offer both content types. It depends on how client generators react to that, ie: will they always pick the first, or always the last, or does that vary per generator? It's a risky change, though; we can check what NSwag and Kiota do today, but that may change, and new generators may be added that break it.

I've tried the following combinations:

  • Only default media type
  • Only openapi media type
  • Default media type, followed by openapi media type
  • Openapi media type, followed by default media type

Below are my observations on how different clients respond to that.

SwaggerUI

  • When multiple response content types, an Accept header with only the first one is sent.
  • When multiple request content types, user can choose via drop-down in UI, but the request always fails when the non-first entry is selected. Because anything else is incompatible with the always-first Accept header that is being sent.

Conclusion: multiple is a no-go; single entry (whether default or openapi media type) works fine.

Scalar

  • When an endpoint has no request body, no Accept header is sent. Otherwise, it always sends */*.
  • Because the Accept header can't be changed from the UI, no extensions can be used.
    • An operations request always fails, for any single or multiple media types.
    • Sending a request body using the openapi extension never works.
    • Sending a request body at a non-operations endpoint only succeeds when the default request content type is used (can edit via textbox).

Conclusion: operations endpoint never works; single entry using the default media type works best.

Kiota

  • When multiple response content types, they are combined in the outgoing Accept header, so order does not matter. Then JsonApiDotNetCore chooses the openapi media type when our OpenAPI NuGet package is referenced. Otherwise, it chooses the default media type.
  • When multiple request content types, a Content-Type header with only the first entry is always sent.

Conclusion: multiple is fine, as long as the openapi media type comes first.

NSwag

  • When multiple response content types, an Accept header with only the first one is sent.
  • When multiple request content types, a Content-Type header with only the first one is sent.
  • When first entry is default media type, produces a compile error for operations endpoint.

Conclusion: multiple is fine, as long as the openapi media type comes first.


Based on the above, offering multiple media types to improve the documentation website isn't helping anyone. Sticking with a single entry, there is none that works in all cases. This is problematic because it's ultimately up to the API consumer (not the API developer) what tooling they're going to use, so offering a configuration switch is painting API developers into a corner (because they'll never be able to toggle without breaking existing users).

Assuming this must be client-driven, a custom path, query string parameter, or HTTP header could be used to indicate a preference. Except this code executes before an HttpContext is available, because it is part of the process of building the ASP.NET routing table, discovering controllers, etc, so before any middleware executes. Even if it were available at this point, it wouldn't be when the OpenAPI document is being generated at build time.

While re-reading the original question, step 4 might be the reason why it didn't work. If using the openapi extension, request/response bodies typically contain openapi:discriminator properties. These are only allowed when the openapi extension is active. Therefore, copy/pasting a request/response body that works in SwaggerUI with the extension turned on can't always be replayed without modifications.

To clarify: whether the OpenAPI package is used or not, and whatever shows up in SwaggerUI, an API client can always use the default application/vnd.api+json media type and not use any extensions (except for the operations endpoint, which has always required the atomic extension, this hasn't changed). Adding OpenAPI to your project should never break existing users who don't use OpenAPI.

@bkoelman I'm not sure it works like you intend. AFAICT for POST/PATCH, JADNC when configured with AddOpenApiForJsonApi does not work with Accept header application/vnd.api+json but only with application/vnd.api+json; ext=openapi.

The error I get from JADNC is misleading - "Include 'application/vnd.api+json; ext=openapi' or 'application/vnd.api+json; ext="https://www.jsonapi.net/ext/openapi\"' or 'application/vnd.api+json' in the Accept header values."

Yes, I know. I'm working on a fix, but I don't have much time at the moment to complete it. Thanks for the heads-up, though.

#1747 fixes the bug where the Accept/Content-Type behavior changes for existing endpoints after enabling OpenAPI.

So if I understand your use case right, that would be: only offer a documentation website and not care about client generators. This makes it annoying that there's a different content type now. Users may be hand-crafting JSON requests and never use client generators, so why do we need the change?

I've spent a few more hours exploring what it would take to suppress the openapi extension in SwaggerUI and related tools.

Assuming this must be client-driven, a custom path, query string parameter, or HTTP header could be used to indicate a preference. Except this code executes before an HttpContext is available, because it is part of the process of building the ASP.NET routing table, discovering controllers, etc, so before any middleware executes. Even if it were available at this point, it wouldn't be when the OpenAPI document is being generated at build time.

The limitation that code executes before an HttpContext is available no longer applies. I even introduced options to indicate (1) whether to enable the openapi extension, and (2) whether to use it in the OpenAPI document, with (3) the capability to override it from a query string parameter.

Unfortunately, all of this brought me back to the original problem: a fundamentally different way of defining component schemas is required when the extension is unavailable. And supporting resource inheritance would never be possible to express. Furthermore, this is an enormous amount of work (months, at least) and effectively duplicates all existing tests. So I'm closing this as unplanned.

Tested main branch with the content negotation fix and it works nicely now, thanks @bkoelman.