Skip to content

Commit

Permalink
Switch to built-in support for nullable reference schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
maurei authored and bkoelman committed Oct 27, 2023
1 parent 088fde5 commit 9d82fcd
Show file tree
Hide file tree
Showing 24 changed files with 3,629 additions and 1,300 deletions.
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 Down Expand Up @@ -182,9 +201,18 @@ 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
}
};

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

if (IsFieldRequired(relationship))
{
Expand All @@ -205,15 +233,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

0 comments on commit 9d82fcd

Please sign in to comment.