Start Debugging

Решение: не удаётся получить OpenAPI 3.0 после обновления Swashbuckle.AspNetCore до v9

Swashbuckle 8 и новее выдают openapi 3.0.4, а не 3.0.1, и значения OpenApiSpecVersion для patch-версии не существует. Почему так вышло и четыре способа зафиксировать нужную строку.

Вы обновили Swashbuckle.AspNetCore до 9.x, в коде по-прежнему стоит OpenApiSpecVersion.OpenApi3_0, а сгенерированный документ теперь содержит "openapi": "3.0.4" вместо "openapi": "3.0.1". Потребители документа его отвергают, а члена OpenApi3_0_1 в перечислении просто нет. Строка версии зашита константой внутри Microsoft.OpenApi, это не настройка Swashbuckle: 1.6.22 и более ранние пишут 3.0.1, 1.6.23 и более поздние пишут 3.0.4. Именно Swashbuckle 8.0.0 взял зависимость на 1.6.23, поэтому изменение затрагивает всех, кто пересекает границу 7.x. Решения ниже по порядку: обновить потребителя, переписать свойство самостоятельно в middleware или зафиксировать весь стек Swashbuckle на 7.2.0.

Всё описанное измерено на .NET SDK 10.0.201 для net10.0, с Swashbuckle.AspNetCore 6.5.0, 7.2.0, 8.1.4, 9.0.6 и 10.2.3.

Ошибки в контексте

Попытка запросить patch-версию напрямую через CLI:

System.NotSupportedException: The specified OpenAPI version "3.0.1" is not supported.
   at Swashbuckle.AspNetCore.Cli.Program.<>c.<Main>b__1_5(IDictionary`2 namedArgs)
   at Swashbuckle.AspNetCore.Cli.CommandRunner.Run(IEnumerable`1 args)
   at Swashbuckle.AspNetCore.Cli.Program.Main(String[] args)

Попытка удержать Microsoft.OpenApi, оставив Swashbuckle 9:

error NU1605: Warning As Error: Detected package downgrade: Microsoft.OpenApi from 1.6.25 to 1.6.22.
  Reference the package directly from the project to select a different version.
   MyApi -> Swashbuckle.AspNetCore 9.0.6 -> Swashbuckle.AspNetCore.Swagger 9.0.6 -> Microsoft.OpenApi (>= 1.6.25)
   MyApi -> Microsoft.OpenApi (>= 1.6.22)

А если заглушить NU1605 и всё же попробовать:

error CS1705: Assembly 'Swashbuckle.AspNetCore.SwaggerGen' with identity
'Swashbuckle.AspNetCore.SwaggerGen, Version=9.0.6.0, ...' uses 'Microsoft.OpenApi, Version=1.6.25.0, ...'
which has a higher version than referenced assembly 'Microsoft.OpenApi' with identity
'Microsoft.OpenApi, Version=1.6.22.0, ...'

Старые сборки Swagger UI отображают документ так:

Unable to render this definition
The provided definition does not specify a valid version field.
Please indicate a valid Swagger or OpenAPI version field. Supported version fields are
swagger: "2.0" and those that match openapi: 3.x.y (for example, openapi: 3.1.0).

Почему строка версии равна 3.0.4, а не тому, чем я управляю?

OpenApiSpecVersion это небольшое перечисление, и ни один из его членов не несёт номер patch-версии. В Microsoft.OpenApi 1.6.25, от которого зависит Swashbuckle 9.0.6, у него ровно два члена:

OpenApi2_0
OpenApi3_0

В Microsoft.OpenApi 2.7.5, от которого зависит Swashbuckle 10.2.3, добавляется ещё один:

OpenApi2_0
OpenApi3_0
OpenApi3_1

Членов 3.0.1, 3.0.3 или 3.0.4 нет, потому что patch-версия не является опцией сериализатора. OpenApiDocument.SerializeAsV3 пишет константу времени компиляции. Изменение видно по дампу строк из поставляемых сборок:

strings -a -e l on lib/netstandard2.0/Microsoft.OpenApi.dll:
  1.2.3   -> 3.0.1
  1.6.22  -> 3.0.1
  1.6.23  -> 3.0.4
  1.6.25  -> 3.0.4
  2.7.5   -> 3.0.4 and 3.1.1

