Skip to content

IOpenApiDocumentTransformer cannot modify components.Schemas since it's null #58406

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

Closed
1 task done
dnv-kimbell opened this issue Oct 14, 2024 · 15 comments
Closed
1 task done
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi ✔️ Resolution: Duplicate Resolved as a duplicate of another issue Status: Resolved

Comments

@dnv-kimbell
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

When running IOpenApiDocumentTransformer, it would be logical to have the ability to modify the OpenApiDocument.Components.Schemas to make any adjustments. The OpenApiDocument.Components is null.

for (var i = 0; i < _options.DocumentTransformers.Count; i++)
{
var transformer = _options.DocumentTransformers[i];
await transformer.TransformAsync(document, documentTransformerContext, cancellationToken);
}
// Move duplicated JSON schemas to the global components.schemas object and map references after all transformers have run.
await _schemaReferenceTransformer.TransformAsync(document, documentTransformerContext, cancellationToken);

The await _schemaReferenceTransformer.TransformAsync(document, documentTransformerContext, cancellationToken) populates the Components.Schemas, but that happens after the document transformers are run.

Expected Behavior

It should be possible to modify Components.Schemas in the IOpenApiDocumentTransformer

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

.NET 9 RC2

Anything else?

No response

@ghost ghost added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Oct 14, 2024
@captainsafia
Copy link
Member

@dnv-kimbell This is expected behavior. If you are trying to make schema-level customizations, the IOpenApiSchemaTransformer API is the one that you want to use.

What are you trying to do?

@captainsafia captainsafia added the Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. label Oct 14, 2024
@dnv-kimbell
Copy link
Author

The reason why I started exploring this was to see if I could create a workaround for polymorphic types. I can't see how the information provided by the current Microsoft implementation can give me enough information to generate C# classes.

There may also be other scenarios where one would want to look at the total list of schemas rather than each one individually. Based on the name of the transformer, it's not unreasonable to expect that you get a complete document, not just parts of it.

Since this code is part of the yearly release schedule, we have to wait another year for any significant changes. If we had access to information, it would be possible for us to create workarounds until the Microsoft implementation is updated.

@dotnet-policy-service dotnet-policy-service bot added Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. and removed Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. labels Oct 15, 2024
@marinasundstrom
Copy link

I have the same problem. I wanted to change the name of the schemas.

But I'm really looking for a way to influence the naming of the schemas, as like removing the suffix "Dto" from the type. That is important to me.

@captainsafia
Copy link
Member

But I'm really looking for a way to influence the naming of the schemas, as like removing the suffix "Dto" from the type. That is important to me.

Have you considered examining if the CreateSchemaReferenceId is a viable option for you? It allows influencing the name of reference IDs that are created for types (except the polymorphic one).

The reason why I started exploring this was to see if I could create a workaround for polymorphic types. I can't see how the information provided by the current Microsoft implementation can give me enough information to generate C# classes.

Out of curiosity, what information is missing for you here?

@captainsafia captainsafia removed the Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. label Oct 26, 2024
@marinasundstrom
Copy link

marinasundstrom commented Oct 26, 2024

I think I commented somewhere else, but I came up with this way after understanding that the annotation "x-schema-id is controls the naming:

    public static OpenApiOptions ApplySchemaNameTransforms(this OpenApiOptions options, Func<Type, string> transformer)
    {
        options.AddSchemaTransformer((schema, context, ct) =>
        {
            const string SchemaId = "x-schema-id";

            if (schema.Annotations?.TryGetValue(SchemaId, out var referenceIdObject) == true
                 && referenceIdObject is string newSchemaId)
            {
                var clrType = context.JsonTypeInfo.Type;
                newSchemaId = transformer(clrType);
                schema.Annotations[SchemaId] = newSchemaId;
            }

            return Task.CompletedTask;
        });

        return options;
    }

Polymorphic classes:

Yes, changing name doesn't work for polymorphic classes since that is dealt by its own private set of transformer methods that are run by default after the transformers have run.

Would be nice if those could become regular transformers, and that there would be transformers that allowed for inheritance, similar to what NSwag and Swashbuckle does by default.


On the IOpenApiDocumentTransformer:

Transformers should be seen as middleware, if that makes sense. But the final composition stage happens after all the transformer have run. That is when similar schemas are being merged and placed in Schemas in OpenApiDocument.

Because of what is mention above, it makes sense that at the transformer stage the "Components.Schemas" are not populated. But still confusing that the property is there if you, as a developer, don't understand this. Because you might expect that a IOpenApiDocumentTransformer has the complete document, which it does not have.

Question:

Should there be a way to hook into when a document has been fully composed? That would give you ability to do finishing touches. Even if risky.

