Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for JsonPolymorphic and JsonDerivedType attributes to Swashbuckle.AspNetCore.Annotations for .NET7 and later #2671

Closed
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,7 @@ services.AddSwaggerGen(c =>
});
```

_NOTE: If you're using the [Swashbuckle Annotations library](#swashbuckleaspnetcoreannotations), it contains a custom selector that's based on the presence of `SwaggerSubType` attributes on base class definitions. This way, you can use simple attributes to explicitly list the inheritance and/or polymorphism relationships you want to expose. To enable this behavior, check out the [Annotations docs](#list-known-subtypes-for-inheritance-and-polymorphism)._
_NOTE: If you're using the [Swashbuckle Annotations library](#swashbuckleaspnetcoreannotations), it contains a custom selector that's based on the presence of `JsonDerivedType` (or `SwaggerSubType` for .NET 6 or earlier) attributes on base class definitions. This way, you can use simple attributes to explicitly list the inheritance and/or polymorphism relationships you want to expose. To enable this behavior, check out the [Annotations docs](#list-known-subtypes-for-inheritance-and-polymorphism)._

#### Describing Discriminators ####

Expand Down Expand Up @@ -1231,7 +1231,7 @@ services.AddSwaggerGen(c =>
});
```

_NOTE: If you're using the [Swashbuckle Annotations library](#swashbuckleaspnetcoreannotations), it contains custom selector functions that are based on the presence of `SwaggerDiscriminator` and `SwaggerSubType` attributes on base class definitions. This way, you can use simple attributes to explicitly provide discriminator metadata. To enable this behavior, check out the [Annotations docs](#enrich-polymorphic-base-classes-with-discriminator-metadata)._
_NOTE: If you're using the [Swashbuckle Annotations library](#swashbuckleaspnetcoreannotations), it contains custom selector functions that are based on the presence of `JsonPolymorphic` (or `SwaggerDiscriminator` for .NET 6 or earlier) and `JsonDerivedType` (or `SwaggerSubType` for .NET 6 or earlier) attributes on base class definitions. This way, you can use simple attributes to explicitly provide discriminator metadata. To enable this behavior, check out the [Annotations docs](#enrich-polymorphic-base-classes-with-discriminator-metadata)._

## Swashbuckle.AspNetCore.SwaggerUI ##

Expand Down Expand Up @@ -1539,6 +1539,15 @@ services.AddSwaggerGen(c =>
});

// Shape.cs

// .NET 7 or later
[JsonDerivedType(typeof(Rectangle))]
[JsonDerivedType(typeof(Circle))]
public abstract class Shape
{
}

// .NET 6 or earlier
[SwaggerSubType(typeof(Rectangle))]
[SwaggerSubType(typeof(Circle))]
public abstract class Shape
Expand All @@ -1548,7 +1557,7 @@ public abstract class Shape

### Enrich Polymorphic Base Classes with Discriminator Metadata ###

If you're using annotations to _explicitly_ indicate the "known" subtypes for a polymorphic base type, you can combine the `SwaggerDiscriminatorAttribute` with the `SwaggerSubTypeAttribute` to provide additional metadata about the "discriminator" property, which will then be incorporated into the generated schema definition:
If you're using annotations to _explicitly_ indicate the "known" subtypes for a polymorphic base type, you can combine the `JsonPolymorphicAttribute` with the `JsonDerivedTypeAttribute` to provide additional metadata about the "discriminator" property, which will then be incorporated into the generated schema definition:


```csharp
Expand All @@ -1559,12 +1568,32 @@ services.AddSwaggerGen(c =>
});

// Shape.cs

// .NET 7 or later
[JsonPolymorphic(TypeDiscriminatorPropertyName = "shapeType")]
[JsonDerivedType(typeof(Rectangle), "rectangle")]
[JsonDerivedType(typeof(Circle), "circle")]
public abstract class Shape
{
// Avoid using a JsonPolymorphicAttribute.TypeDiscriminatorPropertyName
// that conflicts with a property in your type hierarchy.
// Related issue: https://github.com/dotnet/runtime/issues/72170
}

// .NET 6 or earlier
[SwaggerDiscriminator("shapeType")]
[SwaggerSubType(typeof(Rectangle), DiscriminatorValue = "rectangle")]
[SwaggerSubType(typeof(Circle), DiscriminatorValue = "circle")]
public abstract class Shape
{
public ShapeType { get; set; }
public ShapeType ShapeType { get; set; }
}

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ShapeType
{
Circle,
Rectangle
}
```

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.Annotations;

Expand Down Expand Up @@ -56,8 +57,22 @@ public static void EnableAnnotations(this SwaggerGenOptions options)