Изменение пришло в OpenAPI.NET PR #2011, влитом 2024-12-20, который перенёс поведение v2 в ветку v1. Это не баг: OpenAPI 3.0.4 это настоящий patch-релиз спецификации, и выдавать самый свежий patch правильно по умолчанию. Проблема в том, что многие потребители проверяют поле openapi по жёстко заданному списку допустимых значений, а не по шаблону 3.0.x.

Какая версия Swashbuckle выдаёт какую patch-версию?

Поле openapi следует за той сборкой Microsoft.OpenApi, которая реально разрешилась, а не за версией Swashbuckle, записанной в csproj:

Swashbuckle.AspNetCoreMicrosoft.OpenApi (объявленная)поле openapi
6.5.01.2.33.0.1
7.2.01.6.223.0.1
с 8.0.0 по 8.1.41.6.233.0.4
с 9.0.0 по 9.0.6с 1.6.23 по 1.6.253.0.4
с 10.0.0 по 10.2.3с 2.3.0 по 2.7.53.0.4, либо 3.1.1 с OpenApi3_1

Два замечания. Во-первых, настоящая граница это 8.0.0, а не 9.0.0: если вы прыгнули с 7.x сразу на 9.x, вы пересекли её незаметно. Во-вторых, зависимость NuGet задаёт нижнюю границу, а не фиксирует версию. Проект на Swashbuckle 7.2.0, который дополнительно ссылается на что-то, тянущее Microsoft.OpenApi 1.6.23 или новее, разрешится в более новую сборку и начнёт выдавать 3.0.4 вообще без изменений Swashbuckle. Если документ изменился, а версия Swashbuckle нет, сначала выполните это:

dotnet list package --include-transitive

Минимальное воспроизведение на net10.0

// .NET SDK 10.0.201, net10.0, Swashbuckle.AspNetCore 9.0.6
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(o =>
    o.SwaggerDoc("v1", new OpenApiInfo { Title = "Demo", Version = "v1" }));

var app = builder.Build();
app.UseSwagger(o => o.OpenApiVersion = OpenApiSpecVersion.OpenApi3_0);
app.MapGet("/orders/{id}", (int id) => new Order(id, "open", null)).WithName("GetOrder");
app.Run();

record Order(int Id, string Status, string? Note);

GET /swagger/v1/swagger.json возвращает:

{
  "openapi": "3.0.4",
  "info": { "title": "Demo", "version": "v1" },
  "paths": { }
}

Явная установка OpenApiVersion здесь ничего не меняет, потому что OpenApi3_0 и так значение по умолчанию, а более тонкой градации перечисление не предлагает.

Можно ли передать patch-версию в CLI?

Нет. dotnet swagger tofile разбирает --openapiversion по закрытому набору из трёх строк. Из исходников v10.2.3:

// Swashbuckle.AspNetCore.Cli/Program.cs, v10.2.3
specVersion = versionArg switch
{
    "2.0" => OpenApiSpecVersion.OpenApi2_0,
    "3.0" => OpenApiSpecVersion.OpenApi3_0,
    "3.1" => OpenApiSpecVersion.OpenApi3_1,
    _ => throw new NotSupportedException($"The specified OpenAPI version \"{versionArg}\" is not supported."),
};

В 9.0.6 ветки "3.1" тоже нет, так что доступны только 2.0 и 3.0. Измеренный вывод для каждого допустимого значения в 10.2.3: 2.0 даёт "swagger": "2.0", 3.0 даёт "openapi": "3.0.4", 3.1 даёт "openapi": "3.1.1". Всё остальное, включая 3.0.1 и 3.1.1, приводит к исключению.

Отдельное замечание про CLI: инструмент 9.0.6 поставляется с apphost для net9.0, поэтому он отказывается запускаться на машине, где установлена только среда выполнения .NET 10. Установите DOTNET_ROLL_FORWARD=Major перед вызовом или поставьте соответствующую среду выполнения.

Поможет ли откат Microsoft.OpenApi до 1.6.22?