@mattiasnordqvist
Copy link

I have problems with this too. I want to remove some specific schemas that are generated by default. I can't use a schematransformer to remove a schema, and i can't edit the nulled list of schemas in the documenttransformer.

@thomasrea0113
Copy link

I'm finding it quite frustrating trying to extend the default spec for this same reason mentioned here. I want to have an API endpoint that allows for partial model fields to be provided, which would be quite easy in other languages/frameworks. I've also found that the generated doc does not respect the configuration specified by WithOpenApi on a RouteHandlerBuilder. I feel like any IOpenApiDocumentTransformer implementations we provide should be the last stop after all other generation has occurred so that we can customize to our exact needs.

@thomasrea0113
Copy link

I think I commented somewhere else, but I came up with this way after understanding that the annotation "x-schema-id is controls the naming:

public static OpenApiOptions ApplySchemaNameTransforms(this OpenApiOptions options, Func<Type, string> transformer)
{
    options.AddSchemaTransformer((schema, context, ct) =>
    {
        const string SchemaId = "x-schema-id";

        if (schema.Annotations?.TryGetValue(SchemaId, out var referenceIdObject) == true
             && referenceIdObject is string newSchemaId)
        {
            var clrType = context.JsonTypeInfo.Type;
            newSchemaId = transformer(clrType);
            schema.Annotations[SchemaId] = newSchemaId;
        }

        return Task.CompletedTask;
    });

    return options;
}

Polymorphic classes:

Yes, changing name doesn't work for polymorphic classes since that is dealt by its own private set of transformer methods that are run by default after the transformers have run.

Would be nice if those could become regular transformers, and that there would be transformers that allowed for inheritance, similar to what NSwag and Swashbuckle does by default.

On the IOpenApiDocumentTransformer:

Transformers should be seen as middleware, if that makes sense. But the final composition stage happens after all the transformer have run. That is when similar schemas are being merged and placed in Schemas in OpenApiDocument.

Because of what is mention above, it makes sense that at the transformer stage the "Components.Schemas" are not populated. But still confusing that the property is there if you, as a developer, don't understand this. Because you might expect that a IOpenApiDocumentTransformer has the complete document, which it does not have.

Question:

Should there be a way to hook into when a document has been fully composed? That would give you ability to do finishing touches. Even if risky.

It's a huge oversight that the IOpenApiDocumentTransformer isn't the last stop in document generation. What use is it if we don't have access to the complete specification?

@captainsafia
Copy link
Member

Hi everyone! Doing some pruning on the backlog of OpenAPI issues and came across this one. I believe that we can close this out and dupe it to #60589.

Some new APIs are being introduced in Microsoft.OpenApi v2 that make it a little bit easier to treat the document as a component store in third party APIs. This includes the introduction of methods like OpenApiDocument.AddComponent<T>(string id, T object) that make it easier to insert schemas, security requirements, and other referenceable types into the top-level document.

The code sample below shows how these APIs can be composed to support being able to dynamically insert schemas to things. Note, that in this model, you never interact with document.components.schemas, just the top-level document.

builder.Services.AddOpenApi("v1", options =>
{
  options.AddOperationTransformer((operation, context, cancellationToken) =>
  {      
      // Generate schema for error responses
      var errorSchema = context.GetOrCreateSchema(typeof(ProblemDetails));
      context.Document.AddComponent("Error", errorSchema);
      
      // Reference the schema in responses
      operation.Responses["500"] = new OpenApiResponse
      {
          Description = "Error",
          Content =
          {
              ["application/problem+json"] = new OpenApiMediaType
              {
                  Schema = new OpenApiSchemaReference("Error", context.Document)
              }
          }
      };
      
      return Task.CompletedTask;
  });
});

The proposed API above supports adding a Document to the context for use as a store and adds the GetOrCreateSchema APIs.

@captainsafia captainsafia added the ✔️ Resolution: Duplicate Resolved as a duplicate of another issue label Mar 5, 2025
@dnv-kimbell
Copy link
Author

How does this new api allow us to enumerate all the existing schemas in the document? Some things can be done in a schema/operation transformer, but some things are easier to do when you have access to the totality. This might be an edge case, but when you have something called an IOpenApiDocumentTransformer you kinda expect to have access to the whole document.

@captainsafia
Copy link
Member

captainsafia commented Mar 7, 2025

How does this new api allow us to enumerate all the existing schemas in the document? Some things can be done in a schema/operation transformer, but some things are easier to do when you have access to the totality. This might be an edge case, but when you have something called an IOpenApiDocumentTransformer you kinda expect to have access to the whole document.

