From c4958365fe6a5209c3f65cbcc1496c4fff0415b0 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 25 Apr 2024 08:26:44 +0200 Subject: [PATCH] Support OpenAPI polymorphic output with JsonDerivedType (#16144) --- .../OpenApi/IOpenApiDiscriminator.cs | 2 +- .../IUmbracoJsonTypeInfoResolver.cs | 2 ++ .../UmbracoJsonTypeInfoResolver.cs | 29 +++++++++++++++++++ ...reUmbracoManagementApiSwaggerGenOptions.cs | 5 ++-- .../IPermissionPresentationModel.cs | 2 +- 5 files changed, 36 insertions(+), 4 deletions(-) rename src/{Umbraco.Cms.Api.Management => Umbraco.Cms.Api.Common}/OpenApi/IOpenApiDiscriminator.cs (87%) diff --git a/src/Umbraco.Cms.Api.Management/OpenApi/IOpenApiDiscriminator.cs b/src/Umbraco.Cms.Api.Common/OpenApi/IOpenApiDiscriminator.cs similarity index 87% rename from src/Umbraco.Cms.Api.Management/OpenApi/IOpenApiDiscriminator.cs rename to src/Umbraco.Cms.Api.Common/OpenApi/IOpenApiDiscriminator.cs index 6bf39b6c92..f60ad54c27 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi/IOpenApiDiscriminator.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/IOpenApiDiscriminator.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Api.Management.OpenApi; +namespace Umbraco.Cms.Api.Common.OpenApi; /// /// Marker interface that ensure the type have a "$type" discriminator in the open api schema. diff --git a/src/Umbraco.Cms.Api.Common/Serialization/IUmbracoJsonTypeInfoResolver.cs b/src/Umbraco.Cms.Api.Common/Serialization/IUmbracoJsonTypeInfoResolver.cs index 009f05e4e4..fe47f4ebee 100644 --- a/src/Umbraco.Cms.Api.Common/Serialization/IUmbracoJsonTypeInfoResolver.cs +++ b/src/Umbraco.Cms.Api.Common/Serialization/IUmbracoJsonTypeInfoResolver.cs @@ -5,4 +5,6 @@ namespace Umbraco.Cms.Api.Common.Serialization; public interface IUmbracoJsonTypeInfoResolver : IJsonTypeInfoResolver { IEnumerable FindSubTypes(Type type); + + string? GetTypeDiscriminatorValue(Type type); } diff --git a/src/Umbraco.Cms.Api.Common/Serialization/UmbracoJsonTypeInfoResolver.cs b/src/Umbraco.Cms.Api.Common/Serialization/UmbracoJsonTypeInfoResolver.cs index 5fc8bcf5f6..38d6df93e5 100644 --- a/src/Umbraco.Cms.Api.Common/Serialization/UmbracoJsonTypeInfoResolver.cs +++ b/src/Umbraco.Cms.Api.Common/Serialization/UmbracoJsonTypeInfoResolver.cs @@ -1,7 +1,10 @@ using System.Collections.Concurrent; using System.Text.Json; +using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using Umbraco.Cms.Api.Common.OpenApi; using Umbraco.Cms.Core.Composing; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Common.Serialization; @@ -15,6 +18,14 @@ public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, I public IEnumerable FindSubTypes(Type type) { + JsonDerivedTypeAttribute[] explicitJsonDerivedTypes = type + .GetCustomAttributes(false) + .ToArray(); + if (explicitJsonDerivedTypes.Any()) + { + return explicitJsonDerivedTypes.Select(a => a.DerivedType); + } + if (type.IsInterface is false) { // IMPORTANT: do NOT return an empty enumerable here. it will cause nullability to fail on reference @@ -33,6 +44,24 @@ public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, I return result; } + public string? GetTypeDiscriminatorValue(Type type) + { + JsonDerivedTypeAttribute? jsonDerivedTypeAttribute = type + .GetBaseTypes(false) + .WhereNotNull() + .SelectMany(baseType => baseType.GetCustomAttributes(false)) + .FirstOrDefault(attr => attr.DerivedType == type); + + if (jsonDerivedTypeAttribute is not null) + { + // IMPORTANT: do NOT perform fallback to type.Name here - it will work for the schema generation, + // but not for the actual serialization, and then it's only going to cause confusion. + return jsonDerivedTypeAttribute.TypeDiscriminator?.ToString(); + } + + return typeof(IOpenApiDiscriminator).IsAssignableFrom(type) ? type.Name : null; + } + public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) { JsonTypeInfo result = base.GetTypeInfo(type, options); diff --git a/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs index 7281b86b7b..6b67c805af 100644 --- a/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; +using Umbraco.Cms.Api.Common.OpenApi; using Umbraco.Cms.Api.Common.Serialization; using Umbraco.Cms.Api.Management.DependencyInjection; using Umbraco.Cms.Api.Management.OpenApi; @@ -34,8 +35,8 @@ public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions< swaggerGenOptions.UseOneOfForPolymorphism(); // Ensure all types that implements the IOpenApiDiscriminator have a $type property in the OpenApi schema with the default value (The class name) that is expected by the server - swaggerGenOptions.SelectDiscriminatorNameUsing(type => typeof(IOpenApiDiscriminator).IsAssignableFrom(type) ? "$type" : null); - swaggerGenOptions.SelectDiscriminatorValueUsing(type => typeof(IOpenApiDiscriminator).IsAssignableFrom(type) ? type.Name : null); + swaggerGenOptions.SelectDiscriminatorNameUsing(type => _umbracoJsonTypeInfoResolver.GetTypeDiscriminatorValue(type) is not null ? "$type" : null); + swaggerGenOptions.SelectDiscriminatorValueUsing(_umbracoJsonTypeInfoResolver.GetTypeDiscriminatorValue); swaggerGenOptions.AddSecurityDefinition( diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/IPermissionPresentationModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/IPermissionPresentationModel.cs index 5bbeeae9b3..8f3ca63772 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/IPermissionPresentationModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/IPermissionPresentationModel.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Api.Management.OpenApi; +using Umbraco.Cms.Api.Common.OpenApi; namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;