Skip to content

Commit 6f89ded

Browse files
bkoelmanmaurei
andauthored
OpenAPI: Use allOf for complex types (#1372)
* Switch to built-in support for nullable reference schemas * Fix generated documentation on relationships and enum properties --------- Co-authored-by: Maurits Moeys <[email protected]>
1 parent 088fde5 commit 6f89ded

File tree

26 files changed

+3698
-1319
lines changed

26 files changed

+3698
-1319
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Microsoft.OpenApi.Models;
2+
3+
namespace JsonApiDotNetCore.OpenApi;
4+
5+
internal static class OpenApiSchemaExtensions
6+
{
7+
public static OpenApiSchema UnwrapExtendedReferenceSchema(this OpenApiSchema source)
8+
{
9+
ArgumentGuard.NotNull(source);
10+
11+
if (source.AllOf.Count != 1)
12+
{
13+
throw new InvalidOperationException($"Schema '{nameof(source)}' should not contain multiple entries in '{nameof(source.AllOf)}' ");
14+
}
15+
16+
return source.AllOf.Single();
17+
}
18+
}

src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ private static void AddSwaggerGenerator(IServiceScope scope, IServiceCollection
7070
SetOperationInfo(swaggerGenOptions, controllerResourceMapping, namingPolicy);
7171
SetSchemaIdSelector(swaggerGenOptions, resourceGraph, namingPolicy);
7272
swaggerGenOptions.DocumentFilter<EndpointOrderingFilter>();
73+
swaggerGenOptions.UseAllOfToExtendReferenceSchemas();
7374
swaggerGenOptions.OperationFilter<JsonApiOperationDocumentationFilter>();
7475

7576
setupSwaggerGenAction?.Invoke(swaggerGenOptions);

src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs

+21-5
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ internal sealed class JsonApiSchemaGenerator : ISchemaGenerator
4747
private readonly ISchemaGenerator _defaultSchemaGenerator;
4848
private readonly IJsonApiOptions _options;
4949
private readonly ResourceObjectSchemaGenerator _resourceObjectSchemaGenerator;
50-
private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator;
5150
private readonly SchemaRepositoryAccessor _schemaRepositoryAccessor = new();
5251

5352
public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options,
@@ -60,7 +59,6 @@ public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceG
6059

6160
_defaultSchemaGenerator = defaultSchemaGenerator;
6261
_options = options;
63-
_nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(_schemaRepositoryAccessor, options.SerializerOptions.PropertyNamingPolicy);
6462

6563
_resourceObjectSchemaGenerator = new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceGraph, options, _schemaRepositoryAccessor,
6664
resourceFieldValidationMetadataProvider);
@@ -76,7 +74,11 @@ public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepos
7674

7775
if (schemaRepository.TryLookupByType(modelType, out OpenApiSchema jsonApiDocumentSchema))
7876
{
79-
return jsonApiDocumentSchema;
77+
// For unknown reasons, Swashbuckle chooses to wrap root request bodies, but not response bodies.
78+
// See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/861#issuecomment-1373631712
79+
return memberInfo != null || parameterInfo != null
80+
? _defaultSchemaGenerator.GenerateSchema(modelType, schemaRepository, memberInfo, parameterInfo)
81+
: jsonApiDocumentSchema;
8082
}
8183

8284
if (IsJsonApiDocument(modelType))
@@ -92,6 +94,8 @@ public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepos
9294
{
9395
RemoveJsonApiObject(schema);
9496
}
97+
98+
// Schema might depend on other schemas not handled by us, so should not return here.
9599
}
96100

97101
return _defaultSchemaGenerator.GenerateSchema(modelType, schemaRepository, memberInfo, parameterInfo, routeInfo);
@@ -116,7 +120,7 @@ private OpenApiSchema GenerateJsonApiDocumentSchema(Type documentType)
116120

117121
OpenApiSchema referenceSchemaForDataObject = IsManyDataDocument(documentType)
118122
? CreateArrayTypeDataSchema(referenceSchemaForResourceObject)
119-
: referenceSchemaForResourceObject;
123+
: CreateExtendedReferenceSchema(referenceSchemaForResourceObject);
120124

121125
fullSchemaForDocument.Properties[JsonApiPropertyName.Data] = referenceSchemaForDataObject;
122126