Не поможет ни на Swashbuckle 9, ни на 10, и именно этот совет чаще всего встречается в старых обсуждениях. Прямая ссылка сначала вызывает NU1605, который NuGet по умолчанию считает ошибкой. Если подавить его через <WarningsNotAsErrors>NU1605</WarningsNotAsErrors>, восстановление разрешится в 1.6.22, а затем компиляция упадёт с CS1705, потому что Swashbuckle.AspNetCore.Swagger 9.0.6 собран против идентичности сборки 1.6.25. Обе ошибки воспроизводятся на чистом проекте net10.0.

Путь фиксации версий работает, только если откатить весь стек:

<!-- net10.0, verified: emits "openapi": "3.0.1" -->
<ItemGroup>
  <PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
  <PackageReference Include="Microsoft.OpenApi" Version="1.6.22" />
</ItemGroup>

Swashbuckle 7.2.0 всё ещё нацелен на netstandard2.0 и нормально работает на net10.0, разрешая Microsoft.OpenApi в 1.6.22. Явная ссылка на Microsoft.OpenApi нужна, чтобы транзитивное повышение снова не утащило вас вперёд. Считайте это временной мерой со сроком, а не решением: вы замораживаете генератор OpenAPI на две мажорные версии назад, а в 8.x и 9.x есть исправления генерации схем, которые вам рано или поздно понадобятся.

Как переписать строку версии на Swashbuckle 9 или 10?

Точки расширения нет. Сопровождающие Swashbuckle прямо сказали это в issue #3540: SwaggerMiddleware сериализует прямо в поток ответа, ничего не оставляя посередине. Обходной путь, который они предлагают и который действительно работает, это буферизовать ответ и отредактировать свойство. Он одинаково работает на 9.0.6 и 10.2.3, потому что никогда не касается объектной модели:

// net10.0, Swashbuckle.AspNetCore 9.0.6 and 10.2.3, both verified
app.UseWhen(
    ctx => ctx.Request.Path.StartsWithSegments("/swagger")
        && ctx.Request.Path.Value!.EndsWith(".json"),
    branch => branch.Use(async (ctx, next) =>
    {
        var original = ctx.Response.Body;
        using var buffer = new MemoryStream();
        ctx.Response.Body = buffer;

        await next();

        ctx.Response.Body = original;
        if (ctx.Response.StatusCode != StatusCodes.Status200OK)
        {
            buffer.Position = 0;
            await buffer.CopyToAsync(original);
            return;
        }

        var json = Encoding.UTF8.GetString(buffer.ToArray())
            .Replace("\"openapi\": \"3.0.4\"", "\"openapi\": \"3.0.1\"", StringComparison.Ordinal);
        var bytes = Encoding.UTF8.GetBytes(json);
        ctx.Response.ContentLength = bytes.Length;
        await original.WriteAsync(bytes);
    }));

app.UseSwagger(o => o.OpenApiVersion = OpenApiSpecVersion.OpenApi3_0);
app.UseSwaggerUI();

Регистрируйте его до UseSwagger. Swagger UI продолжает работать, /swagger/index.html по-прежнему возвращает 200, а JSON-эндпоинт отдаёт 3.0.1. Важны две детали: вернуть ctx.Response.Body на исходный поток до записи и выставить ContentLength после переписывания, поскольку замена меняет количество байт. Фильтр .EndsWith(".json") не даёт буферизации задевать статические файлы UI. Если вы отдаёте ещё и YAML, добавьте отдельную ветку: там свойство пишется как openapi: '3.0.4', и JSON-замена не сработает.

Если буферизовать не хочется, замените эндпоинт целиком и сериализуйте документ сами:

// net10.0, Swashbuckle.AspNetCore 9.0.6
app.MapGet("/swagger/v1/swagger.json", (ISwaggerProvider provider) =>
{
    var document = provider.GetSwagger("v1");
    var node = JsonNode.Parse(document.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0))!;
    node["openapi"] = "3.0.1";
    return Results.Text(
        node.ToJsonString(new JsonSerializerOptions { WriteIndented = true }),
        "application/json");
}).ExcludeFromDescription();

ExcludeFromDescription() здесь не опция. Без него эндпоинт обнаруживает сам себя, и /swagger/v1/swagger.json появляется как задокументированный путь в собственном выводе. SerializeAsJson в ветке 1.6.x живёт в Microsoft.OpenApi.Extensions; в Swashbuckle 10 с Microsoft.OpenApi 2.x этого расширения больше нет, поэтому там предпочтителен middleware.

Для документа, генерируемого во время сборки через dotnet swagger tofile или OpenApiGenerateDocumentsOnBuild, ничего из этого в коде делать не нужно. Сгенерируйте с --openapiversion 3.0 и поправьте файл отдельным шагом сборки:

jq '.openapi = "3.0.1"' swagger.json > swagger.tmp && mv swagger.tmp swagger.json

Swagger UI по-прежнему отклоняет определение, что дальше?

Если браузер показывает “The provided definition does not specify a valid version field”, с документом всё в порядке, а UI устарел. Поддержка 3.0.4 появилась в swagger-ui v5.19.0, выпущенной 2025-02-17, через PR #10247. Swashbuckle подхватил её в Swashbuckle.AspNetCore.SwaggerUI 7.3.0. Всё, что старше, показывает ошибку на совершенно корректном документе 3.0.4.

Ловушка в рассинхроне версий внутри одного решения. Swashbuckle.AspNetCore.SwaggerUI это отдельный пакет, и проекты, ссылающиеся на три подпакета по отдельности, часто поднимают Swagger и SwaggerGen, оставляя SwaggerUI позади. Проверьте все три, затем перезагрузите страницу с очисткой кеша, потому что встроенный swagger-ui-bundle.js кешируется агрессивно.

Если проблема в рендерере, а не в документе, это подходящий момент посмотреть на отдачу документации через Scalar, который читает и 3.0.4, и 3.1 без нареканий.

А если мне действительно нужен 3.1?

Тогда нужен Swashbuckle 10 или новее, потому что в Microsoft.OpenApi 1.6.x члена OpenApi3_1 нет вовсе. В 10.x это включается явно, так что по умолчанию остаётся 3.0.4, а 3.1 запрашивается отдельно:

// net10.0, Swashbuckle.AspNetCore 10.2.3, emits "openapi": "3.1.1"
app.UseSwagger(o => o.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1);

Заложите время на обновление. Swashbuckle 10 переходит на Microsoft.OpenApi v2, где пространства имён уплощены, поэтому первое, во что вы упрётесь, это:

error CS0234: The type or namespace name 'Models' does not exist in the namespace 'Microsoft.OpenApi'

Удалите using Microsoft.OpenApi.Models;, поскольку типы теперь лежат прямо в Microsoft.OpenApi. Кроме того, конкретные типы модели становятся интерфейсами (OpenApiSchema превращается в IOpenApiSchema), строковые имена типов заменяются значениями перечисления JsonSchemaType, а WithOpenApi() больше не поддерживается. Руководство по миграции на v10 советует сначала пройти через 9.0.6, и это хороший совет: так ломающие изменения 9.x (отказ от netstandard2.0, удаление устаревших членов, удаление --serializeasv2) отделяются от изменений OpenAPI.NET v2.

Какое решение выбрать?

По порядку того, что я сделал бы на практике:

  1. Обновите потребителя. 3.0.4 это корректный OpenAPI 3.0, и любой актуальный валидатор, генератор или шлюз его принимает. Большинство таких сообщений сводится к инструменту, отставшему на три версии.
  2. Если потребитель это вендор, которого не сдвинуть, добавьте переписывание в middleware. Это 20 строк, оно не зависит от версии и не замораживает граф зависимостей.
  3. Правьте файл в CI через jq, если документ генерируется на этапе сборки, а не отдаётся во время выполнения.
  4. Фиксируйте Swashbuckle на 7.2.0 только как временную меру, с заведённой задачей на снятие.

Что не работает, что бы ни говорили результаты поиска: откат Microsoft.OpenApi под актуальным Swashbuckle и поиск члена OpenApiSpecVersion, кодирующего patch-версию.

Похожие материалы

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад