From 23b48a0d0f92706bc4f533cfa78077796ce8da61 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Fri, 2 Jan 2026 02:46:51 +0100 Subject: [PATCH] Upgrade Swashbuckle and fix OpenAPI spec (#15886) --- Directory.Packages.props | 4 +- .../ApiServiceCollectionExtensions.cs | 35 +----------- .../Filters/AdditionalModelFilter.cs | 9 ---- .../Filters/CachingOpenApiProvider.cs | 16 +++++- .../Filters/FlagsEnumSchemaFilter.cs | 53 +++++++++++++++++++ 5 files changed, 71 insertions(+), 46 deletions(-) create mode 100644 Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 14ff3fadbe..f71027e1a7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -80,8 +80,8 @@ - - + + diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 04dd19eda6..8373fd50fd 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -255,6 +255,7 @@ namespace Jellyfin.Server.Extensions c.AddSwaggerTypeMappings(); c.SchemaFilter(); + c.SchemaFilter(); c.OperationFilter(); c.OperationFilter(); c.OperationFilter(); @@ -342,25 +343,6 @@ namespace Jellyfin.Server.Extensions } }); - /* - * Support BlurHash dictionary - */ - options.MapType>>(() => - 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. options.MapType>(() => new OpenApiSchema @@ -373,21 +355,6 @@ namespace Jellyfin.Server.Extensions } }); - // Manually describe Flags enum. - options.MapType(() => - 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. options.MapType(() => new OpenApiSchema { diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs index 58d37db5a5..7407bd2eb7 100644 --- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs +++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs @@ -225,15 +225,6 @@ namespace Jellyfin.Server.Filters context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository); } - - context.SchemaRepository.AddDefinition(nameof(TranscodeReason), new OpenApiSchema - { - Type = "string", - Enum = Enum.GetNames() - .Select(e => new OpenApiString(e)) - .Cast() - .ToArray() - }); } } } diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs index b560ec50ef..833b684444 100644 --- a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs +++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs @@ -2,6 +2,7 @@ using System; using AsyncKeyedLock; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.Swagger; @@ -23,6 +24,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider private readonly IMemoryCache _memoryCache; private readonly SwaggerGenerator _swaggerGenerator; private readonly SwaggerGeneratorOptions _swaggerGeneratorOptions; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -31,15 +33,18 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider /// The api descriptions provider. /// The schema generator. /// The memory cache. + /// The logger. public CachingOpenApiProvider( IOptions optionsAccessor, IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, ISchemaGenerator schemaGenerator, - IMemoryCache memoryCache) + IMemoryCache memoryCache, + ILogger logger) { _swaggerGeneratorOptions = optionsAccessor.Value; _swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator); _memoryCache = memoryCache; + _logger = logger; } /// @@ -61,7 +66,16 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider throw new InvalidOperationException("OpenApi document is generating"); } + try + { openApiDocument = _swaggerGenerator.GetSwagger(documentName); + } + catch (Exception ex) + { + _logger.LogError(ex, "OpenAPI generation error"); + throw; + } + _memoryCache.Set(CacheKey, openApiDocument, _cacheOptions); return AdjustDocument(openApiDocument, host, basePath); } diff --git a/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs b/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs new file mode 100644 index 0000000000..3e0b69d017 --- /dev/null +++ b/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters; + +/// +/// Schema filter to ensure flags enums are represented correctly in OpenAPI. +/// +/// +/// 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. +/// +public class FlagsEnumSchemaFilter : ISchemaFilter +{ + /// + 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; + } + } +}