.NET ASP.NET Core OpenAPI Swagger Web API Documentação

OpenAPI no .NET 9: documentação de APIs profissional sem Swashbuckle

O .NET 9 trouxe suporte nativo a OpenAPI sem Swashbuckle. Aprenda a configurar documentação completa, versionamento de API.

N
Neryx Digital Architects
9 de janeiro de 2026
11 min de leitura
220 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

O .NET 9 mudou a forma como geramos documentação OpenAPI: o Swashbuckle — pacote de terceiros usado por praticamente toda API .NET há anos — deixou de ser a opção padrão. O framework agora tem suporte nativo a OpenAPI via Microsoft.AspNetCore.OpenApi. Este artigo cobre a migração e as funcionalidades avançadas da nova abordagem.

O que mudou no .NET 9

No .NET 8 e anteriores, o template padrão gerava:

dotnet add package Swashbuckle.AspNetCore

No .NET 9, o template usa:

dotnet add package Microsoft.AspNetCore.OpenApi  # Já incluído no SDK

A diferença não é apenas de pacote — é arquitetural. O Swashbuckle gera o documento OpenAPI em runtime via reflection. O suporte nativo usa source generation e transformers em tempo de build, resultando em startup mais rápido e documentação mais precisa.

Setup básico no .NET 9

// Program.cs — .NET 9
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_0;
    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        document.Info = new OpenApiInfo
        {
            Title = "Neryx Orders API",
            Version = "v1",
            Description = "API de pedidos da plataforma Neryx",
            Contact = new OpenApiContact
            {
                Name = "Equipe Neryx",
                Email = "api@neryx.com.br",
                Url = new Uri("https://neryx.com.br")
            },
            License = new OpenApiLicense
            {
                Name = "Proprietário",
                Url = new Uri("https://neryx.com.br/termos")
            }
        };
        return Task.CompletedTask;
    });
});

var app = builder.Build();

// Endpoint que serve o documento JSON
app.MapOpenApi(); // GET /openapi/v1.json

// Scalar UI (alternativa moderna ao Swagger UI no .NET 9)
app.MapScalarApiReference(); // GET /scalar/v1

// Swagger UI tradicional (ainda funciona com pacote adicional)
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/openapi/v1.json", "Neryx Orders API v1");
});

Scalar: o novo Swagger UI recomendado

dotnet add package Scalar.AspNetCore
// Scalar com configurações avançadas
app.MapScalarApiReference(options =>
{
    options.WithTitle("Neryx API")
           .WithTheme(ScalarTheme.DeepSpace) // Tema escuro elegante
           .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient)
           .WithPreferredScheme("Bearer") // JWT como padrão
           .WithHttpBearerAuthentication(bearer =>
           {
               bearer.Token = "seu-jwt-para-testes"; // Token pré-preenchido (só em dev)
           });
});

Anotando endpoints com metadados

// Minimal APIs — metadados inline
app.MapGet("/api/products/{id:guid}", async (Guid id, IProductService service) =>
{
    var product = await service.GetByIdAsync(id);
    return product is null ? Results.NotFound() : Results.Ok(product);
})
.WithName("GetProductById")
.WithSummary("Busca produto por ID")
.WithDescription("Retorna os detalhes completos de um produto ativo pelo seu identificador único.")
.WithTags("Products")
.Produces<ProductDto>(StatusCodes.Status200OK)
.Produces<ProblemDetails>(StatusCodes.Status404NotFound)
.RequireAuthorization();

