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

OpenAPI: Use allOf for complex types #1372

Merged
merged 2 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/JsonApiDotNetCore.OpenApi/OpenApiSchemaExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.OpenApi.Models;

namespace JsonApiDotNetCore.OpenApi;

internal static class OpenApiSchemaExtensions
{
public static OpenApiSchema UnwrapExtendedReferenceSchema(this OpenApiSchema source)
{
ArgumentGuard.NotNull(source);

if (source.AllOf.Count != 1)
{
throw new InvalidOperationException($"Schema '{nameof(source)}' should not contain multiple entries in '{nameof(source.AllOf)}' ");
}

return source.AllOf.Single();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ private static void AddSwaggerGenerator(IServiceScope scope, IServiceCollection
SetOperationInfo(swaggerGenOptions, controllerResourceMapping, namingPolicy);
SetSchemaIdSelector(swaggerGenOptions, resourceGraph, namingPolicy);
swaggerGenOptions.DocumentFilter<EndpointOrderingFilter>();
swaggerGenOptions.UseAllOfToExtendReferenceSchemas();
swaggerGenOptions.OperationFilter<JsonApiOperationDocumentationFilter>();

setupSwaggerGenAction?.Invoke(swaggerGenOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ internal sealed class JsonApiSchemaGenerator : ISchemaGenerator
private readonly ISchemaGenerator _defaultSchemaGenerator;
private readonly IJsonApiOptions _options;
private readonly ResourceObjectSchemaGenerator _resourceObjectSchemaGenerator;
private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator;
private readonly SchemaRepositoryAccessor _schemaRepositoryAccessor = new();

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

_defaultSchemaGenerator = defaultSchemaGenerator;
_options = options;
_nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(_schemaRepositoryAccessor, options.SerializerOptions.PropertyNamingPolicy);

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

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

if (IsJsonApiDocument(modelType))
Expand All @@ -92,6 +94,8 @@ public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepos
{
RemoveJsonApiObject(schema);
}

// Schema might depend on other schemas not handled by us, so should not return here.
}

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

OpenApiSchema referenceSchemaForDataObject = IsManyDataDocument(documentType)
? CreateArrayTypeDataSchema(referenceSchemaForResourceObject)
: referenceSchemaForResourceObject;
: CreateExtendedReferenceSchema(referenceSchemaForResourceObject);

fullSchemaForDocument.Properties[JsonApiPropertyName.Data] = referenceSchemaForDataObject;

Expand Down Expand Up @@ -150,7 +154,8 @@ private void SetDataObjectSchemaToNullable(OpenApiSchema referenceSchemaForDocum
{
OpenApiSchema fullSchemaForDocument = _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForDocument.Reference.Id];
OpenApiSchema referenceSchemaForData = fullSchemaForDocument.Properties[JsonApiPropertyName.Data];
fullSchemaForDocument.Properties[JsonApiPropertyName.Data] = _nullableReferenceSchemaGenerator.GenerateSchema(referenceSchemaForData);
referenceSchemaForData.Nullable = true;
fullSchemaForDocument.Properties[JsonApiPropertyName.Data] = referenceSchemaForData;
}

private void RemoveJsonApiObject(OpenApiSchema referenceSchemaForDocument)
Expand All @@ -160,4 +165,15 @@ private void RemoveJsonApiObject(OpenApiSchema referenceSchemaForDocument)

_schemaRepositoryAccessor.Current.Schemas.Remove("jsonapi-object");
}

private static OpenApiSchema CreateExtendedReferenceSchema(OpenApiSchema referenceSchemaForResourceObject)
{
return new OpenApiSchema
{
AllOf = new List<OpenApiSchema>
{
referenceSchemaForResourceObject
}
};
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Text.Json;
using System.Reflection;
using JsonApiDotNetCore.OpenApi.JsonApiMetadata;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
Expand Down Expand Up @@ -35,14 +35,14 @@ internal sealed class ResourceFieldObjectSchemaBuilder
private readonly SchemaGenerator _defaultSchemaGenerator;
private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator;
private readonly SchemaRepository _resourceSchemaRepository = new();
private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator;
private readonly IDictionary<string, OpenApiSchema> _schemasForResourceFields;
private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider;
private readonly RelationshipTypeFactory _relationshipTypeFactory;
private readonly NullabilityInfoContext _nullabilityInfoContext = new();
private readonly ResourceObjectDocumentationReader _resourceObjectDocumentationReader;

public ResourceFieldObjectSchemaBuilder(ResourceTypeInfo resourceTypeInfo, ISchemaRepositoryAccessor schemaRepositoryAccessor,
SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, JsonNamingPolicy? namingPolicy,
SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator,
ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider)
{
ArgumentGuard.NotNull(resourceTypeInfo);
Expand All @@ -57,7 +57,6 @@ public ResourceFieldObjectSchemaBuilder(ResourceTypeInfo resourceTypeInfo, ISche
_resourceTypeSchemaGenerator = resourceTypeSchemaGenerator;
_resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider;

_nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor, namingPolicy);
_relationshipTypeFactory = new RelationshipTypeFactory(resourceFieldValidationMetadataProvider);
_schemasForResourceFields = GetFieldSchemas();
_resourceObjectDocumentationReader = new ResourceObjectDocumentationReader();
Expand Down Expand Up @@ -86,7 +85,15 @@ public void SetMembersOfAttributesObject(OpenApiSchema fullSchemaForAttributesOb

if (matchingAttribute != null && matchingAttribute.Capabilities.HasFlag(requiredCapability))
{
AddAttributeSchemaToResourceObject(matchingAttribute, fullSchemaForAttributesObject, resourceFieldSchema);
bool isPrimitiveOpenApiType = resourceFieldSchema.AllOf.IsNullOrEmpty();

// Types like enum and complex attributes are not primitive and handled as reference schemas.
if (!isPrimitiveOpenApiType)
{
EnsureAttributeSchemaIsExposed(resourceFieldSchema, matchingAttribute);
}

fullSchemaForAttributesObject.Properties[matchingAttribute.PublicName] = resourceFieldSchema;

resourceFieldSchema.Nullable = _resourceFieldValidationMetadataProvider.IsNullable(matchingAttribute);

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

private void AddAttributeSchemaToResourceObject(AttrAttribute attribute, OpenApiSchema attributesObjectSchema, OpenApiSchema resourceAttributeSchema)
private void EnsureAttributeSchemaIsExposed(OpenApiSchema attributeReferenceSchema, AttrAttribute attribute)
{
if (resourceAttributeSchema.Reference != null && !_schemaRepositoryAccessor.Current.TryLookupByType(attribute.Property.PropertyType, out _))
Type nonNullableTypeInPropertyType = GetRepresentedTypeForAttributeSchema(attribute);

if (_schemaRepositoryAccessor.Current.TryLookupByType(nonNullableTypeInPropertyType, out _))
{
ExposeSchema(resourceAttributeSchema.Reference, attribute.Property.PropertyType);
return;
}

attributesObjectSchema.Properties.Add(attribute.PublicName, resourceAttributeSchema);
string schemaId = attributeReferenceSchema.UnwrapExtendedReferenceSchema().Reference.Id;

OpenApiSchema fullSchema = _resourceSchemaRepository.Schemas[schemaId];
_schemaRepositoryAccessor.Current.AddDefinition(schemaId, fullSchema);
_schemaRepositoryAccessor.Current.RegisterType(nonNullableTypeInPropertyType, schemaId);
}

private void ExposeSchema(OpenApiReference openApiReference, Type typeRepresentedBySchema)
private Type GetRepresentedTypeForAttributeSchema(AttrAttribute attribute)
{
OpenApiSchema fullSchema = _resourceSchemaRepository.Schemas[openApiReference.Id];
_schemaRepositoryAccessor.Current.AddDefinition(openApiReference.Id, fullSchema);
_schemaRepositoryAccessor.Current.RegisterType(typeRepresentedBySchema, openApiReference.Id);
NullabilityInfo attributeNullabilityInfo = _nullabilityInfoContext.Create(attribute.Property);

bool isNullable = attributeNullabilityInfo is { ReadState: NullabilityState.Nullable, WriteState: NullabilityState.Nullable };

Type nonNullableTypeInPropertyType = isNullable
? Nullable.GetUnderlyingType(attribute.Property.PropertyType) ?? attribute.Property.PropertyType
: attribute.Property.PropertyType;

return nonNullableTypeInPropertyType;
}

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

foreach ((string fieldName, OpenApiSchema resourceFieldSchema) in _schemasForResourceFields)
foreach (string fieldName in _schemasForResourceFields.Keys)
{
RelationshipAttribute? matchingRelationship = _resourceTypeInfo.ResourceType.FindRelationshipByPublicName(fieldName);

if (matchingRelationship != null)
{
EnsureResourceIdentifierObjectSchemaExists(matchingRelationship);
AddRelationshipSchemaToResourceObject(matchingRelationship, fullSchemaForRelationshipsObject);

// This currently has no effect because $ref cannot be combined with other elements in OAS 3.0.
// This can be worked around by using the allOf operator. See https://github.com/OAI/OpenAPI-Specification/issues/1514.
resourceFieldSchema.Description = _resourceObjectDocumentationReader.GetDocumentationForRelationship(matchingRelationship);
}
}
}
Expand Down Expand Up @@ -182,9 +197,19 @@ private void AddRelationshipSchemaToResourceObject(RelationshipAttribute relatio
{
Type relationshipSchemaType = GetRelationshipSchemaType(relationship, _resourceTypeInfo.ResourceObjectOpenType);

OpenApiSchema relationshipSchema = GetReferenceSchemaForRelationship(relationshipSchemaType) ?? CreateRelationshipSchema(relationshipSchemaType);
OpenApiSchema referenceSchemaForRelationship =
GetReferenceSchemaForRelationship(relationshipSchemaType) ?? CreateRelationshipReferenceSchema(relationshipSchemaType);

var extendedReferenceSchemaForRelationship = new OpenApiSchema
{
AllOf = new List<OpenApiSchema>
{
referenceSchemaForRelationship
},
Description = _resourceObjectDocumentationReader.GetDocumentationForRelationship(relationship)
};

fullSchemaForRelationshipsObject.Properties.Add(relationship.PublicName, relationshipSchema);
fullSchemaForRelationshipsObject.Properties.Add(relationship.PublicName, extendedReferenceSchemaForRelationship);

if (IsFieldRequired(relationship))
{
Expand All @@ -205,15 +230,15 @@ private Type GetRelationshipSchemaType(RelationshipAttribute relationship, Type
return referenceSchema;
}

private OpenApiSchema CreateRelationshipSchema(Type relationshipSchemaType)
private OpenApiSchema CreateRelationshipReferenceSchema(Type relationshipSchemaType)
{
OpenApiSchema referenceSchema = _defaultSchemaGenerator.GenerateSchema(relationshipSchemaType, _schemaRepositoryAccessor.Current);

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

if (IsDataPropertyNullableInRelationshipSchemaType(relationshipSchemaType))
{
fullSchema.Properties[JsonApiPropertyName.Data] = _nullableReferenceSchemaGenerator.GenerateSchema(fullSchema.Properties[JsonApiPropertyName.Data]);
fullSchema.Properties[JsonApiPropertyName.Data].Nullable = true;
}

if (IsRelationshipInResponseType(relationshipSchemaType))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IRe
_resourceTypeSchemaGenerator = new ResourceTypeSchemaGenerator(schemaRepositoryAccessor, resourceGraph, options.SerializerOptions.PropertyNamingPolicy);

_resourceFieldObjectSchemaBuilderFactory = resourceTypeInfo => new ResourceFieldObjectSchemaBuilder(resourceTypeInfo, schemaRepositoryAccessor,
defaultSchemaGenerator, _resourceTypeSchemaGenerator, options.SerializerOptions.PropertyNamingPolicy, resourceFieldValidationMetadataProvider);
defaultSchemaGenerator, _resourceTypeSchemaGenerator, resourceFieldValidationMetadataProvider);

_resourceObjectDocumentationReader = new ResourceObjectDocumentationReader();
}
Expand Down Expand Up @@ -108,7 +108,9 @@ private void SetResourceType(OpenApiSchema fullSchemaForResourceObject, Resource

private void SetResourceAttributes(OpenApiSchema fullSchemaForResourceObject, ResourceFieldObjectSchemaBuilder builder)
{
OpenApiSchema referenceSchemaForAttributesObject = fullSchemaForResourceObject.Properties[JsonApiPropertyName.Attributes];
OpenApiSchema referenceSchemaForAttributesObject =
fullSchemaForResourceObject.Properties[JsonApiPropertyName.Attributes].UnwrapExtendedReferenceSchema();

OpenApiSchema fullSchemaForAttributesObject = _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForAttributesObject.Reference.Id];

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

private void SetResourceRelationships(OpenApiSchema fullSchemaForResourceObject, ResourceFieldObjectSchemaBuilder builder)
{
OpenApiSchema referenceSchemaForRelationshipsObject = fullSchemaForResourceObject.Properties[JsonApiPropertyName.Relationships];
OpenApiSchema referenceSchemaForRelationshipsObject =
fullSchemaForResourceObject.Properties[JsonApiPropertyName.Relationships].UnwrapExtendedReferenceSchema();

OpenApiSchema fullSchemaForRelationshipsObject = _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForRelationshipsObject.Reference.Id];

builder.SetMembersOfRelationshipsObject(fullSchemaForRelationshipsObject);
Expand Down
Loading