aspnetcore
aspnetcore copied to clipboard
IOpenApiDocumentTransformer cannot modify components.Schemas since it's null
Is there an existing issue for this?
- [x] 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.
https://github.com/dotnet/aspnetcore/blob/049814ca468cad1ea1e29412e0aa3eea182a63c1/src/OpenApi/src/Services/OpenApiDocumentService.cs#L100-L106
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
@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?
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.
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.
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?
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.
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.
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.
I think I commented somewhere else, but I came up with this way after understanding that the annotation
"x-schema-idis 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
IOpenApiDocumentTransformerhas 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?
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 https://github.com/dotnet/aspnetcore/issues/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.
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.
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
IOpenApiDocumentTransformeryou 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
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:
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:
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:
Is there any way to remove or update schemas similar to how we currently can using the SwaggerGen package?
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.
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();
});
}
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.