@@ -150,7 +154,8 @@ private void SetDataObjectSchemaToNullable(OpenApiSchema referenceSchemaForDocum
150154
{
151155
OpenApiSchema fullSchemaForDocument = _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForDocument.Reference.Id];
152156
OpenApiSchema referenceSchemaForData = fullSchemaForDocument.Properties[JsonApiPropertyName.Data];
153-
fullSchemaForDocument.Properties[JsonApiPropertyName.Data] = _nullableReferenceSchemaGenerator.GenerateSchema(referenceSchemaForData);
157+
referenceSchemaForData.Nullable = true;
158+
fullSchemaForDocument.Properties[JsonApiPropertyName.Data] = referenceSchemaForData;
154159
}
155160

156161
private void RemoveJsonApiObject(OpenApiSchema referenceSchemaForDocument)
@@ -160,4 +165,15 @@ private void RemoveJsonApiObject(OpenApiSchema referenceSchemaForDocument)
160165

161166
_schemaRepositoryAccessor.Current.Schemas.Remove("jsonapi-object");
162167
}
168+
169+
private static OpenApiSchema CreateExtendedReferenceSchema(OpenApiSchema referenceSchemaForResourceObject)
170+
{
171+
return new OpenApiSchema
172+
{
173+
AllOf = new List<OpenApiSchema>
174+
{
175+
referenceSchemaForResourceObject
176+
}
177+
};
178+
}
163179
}

src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaGenerator.cs

-103
This file was deleted.

src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaStrategy.cs

-7
This file was deleted.

src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs

+47-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Text.Json;
1+
using System.Reflection;
22
using JsonApiDotNetCore.OpenApi.JsonApiMetadata;
33
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
44
using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
@@ -35,14 +35,14 @@ internal sealed class ResourceFieldObjectSchemaBuilder
3535
private readonly SchemaGenerator _defaultSchemaGenerator;
3636
private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator;
3737
private readonly SchemaRepository _resourceSchemaRepository = new();
38-
private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator;
3938
private readonly IDictionary<string, OpenApiSchema> _schemasForResourceFields;
4039
private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider;
4140
private readonly RelationshipTypeFactory _relationshipTypeFactory;
41+
private readonly NullabilityInfoContext _nullabilityInfoContext = new();
4242
private readonly ResourceObjectDocumentationReader _resourceObjectDocumentationReader;
4343

