Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support returning OpenAPI document in YAML from MapOpenApi #58616

Merged
merged 2 commits into from
Nov 9, 2024

Conversation

captainsafia
Copy link
Member

Addresses #58516.

This PR adds support for returning the OpenAPI document at runtime in the YAML format. It does this by checking the extension on the route pattern that is provided for resolving the OpenAPI document. If a user calls:

app.MapOpenApi("/openapi/{documentName}.yaml");

Then visiting the /openapi/v1.yaml route in their application will return the document in YAML format. The implementation treats routes ending in either yaml or yml as YAML-generating by default. All other routes will produce an OpenAPI document serialized to JSON.

As an alternative approach, we could follow the pattern proposed in this issue and introduce an additional OpenApiFormat argument to the MapOpenApi method. However, because the implementation assumes that the extension appears in the route pattern by default we run the risk of users inadvertently calling:

app.MapOpenApi(format: OpenApiFormat.Yaml);

Which would use the default route pattern for the document that uses a JSON file extension with the incorrect file format. I figured it was more straightforward if we define the format based on the route pattern and avoid adding another option. There is a risk that more formats get added in the future and the complexity of our checks increases but that isn't a likely eventuality at the moment.

@captainsafia captainsafia requested a review from a team as a code owner October 24, 2024 21:49
@dotnet-issue-labeler dotnet-issue-labeler bot added the old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Oct 24, 2024
if (UseYaml(pattern))
{
document.Serialize(new OpenApiYamlWriter(writer), documentOptions.OpenApiVersion);
context.Response.ContentType = "application/yaml;charset=utf-8";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC 9512 declares application/yaml as the official content-type for YAML-based types. One thing I observed is that this MIME type is fairly new and most browsers still don't know how to render it inline and will fallback to downloading the YAML file to disk.

To get around this, we could use the text/plain+yaml content type to force inline rendering while still being in compliance with the MIME type spec for YAML.

Side note: This RFC is in draft which defines an official application/openapi content type that we might consider using in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on my limited knowledge of MIME types, application/yaml seems more correct since it is primarily for programmatic consumption, but I can definitely imagine people asking for text+yaml as a convenience. I see one of the RFC authors was from Mozilla, so hopefully built-in support isn't too far off?

@@ -29,11 +29,13 @@ public void MapOpenApi_ReturnsEndpointConventionBuilder()
Assert.IsAssignableFrom<IEndpointConventionBuilder>(returnedBuilder);
}

[Fact]
public void MapOpenApi_SupportsCustomizingPath()
[Theory]
Copy link
Member

@martincostello martincostello Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do any of these tests validate it is actually YAML? They check the content type and that a OpenAPI parser can parse the content, but if you replaced the writer implementation in the YAML block with the JSON one would they still pass?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it would still pass if the writer used was a JSON one. Since JSON is valid YAML, if we wanted to verify that the emitted content was actually YAML we'd have to perhaps do some string-based check to see if it contained the correct starting characters compared to a JSON output?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL JSON is YML, but that still feels wrong to me that the tests wouldn't detect the content not being the requested format.

@captainsafia captainsafia added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc and removed old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels Oct 24, 2024
Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems sensible, but might have a bug tail. Getting it in early makes sense.

if (UseYaml(pattern))
{
document.Serialize(new OpenApiYamlWriter(writer), documentOptions.OpenApiVersion);
context.Response.ContentType = "application/yaml;charset=utf-8";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on my limited knowledge of MIME types, application/yaml seems more correct since it is primarily for programmatic consumption, but I can definitely imagine people asking for text+yaml as a convenience. I see one of the RFC authors was from Mozilla, so hopefully built-in support isn't too far off?

context.Response.ContentType = "application/json;charset=utf-8";
if (UseYaml(pattern))
{
document.Serialize(new OpenApiYamlWriter(writer), documentOptions.OpenApiVersion);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume OpenApiYamlWriter does smart things to guard against, e.g., the Norway Problem?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAPI recommends YAML 1.2 of the spec, which prohibits non-boolean string literals from being interpreted as booleans, AFAIK.

The serialization itself seems to emit it as a string correctly. For example:

[JsonConverter(typeof(JsonStringEnumConverter<Choices>))]
public enum Choices
{
    Yes,
    No
}

becomes

schemas:
    Choices:
      enum:
        - Yes
        - No

}
else
{
document.Serialize(new OpenApiJsonWriter(writer), documentOptions.OpenApiVersion);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are people going to be surprised when openapi.toml returns JSON, rather than failing?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe give more control for the user, passing all values in params, and give some magic extension .json or .yaml in the end of path if exists or not or wrong will be removed and set the right extension, for content type / mime type pass in the last param like mimeType: | default value like pattern: "openapi/v1" format: JSON mimeType: "application/json" ( 0-0)/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are people going to be surprised when openapi.toml returns JSON, rather than failing?

Depends on user expectation and the type of experience that we want to provide. TOML isn't a supported OpenAPI document format so we could decide to throw if we encounter an extension and it's not associated with the supported set of formats.

  • Route with extension ending in .yaml or yml produces an OpenAPI document in YAML format.
  • Route with extension ending in .json produces an OpenAPI document in JSON format.
  • Route with extension not ending in any of the above produces an error.
  • Route with no extension produces an OpenAPI document in JSON format.

With this behavior, #3 would be a breaking change between .NET 9 and .NET 10 but I think we can live with it.

Maybe give more control for the user, passing all values in params, and give some magic extension .json or .yaml in the end of path if exists or not or wrong will be removed and set the right extension, for content type / mime type pass in the last param like mimeType: | default value like pattern: "openapi/v1" format: JSON mimeType: "application/json" ( 0-0)/

Yeah, I considered this option as well but opted to be more conservative in the amount of new API that was introduced. It also feels that given there are only two formats supported currently, we might want to support a simpler API for this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opted to avoid the breaking change for now. We've got runaway to revisit this if needed.

@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Nov 6, 2024
@john-shika
Copy link

/azp run LGTM

Copy link

Commenter does not have sufficient privileges for PR 58616 in repo dotnet/aspnetcore

@captainsafia
Copy link
Member Author

/azp run LGTM

Thanks! I owe another commit to this branch that improves the tests based on the feedback in #58616 (comment).

I'm inclined to leave the implementation as is for now (anything that isn't YAML will produce JSON) and avoid the extra logic in around what extension is actually in the file. We're early enough in the release cycle that we can actually change this.

@captainsafia captainsafia removed the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Nov 8, 2024
@captainsafia captainsafia merged commit 3c9639f into main Nov 9, 2024
27 checks passed
@captainsafia captainsafia deleted the safia/openapi-yml branch November 9, 2024 00:47
@dotnet-policy-service dotnet-policy-service bot added this to the 10.0-preview1 milestone Nov 9, 2024
captainsafia added a commit that referenced this pull request Feb 11, 2025
* Support returning OpenAPI document in YAML from MapOpenApi

* Update tests and YAML content type
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants