Upgrade Swashbuckle and fix OpenAPI spec (#15886)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled

This commit is contained in:
Tim Eisele
2026-01-02 02:46:51 +01:00
committed by GitHub
parent d1055b0b36
commit 23b48a0d0f
5 changed files with 71 additions and 46 deletions

View File

@@ -80,8 +80,8 @@
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.2.1" /> <PackageVersion Include="Svg.Skia" Version="3.2.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
<PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" /> <PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.11" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.11" />

View File

@@ -255,6 +255,7 @@ namespace Jellyfin.Server.Extensions
c.AddSwaggerTypeMappings(); c.AddSwaggerTypeMappings();
c.SchemaFilter<IgnoreEnumSchemaFilter>(); c.SchemaFilter<IgnoreEnumSchemaFilter>();
c.SchemaFilter<FlagsEnumSchemaFilter>();
c.OperationFilter<RetryOnTemporarilyUnavailableFilter>(); c.OperationFilter<RetryOnTemporarilyUnavailableFilter>();
c.OperationFilter<SecurityRequirementsOperationFilter>(); c.OperationFilter<SecurityRequirementsOperationFilter>();
c.OperationFilter<FileResponseFilter>(); c.OperationFilter<FileResponseFilter>();
@@ -342,25 +343,6 @@ namespace Jellyfin.Server.Extensions
} }
}); });
/*
* Support BlurHash dictionary
*/
options.MapType<Dictionary<ImageType, Dictionary<string, string>>>(() =>
new OpenApiSchema
{
Type = "object",
Properties = typeof(ImageType).GetEnumNames().ToDictionary(
name => name,
_ => new OpenApiSchema
{
Type = "object",
AdditionalProperties = new OpenApiSchema
{
Type = "string"
}
})
});
// Support dictionary with nullable string value. // Support dictionary with nullable string value.
options.MapType<Dictionary<string, string?>>(() => options.MapType<Dictionary<string, string?>>(() =>
new OpenApiSchema new OpenApiSchema
@@ -373,21 +355,6 @@ namespace Jellyfin.Server.Extensions
} }
}); });
// Manually describe Flags enum.
options.MapType<TranscodeReason>(() =>
new OpenApiSchema
{
Type = "array",
Items = new OpenApiSchema
{
Reference = new OpenApiReference
{
Id = nameof(TranscodeReason),
Type = ReferenceType.Schema,
}
}
});
// Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it. // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
options.MapType<Version>(() => new OpenApiSchema options.MapType<Version>(() => new OpenApiSchema
{ {

View File

@@ -225,15 +225,6 @@ namespace Jellyfin.Server.Filters
context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository); context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository);
} }
context.SchemaRepository.AddDefinition(nameof(TranscodeReason), new OpenApiSchema
{
Type = "string",
Enum = Enum.GetNames<TranscodeReason>()
.Select(e => new OpenApiString(e))
.Cast<IOpenApiAny>()
.ToArray()
});
} }
} }
} }

View File

@@ -2,6 +2,7 @@ using System;
using AsyncKeyedLock; using AsyncKeyedLock;
using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.Swagger;
@@ -23,6 +24,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider
private readonly IMemoryCache _memoryCache; private readonly IMemoryCache _memoryCache;
private readonly SwaggerGenerator _swaggerGenerator; private readonly SwaggerGenerator _swaggerGenerator;
private readonly SwaggerGeneratorOptions _swaggerGeneratorOptions; private readonly SwaggerGeneratorOptions _swaggerGeneratorOptions;
private readonly ILogger<CachingOpenApiProvider> _logger;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CachingOpenApiProvider"/> class. /// Initializes a new instance of the <see cref="CachingOpenApiProvider"/> class.
@@ -31,15 +33,18 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider
/// <param name="apiDescriptionsProvider">The api descriptions provider.</param> /// <param name="apiDescriptionsProvider">The api descriptions provider.</param>
/// <param name="schemaGenerator">The schema generator.</param> /// <param name="schemaGenerator">The schema generator.</param>
/// <param name="memoryCache">The memory cache.</param> /// <param name="memoryCache">The memory cache.</param>
/// <param name="logger">The logger.</param>
public CachingOpenApiProvider( public CachingOpenApiProvider(
IOptions<SwaggerGeneratorOptions> optionsAccessor, IOptions<SwaggerGeneratorOptions> optionsAccessor,
IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, IApiDescriptionGroupCollectionProvider apiDescriptionsProvider,
ISchemaGenerator schemaGenerator, ISchemaGenerator schemaGenerator,
IMemoryCache memoryCache) IMemoryCache memoryCache,
ILogger<CachingOpenApiProvider> logger)
{ {
_swaggerGeneratorOptions = optionsAccessor.Value; _swaggerGeneratorOptions = optionsAccessor.Value;
_swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator); _swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator);
_memoryCache = memoryCache; _memoryCache = memoryCache;
_logger = logger;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -61,7 +66,16 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider
throw new InvalidOperationException("OpenApi document is generating"); throw new InvalidOperationException("OpenApi document is generating");
} }
try
{
openApiDocument = _swaggerGenerator.GetSwagger(documentName); openApiDocument = _swaggerGenerator.GetSwagger(documentName);
}
catch (Exception ex)
{
_logger.LogError(ex, "OpenAPI generation error");
throw;
}
_memoryCache.Set(CacheKey, openApiDocument, _cacheOptions); _memoryCache.Set(CacheKey, openApiDocument, _cacheOptions);
return AdjustDocument(openApiDocument, host, basePath); return AdjustDocument(openApiDocument, host, basePath);
} }

View File

@@ -0,0 +1,53 @@
using System;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters;
/// <summary>
/// Schema filter to ensure flags enums are represented correctly in OpenAPI.
/// </summary>
/// <remarks>
/// For flags enums:
/// - The enum schema definition is set to type "string" (not integer).
/// - Properties using flags enums are transformed to arrays referencing the enum schema.
/// </remarks>
public class FlagsEnumSchemaFilter : ISchemaFilter
{
/// <inheritdoc />
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var type = context.Type.IsEnum ? context.Type : Nullable.GetUnderlyingType(context.Type);
if (type is null || !type.IsEnum)
{
return;
}
// Check if enum has [Flags] attribute
if (!type.IsDefined(typeof(FlagsAttribute), false))
{
return;
}
if (context.MemberInfo is null)
{
// Processing the enum definition itself - ensure it's type "string" not "integer"
schema.Type = "string";
schema.Format = null;
}
else
{
// Processing a property that uses the flags enum - transform to array
// Generate the enum schema to ensure it exists in the repository
var enumSchema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
// Flags enums should be represented as arrays referencing the enum schema
// since multiple values can be combined
schema.Type = "array";
schema.Format = null;
schema.Enum = null;
schema.AllOf = null;
schema.Items = enumSchema;
}
}
}