4444
public ResourceFieldObjectSchemaBuilder(ResourceTypeInfo resourceTypeInfo, ISchemaRepositoryAccessor schemaRepositoryAccessor,
45-
SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, JsonNamingPolicy? namingPolicy,
45+
SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator,
4646
ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider)
4747
{
4848
ArgumentGuard.NotNull(resourceTypeInfo);
@@ -57,7 +57,6 @@ public ResourceFieldObjectSchemaBuilder(ResourceTypeInfo resourceTypeInfo, ISche
5757
_resourceTypeSchemaGenerator = resourceTypeSchemaGenerator;
5858
_resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider;
5959

60-
_nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor, namingPolicy);
6160
_relationshipTypeFactory = new RelationshipTypeFactory(resourceFieldValidationMetadataProvider);
6261
_schemasForResourceFields = GetFieldSchemas();
6362
_resourceObjectDocumentationReader = new ResourceObjectDocumentationReader();
@@ -86,7 +85,15 @@ public void SetMembersOfAttributesObject(OpenApiSchema fullSchemaForAttributesOb
8685

8786
if (matchingAttribute != null && matchingAttribute.Capabilities.HasFlag(requiredCapability))
8887
{
89-
AddAttributeSchemaToResourceObject(matchingAttribute, fullSchemaForAttributesObject, resourceFieldSchema);
88+
bool isPrimitiveOpenApiType = resourceFieldSchema.AllOf.IsNullOrEmpty();
89+
90+
// Types like enum and complex attributes are not primitive and handled as reference schemas.
91+
if (!isPrimitiveOpenApiType)
92+
{
93+
EnsureAttributeSchemaIsExposed(resourceFieldSchema, matchingAttribute);
94+
}
95+
96+
fullSchemaForAttributesObject.Properties[matchingAttribute.PublicName] = resourceFieldSchema;
9097

9198
resourceFieldSchema.Nullable = _resourceFieldValidationMetadataProvider.IsNullable(matchingAttribute);
9299

@@ -107,21 +114,33 @@ private static AttrCapabilities GetRequiredCapabilityForAttributes(Type resource
107114
resourceObjectOpenType == typeof(ResourceObjectInPatchRequest<>) ? AttrCapabilities.AllowChange : throw new UnreachableCodeException();
108115
}
109116

110-
private void AddAttributeSchemaToResourceObject(AttrAttribute attribute, OpenApiSchema attributesObjectSchema, OpenApiSchema resourceAttributeSchema)
117+
private void EnsureAttributeSchemaIsExposed(OpenApiSchema attributeReferenceSchema, AttrAttribute attribute)
111118
{
112-
if (resourceAttributeSchema.Reference != null && !_schemaRepositoryAccessor.Current.TryLookupByType(attribute.Property.PropertyType, out _))
119+
Type nonNullableTypeInPropertyType = GetRepresentedTypeForAttributeSchema(attribute);
120+
121+
if (_schemaRepositoryAccessor.Current.TryLookupByType(nonNullableTypeInPropertyType, out _))
113122
{
114-
ExposeSchema(resourceAttributeSchema.Reference, attribute.Property.PropertyType);
123+
return;
115124
}
116125

117-
attributesObjectSchema.Properties.Add(attribute.PublicName, resourceAttributeSchema);
126+
string schemaId = attributeReferenceSchema.UnwrapExtendedReferenceSchema().Reference.Id;
127+
128+
OpenApiSchema fullSchema = _resourceSchemaRepository.Schemas[schemaId];
129+
_schemaRepositoryAccessor.Current.AddDefinition(schemaId, fullSchema);
130+
_schemaRepositoryAccessor.Current.RegisterType(nonNullableTypeInPropertyType, schemaId);
118131
}
119132

120-
private void ExposeSchema(OpenApiReference openApiReference, Type typeRepresentedBySchema)
133+
private Type GetRepresentedTypeForAttributeSchema(AttrAttribute attribute)
121134
{
122-
OpenApiSchema fullSchema = _resourceSchemaRepository.Schemas[openApiReference.Id];
123-
_schemaRepositoryAccessor.Current.AddDefinition(openApiReference.Id, fullSchema);
124-
_schemaRepositoryAccessor.Current.RegisterType(typeRepresentedBySchema, openApiReference.Id);
135+
NullabilityInfo attributeNullabilityInfo = _nullabilityInfoContext.Create(attribute.Property);
136+
137+
bool isNullable = attributeNullabilityInfo is { ReadState: NullabilityState.Nullable, WriteState: NullabilityState.Nullable };
138+
139+
Type nonNullableTypeInPropertyType = isNullable
140+
? Nullable.GetUnderlyingType(attribute.Property.PropertyType) ?? attribute.Property.PropertyType
141+
: attribute.Property.PropertyType;
142+
143+
return nonNullableTypeInPropertyType;
125144
}
126145

127146
private bool IsFieldRequired(ResourceFieldAttribute field)
@@ -135,18 +154,14 @@ public void SetMembersOfRelationshipsObject(OpenApiSchema fullSchemaForRelations
135154
{
136155
ArgumentGuard.NotNull(fullSchemaForRelationshipsObject);
137156

138-
foreach ((string fieldName, OpenApiSchema resourceFieldSchema) in _schemasForResourceFields)
157+
foreach (string fieldName in _schemasForResourceFields.Keys)
139158
{
140159
RelationshipAttribute? matchingRelationship = _resourceTypeInfo.ResourceType.FindRelationshipByPublicName(fieldName);
141160

142161
if (matchingRelationship != null)
143162
{
144163
EnsureResourceIdentifierObjectSchemaExists(matchingRelationship);
145164
AddRelationshipSchemaToResourceObject(matchingRelationship, fullSchemaForRelationshipsObject);
146-
147-
// This currently has no effect because $ref cannot be combined with other elements in OAS 3.0.
148-
// This can be worked around by using the allOf operator. See https://github.com/OAI/OpenAPI-Specification/issues/1514.
149-
resourceFieldSchema.Description = _resourceObjectDocumentationReader.GetDocumentationForRelationship(matchingRelationship);
150165
}
151166
}
152167
}
@@ -182,9 +197,19 @@ private void AddRelationshipSchemaToResourceObject(RelationshipAttribute relatio
182197
{
183198
Type relationshipSchemaType = GetRelationshipSchemaType(relationship, _resourceTypeInfo.ResourceObjectOpenType);
184199

185-
OpenApiSchema relationshipSchema = GetReferenceSchemaForRelationship(relationshipSchemaType) ?? CreateRelationshipSchema(relationshipSchemaType);
200+
OpenApiSchema referenceSchemaForRelationship =
201+
GetReferenceSchemaForRelationship(relationshipSchemaType) ?? CreateRelationshipReferenceSchema(relationshipSchemaType);
202+
203+
var extendedReferenceSchemaForRelationship = new OpenApiSchema
204+
{
205+
AllOf = new List<OpenApiSchema>
206+
{
207+
referenceSchemaForRelationship
208+
},
209+
Description = _resourceObjectDocumentationReader.GetDocumentationForRelationship(relationship)
210+
};
186211

187-
fullSchemaForRelationshipsObject.Properties.Add(relationship.PublicName, relationshipSchema);
212+
fullSchemaForRelationshipsObject.Properties.Add(relationship.PublicName, extendedReferenceSchemaForRelationship);
188213

189214
if (IsFieldRequired(relationship))
190215
{
@@ -205,15 +230,15 @@ private Type GetRelationshipSchemaType(RelationshipAttribute relationship, Type
205230
return referenceSchema;
206231
}
207232

208-
private OpenApiSchema CreateRelationshipSchema(Type relationshipSchemaType)
233+
private OpenApiSchema CreateRelationshipReferenceSchema(Type relationshipSchemaType)
209234
{
210235
OpenApiSchema referenceSchema = _defaultSchemaGenerator.GenerateSchema(relationshipSchemaType, _schemaRepositoryAccessor.Current);
211236

212237
OpenApiSchema fullSchema = _schemaRepositoryAccessor.Current.Schemas[referenceSchema.Reference.Id];
213238

214239
if (IsDataPropertyNullableInRelationshipSchemaType(relationshipSchemaType))
215240
{
216-
fullSchema.Properties[JsonApiPropertyName.Data] = _nullableReferenceSchemaGenerator.GenerateSchema(fullSchema.Properties[JsonApiPropertyName.Data]);
241+
fullSchema.Properties[JsonApiPropertyName.Data].Nullable = true;
217242
}
218243

219244
if (IsRelationshipInResponseType(relationshipSchemaType))

src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs

+7-3
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IRe
4242
_resourceTypeSchemaGenerator = new ResourceTypeSchemaGenerator(schemaRepositoryAccessor, resourceGraph, options.SerializerOptions.PropertyNamingPolicy);
4343

4444
_resourceFieldObjectSchemaBuilderFactory = resourceTypeInfo => new ResourceFieldObjectSchemaBuilder(resourceTypeInfo, schemaRepositoryAccessor,
45-
defaultSchemaGenerator, _resourceTypeSchemaGenerator, options.SerializerOptions.PropertyNamingPolicy, resourceFieldValidationMetadataProvider);
45+
defaultSchemaGenerator, _resourceTypeSchemaGenerator, resourceFieldValidationMetadataProvider);
4646

4747
_resourceObjectDocumentationReader = new ResourceObjectDocumentationReader();
4848
}
@@ -108,7 +108,9 @@ private void SetResourceType(OpenApiSchema fullSchemaForResourceObject, Resource
108108

109109
private void SetResourceAttributes(OpenApiSchema fullSchemaForResourceObject, ResourceFieldObjectSchemaBuilder builder)
110110
{
111-
OpenApiSchema referenceSchemaForAttributesObject = fullSchemaForResourceObject.Properties[JsonApiPropertyName.Attributes];
111+
OpenApiSchema referenceSchemaForAttributesObject =
112+
fullSchemaForResourceObject.Properties[JsonApiPropertyName.Attributes].UnwrapExtendedReferenceSchema();
113+
112114
OpenApiSchema fullSchemaForAttributesObject = _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForAttributesObject.Reference.Id];
113115

114116
builder.SetMembersOfAttributesObject(fullSchemaForAttributesObject);
@@ -126,7 +128,9 @@ private void SetResourceAttributes(OpenApiSchema fullSchemaForResourceObject, Re
126128

127129
private void SetResourceRelationships(OpenApiSchema fullSchemaForResourceObject, ResourceFieldObjectSchemaBuilder builder)
128130
{
129-
OpenApiSchema referenceSchemaForRelationshipsObject = fullSchemaForResourceObject.Properties[JsonApiPropertyName.Relationships];
131+
OpenApiSchema referenceSchemaForRelationshipsObject =
132+
fullSchemaForResourceObject.Properties[JsonApiPropertyName.Relationships].UnwrapExtendedReferenceSchema();
133+
130134
OpenApiSchema fullSchemaForRelationshipsObject = _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForRelationshipsObject.Reference.Id];
131135

132136
builder.SetMembersOfRelationshipsObject(fullSchemaForRelationshipsObject);

0 commit comments

Comments
 (0)