// Controllers — via atributos XML + OpenAPI
/// <summary>
/// Cria um novo pedido para o cliente autenticado.
/// </summary>
/// <param name="request">Dados do pedido a ser criado.</param>
/// <returns>O pedido criado com ID e status inicial.</returns>
/// <response code="201">Pedido criado com sucesso.</response>
/// <response code="400">Dados de entrada inválidos.</response>
/// <response code="422">Regra de negócio violada.</response>
[HttpPost]
[ProducesResponseType(typeof(OrderDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> Create([FromBody] CreateOrderRequest request)
{
    // ...
}

Versionamento de API com documentação separada

dotnet add package Asp.Versioning.Http
dotnet add package Asp.Versioning.Mvc.ApiExplorer
// Program.cs
builder.Services
    .AddApiVersioning(options =>
    {
        options.DefaultApiVersion = new ApiVersion(1);
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.ReportApiVersions = true; // Adiciona headers api-supported-versions
        options.ApiVersionReader = ApiVersionReader.Combine(
            new UrlSegmentApiVersionReader(),        // /api/v1/products
            new HeaderApiVersionReader("X-Version"), // Header: X-Version: 1
            new QueryStringApiVersionReader("v"));   // ?v=1
    })
    .AddApiExplorer(options =>
    {
        options.GroupNameFormat = "'v'VVV"; // v1, v2
        options.SubstituteApiVersionInUrl = true;
    });

// Documento OpenAPI separado por versão
builder.Services.AddOpenApi("v1", options =>
{
    options.AddDocumentTransformer<V1DocumentTransformer>();
});

builder.Services.AddOpenApi("v2", options =>
{
    options.AddDocumentTransformer<V2DocumentTransformer>();
});

// Endpoints para cada versão
app.MapOpenApi("/openapi/{documentName}.json"); // /openapi/v1.json, /openapi/v2.json

app.MapScalarApiReference(options =>
{
    options.WithEndpointPrefix("/scalar/{documentName}")
           .AddDocuments("v1", "v2");
});
// Controllers versionados
[ApiController]
[ApiVersion(1)]
[ApiVersion(2)]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id:guid}")]
    [MapToApiVersion(1)]
    public async Task<IActionResult> GetByIdV1(Guid id)
    {
        // Resposta da v1 (simplificada)
    }

    [HttpGet("{id:guid}")]
    [MapToApiVersion(2)]
    public async Task<IActionResult> GetByIdV2(Guid id)
    {
        // Resposta da v2 (com campos adicionais)
    }
}

Configurando autenticação JWT no Swagger UI

// Document transformer para adicionar esquema de segurança JWT
public class JwtSecuritySchemeTransformer : IOpenApiDocumentTransformer
{
    private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;

    public JwtSecuritySchemeTransformer(
        IAuthenticationSchemeProvider authenticationSchemeProvider)
        => _authenticationSchemeProvider = authenticationSchemeProvider;

    public async Task TransformAsync(
        OpenApiDocument document,
        OpenApiDocumentTransformerContext context,
        CancellationToken cancellationToken)
    {
        var authenticationSchemes = await _authenticationSchemeProvider.GetAllSchemesAsync();

        if (authenticationSchemes.Any(s => s.Name == JwtBearerDefaults.AuthenticationScheme))
        {
            var requirements = new Dictionary<string, OpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer",
                    BearerFormat = "JWT",
                    Description = "Insira o token JWT. Exemplo: **eyJhbGci...**"
                }
            };

            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = requirements;

            // Aplica o esquema de segurança a todos os endpoints
            foreach (var operation in document.Paths.Values.SelectMany(p => p.Operations))
            {
                operation.Value.Security.Add(new OpenApiSecurityRequirement
                {
                    [new OpenApiSecurityScheme
                    {
                        Reference = new OpenApiReference
                        {
                            Id = "Bearer",
                            Type = ReferenceType.SecurityScheme
                        }
                    }] = Array.Empty<string>()
                });
            }
        }
    }
}

// Registrar
builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer<JwtSecuritySchemeTransformer>();
});

Operation transformer: enriquecer operações automaticamente

// Adiciona automaticamente resposta 401/403 em todos endpoints com [Authorize]
public class AuthorizationOperationTransformer : IOpenApiOperationTransformer
{
    public Task TransformAsync(
        OpenApiOperation operation,
        OpenApiOperationTransformerContext context,
        CancellationToken cancellationToken)
    {
        var hasAuthorize = context.Description.ActionDescriptor.EndpointMetadata
            .OfType<AuthorizeAttribute>()
            .Any();

        if (!hasAuthorize) return Task.CompletedTask;

        operation.Responses.TryAdd("401", new OpenApiResponse
        {
            Description = "Não autenticado — token JWT ausente ou inválido"
        });

        operation.Responses.TryAdd("403", new OpenApiResponse
        {
            Description = "Sem permissão para este recurso"
        });

        return Task.CompletedTask;
    }
}

// Schema transformer: adiciona exemplos a tipos comuns
public class ExampleSchemaTransformer : IOpenApiSchemaTransformer
{
    public Task TransformAsync(
        OpenApiSchema schema,
        OpenApiSchemaTransformerContext context,
        CancellationToken cancellationToken)
    {
        // Adiciona exemplos para tipos comuns
        if (context.JsonTypeInfo.Type == typeof(Guid))
        {
            schema.Example = new OpenApiString("3fa85f64-5717-4562-b3fc-2c963f66afa6");
        }
        else if (context.JsonTypeInfo.Type == typeof(decimal))
        {
            schema.Example = new OpenApiDouble(99.90);
        }
        else if (context.JsonTypeInfo.Type == typeof(DateTime))
        {
            schema.Example = new OpenApiString("2026-05-07T10:00:00Z");
        }

        return Task.CompletedTask;
    }
}

// Registrar transformers
builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer<AuthorizationOperationTransformer>();
    options.AddSchemaTransformer<ExampleSchemaTransformer>();
});

Expondo o documento apenas em não-produção

// Boas práticas de segurança: nunca exponha Swagger em produção
if (app.Environment.IsDevelopment() || app.Environment.IsStaging())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

// Alternativa: proteger com autenticação básica em staging
app.MapOpenApi().RequireAuthorization("ApiDocs");

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ApiDocs", policy =>
        policy.RequireRole("Developer", "Admin"));
});

Gerando o documento OpenAPI em tempo de build (CI/CD)

# Gerar o arquivo openapi.json durante o build para versionamento
dotnet add package Microsoft.Extensions.ApiDescription.Server

# No .csproj:
# <PropertyGroup>
#   <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
#   <OpenApiDocumentsDirectory>./docs</OpenApiDocumentsDirectory>
# </PropertyGroup>

# Após dotnet build, o arquivo docs/v1.json é gerado
# Use no pipeline para:
# 1. Diff de breaking changes entre versões
# 2. Geração de clientes SDK automaticamente
# 3. Validação de contrato antes do merge
# .github/workflows/api-docs.yml
- name: Gerar documentação OpenAPI
  run: dotnet build --configuration Release

- name: Verificar breaking changes na API
  run: |
    # Comparar o novo openapi.json com o da branch main
    npx @openapitools/openapi-diff \
      ./docs/v1-main.json \
      ./docs/v1.json \
      --fail-on-incompatible

- name: Publicar docs no GitHub Pages
  uses: peaceiris/actions-gh-pages@v3
  if: github.ref == 'refs/heads/main'
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: ./docs

Migração do Swashbuckle para o suporte nativo

Swashbuckle .NET 9 nativo
AddSwaggerGen() AddOpenApi()
UseSwagger() MapOpenApi()
UseSwaggerUI() MapScalarApiReference()
IDocumentFilter IOpenApiDocumentTransformer
IOperationFilter IOpenApiOperationTransformer
ISchemaFilter IOpenApiSchemaTransformer
SwaggerGen.IncludeXmlComments() Automático via XML comments

Uma API bem documentada é cartão de visita técnico. Se você precisa de uma documentação OpenAPI completa e integrada ao seu pipeline de CI/CD, fale com a Neryx.

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.