In the new model, you do have access to the all the schemas in the document since they are inserted into the document as they are constructed instead of via a document transformer as the final step (as in the current model).

The following code:

using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, ct) =>
    {
        Debug.Assert(document.Components != null, "Components should not be null");
        Debug.Assert(document.Components.Schemas != null, "Schemas should not be null");
        foreach (var schema in document.Components.Schemas)
        {
            Console.WriteLine(schema.Key);
        }
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapPost("/weather", (WeatherForecast forecast) => { });
app.MapPost("/todo", (Todo todo) => { });
app.MapPost("/user", (User user) => { });

app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);
record Todo(int Id, string Title, bool Completed);
record User(int Id, string Name, string Email);

will write the following to the console:

WeatherForecast
Todo
User

@jbrenson
Copy link

jbrenson commented Mar 7, 2025

Unfortunately that code sample isn't working for me - document.Components is always null in the document transformer.

In my case I have an OData endpoint which causes the Open API document to include a bunch of objects not really relevant to the API end users:

Image

Using Swashbuckle.AspNetCore.SwaggerGen I am able to remove schemas from the final open API document using the IDocumentFilter interface and the document.Components.Schemas.Remove() method in code similar to this:

Image

I am not seeing any way to remove schemas in the new Microsoft.AspNetCore.OpenApi IOpenApiDocumentTransformer since the Components object is always null on the document:

Image

Is there any way to remove or update schemas similar to how we currently can using the SwaggerGen package?

@captainsafia
Copy link
Member

Unfortunately that code sample isn't working for me - document.Components is always null in the document transformer.

To clarify, the code sample only works for .NET 10 because we take a dependency on some API changes in Microsoft.OpenApi v2. This isn't feasible in .NET 9 and we likely won't be making any retroactive changes to support it.

@paul-green
Copy link

paul-green commented Mar 23, 2025

You can use this middleware to modify the response on the way out. In this example I'm removing well-known types that I don't want in the response. They key part is loading it into the OpenApiStringReader which then allows you to modify any part of the document;

public static void UseCustomSwagger(this IApplicationBuilder app)
{
    string[] excludedSchemaTypes = [
    "Microsoft.AspNetCore.Mvc.ValidationProblemDetails",
    "Microsoft.AspNetCore.Mvc.ProblemDetails",
    "Microsoft.AspNetCore.Http.HttpValidationProblemDetails"];

    app.Use(async (context, next) =>
        {
            if (context.Request.Path == "/swagger/v1/swagger.json")
            {
                var originalBodyStream = context.Response.Body;
                using (var memoryStream = new MemoryStream())
                {
                    // Replace the response body stream with the memory stream
                    context.Response.Body = memoryStream;

                    await next.Invoke(); // Call the next middleware

                    // Reset the position to read from the beginning
                    memoryStream.Seek(0, SeekOrigin.Begin);
                    string responseBody = await new StreamReader(memoryStream).ReadToEndAsync();

                    var reader = new OpenApiStringReader();
                    var openApiDocument = reader.Read(responseBody, out var diagnostic);

                    var remove = openApiDocument.Components.Schemas.Where(s => excludedSchemaTypes.Contains(s.Value.Type));
                    foreach (var schema in remove)
                    {
                        openApiDocument.Components.Schemas.Remove(schema.Key);
                    }


                    using var stringWriter = new StringWriter();
                    var jsonWriter = new OpenApiJsonWriter(stringWriter);
                    openApiDocument.SerializeAsV3(jsonWriter);
                    jsonWriter.Flush();
                    var modifiedResponseBody = stringWriter.ToString();


                    byte[] modifiedBytes = Encoding.UTF8.GetBytes(modifiedResponseBody);

                    // Set the response content length if needed
                    context.Response.ContentLength = modifiedBytes.Length;
                    await originalBodyStream.WriteAsync(modifiedBytes);

                    return;
                }

            }

            await next();


        });
}

@luckyycode
Copy link

Since this is funny so I decided to keep it on:

public class SillyDocumentTransformer : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context,
        CancellationToken cancellationToken)
    {
        var type = Type.GetType("Microsoft.AspNetCore.OpenApi.OpenApiSchemaReferenceTransformer, Microsoft.AspNetCore.OpenApi");
        var method = type?.GetMethod("TransformAsync", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

        var instance = Activator.CreateInstance(type!);
        var task = (Task) method!.Invoke(instance, [document, context, CancellationToken.None])!;

        await task;
    }
}

It fulfills all the collections I need so now I can freely remove properties from openapi docs. Yes this is silly but it's working better than the variation above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi ✔️ Resolution: Duplicate Resolved as a duplicate of another issue Status: Resolved
Projects
None yet
Development

No branches or pull requests

9 participants