private static IEnumerable<Type> AnnotationsSubTypesSelector(Type type)
{
#if NET7_0_OR_GREATER
var jsonDerivedTypeAttributes = type.GetCustomAttributes(false)
.OfType<JsonDerivedTypeAttribute>()
.ToArray();

if (jsonDerivedTypeAttributes.Any())
{
return jsonDerivedTypeAttributes.Select(attr => attr.DerivedType);
}
#endif

#pragma warning disable CS0618 // Type or member is obsolete
var subTypeAttributes = type.GetCustomAttributes(false)
.OfType<SwaggerSubTypeAttribute>();
.OfType<SwaggerSubTypeAttribute>()
.ToArray();
#pragma warning restore CS0618 // Type or member is obsolete

if (subTypeAttributes.Any())
{
Expand All @@ -80,9 +95,22 @@ private static IEnumerable<Type> AnnotationsSubTypesSelector(Type type)

private static string AnnotationsDiscriminatorNameSelector(Type baseType)
{
#if NET7_0_OR_GREATER
var jsonPolymorphicAttribute = baseType.GetCustomAttributes(false)
.OfType<JsonPolymorphicAttribute>()
.FirstOrDefault();

if (jsonPolymorphicAttribute is not null)
{
return jsonPolymorphicAttribute.TypeDiscriminatorPropertyName;
}
#endif

#pragma warning disable CS0618 // Type or member is obsolete
var discriminatorAttribute = baseType.GetCustomAttributes(false)
.OfType<SwaggerDiscriminatorAttribute>()
.FirstOrDefault();
#pragma warning restore CS0618 // Type or member is obsolete

if (discriminatorAttribute != null)
{
Expand All @@ -108,9 +136,22 @@ private static string AnnotationsDiscriminatorValueSelector(Type subType)
if (subType.BaseType == null)
return null;

#if NET7_0_OR_GREATER
var jsonDerivedTypeAttribute = subType.BaseType.GetCustomAttributes(false)
.OfType<JsonDerivedTypeAttribute>()
.FirstOrDefault(attr => attr.DerivedType == subType);

if (jsonDerivedTypeAttribute is not null)
{
return jsonDerivedTypeAttribute.TypeDiscriminator?.ToString();
}
#endif

#pragma warning disable CS0618 // Type or member is obsolete
var subTypeAttribute = subType.BaseType.GetCustomAttributes(false)
.OfType<SwaggerSubTypeAttribute>()
.FirstOrDefault(attr => attr.SubType == subType);
#pragma warning restore CS0618 // Type or member is obsolete

if (subTypeAttribute != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public partial class SwaggerVerifyIntegrationTest
[InlineData(typeof(GenericControllers.Startup), "/swagger/v1/swagger.json")]
[InlineData(typeof(MultipleVersions.Startup), "/swagger/1.0/swagger.json")]
[InlineData(typeof(MultipleVersions.Startup), "/swagger/2.0/swagger.json")]
[InlineData(typeof(NSwagClientExample.Startup), "/swagger/v1/swagger.json")]
[InlineData(typeof(OAuth2Integration.Startup), "/resource-server/swagger/v1/swagger.json")]
[InlineData(typeof(ReDocApp.Startup), "/swagger/v1/swagger.json")]
[InlineData(typeof(TestFirst.Startup), "/swagger/v1-generated/openapi.json")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<ProjectReference Include="..\WebSites\CustomUIIndex\CustomUIIndex.csproj" />
<ProjectReference Include="..\WebSites\GenericControllers\GenericControllers.csproj" />
<ProjectReference Include="..\WebSites\MultipleVersions\MultipleVersions.csproj" />
<ProjectReference Include="..\WebSites\NswagClientExample\NswagClientExample.csproj" />
<ProjectReference Include="..\WebSites\OAuth2Integration\OAuth2Integration.csproj" />
<ProjectReference Include="..\WebSites\ReDoc\ReDoc.csproj" />
<ProjectReference Include="..\WebSites\TestFirst\TestFirst.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#if NET7_0_OR_GREATER
using System;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;

namespace NSwagClientExample.Controllers
{
[ApiController]
[Route("[controller]")]
public class PaymentMethodsController : ControllerBase
{
[HttpPost]
public void AddPaymentMethod([Required]PaymentMethod paymentMethod)
{
throw new NotImplementedException();
}
}

[JsonPolymorphic(TypeDiscriminatorPropertyName = "paymentMethod")]
[JsonDerivedType(typeof(CreditCard), "CreditCard")]
[JsonDerivedType(typeof(MobileWallet), "MobileWallet")]
public class PaymentMethod
{
}

public class CreditCard : PaymentMethod
{
public string CardNumber { get; set; }
}

public class MobileWallet : PaymentMethod
{
public string WalletId { get; set; }
}
}
#endif
115 changes: 115 additions & 0 deletions test/WebSites/NswagClientExample/swagger_net8.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,68 @@
}
}
}
},
"/PaymentMethods": {
"post": {
"tags": [
"PaymentMethods"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PaymentMethod"
},
{
"$ref": "#/components/schemas/CreditCard"
},
{
"$ref": "#/components/schemas/MobileWallet"
}
]
}
},
"text/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PaymentMethod"
},
{
"$ref": "#/components/schemas/CreditCard"
},
{
"$ref": "#/components/schemas/MobileWallet"
}
]
}
},
"application/*+json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PaymentMethod"
},
{
"$ref": "#/components/schemas/CreditCard"
},
{
"$ref": "#/components/schemas/MobileWallet"
}
]
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK"
}
}
}
}
},
"components": {
Expand Down Expand Up @@ -118,6 +180,23 @@
}
]
},
"CreditCard": {
"allOf": [
{
"$ref": "#/components/schemas/PaymentMethod"
},
{
"type": "object",
"properties": {
"cardNumber": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
}
]
},
"Dog": {
"allOf": [
{
Expand All @@ -134,6 +213,42 @@
"additionalProperties": false
}
]
},
"MobileWallet": {
"allOf": [
{
"$ref": "#/components/schemas/PaymentMethod"
},
{
"type": "object",
"properties": {
"walletId": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
}
]
},
"PaymentMethod": {
"required": [
"paymentMethod"
],
"type": "object",
"properties": {
"paymentMethod": {
"type": "string"
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "paymentMethod",
"mapping": {
"CreditCard": "#/components/schemas/CreditCard",
"MobileWallet": "#/components/schemas/MobileWallet"
}
}
}
}
}
Expand Down