Skip to content

Add support for resource inheritance in OpenAPI #1704

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

Merged
merged 4 commits into from
Mar 24, 2025
Merged

Conversation

bkoelman
Copy link
Member

This PR adds full OpenAPI support for resource inheritance in JSON:API.

Foundational changes

Component schema renames

To generate a universal set of schema/type names for JSON:API with and without resource inheritance, this PR applies the following renames. They affect the OpenAPI file and the type names of the generated code.

  • [ResourceName] Primary Response Document -> Primary [ResourceName] Response Document
  • [ResourceName] Secondary Response Document -> Secondary [ResourceName] Response Document
  • Nullable [ResourceName] Secondary Response Document -> Nullable Secondary [ResourceName] Response Document
  • Data In Response -> Resource In Response
  • [ResourceName] Data In Response -> Data In [ResourceName] Response
  • [ResourceName] Attributes In Response -> Attributes In [ResourceName] Response
  • [ResourceName] Relationships In Response -> Relationships In [ResourceName] Response

Hopefully, this is the final set of renames. It affects everyone who uses the pre-release versions; sorry for that.

Structural changes

The component schemas for the following request types now always have a shared abstract base type:

  • resources
  • resource identifiers
  • attributes
  • relationships

The resource type for resource data is now defined in a shared enumeration that lists all resource types, so there are no more resource-specific single-valued enumerations. This hasn't changed for resource identifiers.
Either way, these enumerations matter only to kiota users, who must specify the resource type explicitly. NSwag handles discriminator properties automatically and doesn't even expose them.

Resource inheritance

In short, there's a fundamental mismatch between the concept of inheritance in object-oriented design and composition in OpenAPI. NSwag overcomes that by breaking the inheritance chain in C#, while Kiota doesn't support it at all. See microsoft/kiota#5086 for the details.

To make it work in both tools, this PR introduces a JSON:API extension that allows sending an extra discriminator (which JsonApiDotNetCore ignores) in attributes, relationships and operations. This JSON:API OpenAPI extension is documented in /docs/ext/openapi/index.md. The extension is automatically activated when using the JsonApiDotNetCore.OpenApi.Swashbuckle package. The package hooks into the JsonApiDotNetCore content negotiation and serializer to validate the extra discriminators on incoming JSON:API requests.

Because of the above mismatch, clients must upcast attributes and relationships to access derived properties.

For example:

// server-side model:
// public abstract class Room : Identifiable<long>;
// public sealed class Bedroom : Room
// {
//     [Attr] [Required] public int? BedCount { get; set; }
//     [HasOne] public Residence Residence { get; set; } = null!;
// }
// public sealed class Kitchen : Room;

// client code:
foreach (ResourceInResponse include in response.Included)
{
    if (include is DataInBedroomResponse bedroom)
    {
        if (bedroom.Attributes /* compile-time type: AttributesInRoomResponse */
            is AttributesInBedroomResponse bedroomAttributes)
        {
            int? bedCount = bedroomAttributes.BedCount;
        }

        if (bedroom.Relationships /* compile-time type: RelationshipsInRoomResponse */
            is RelationshipsInBedroomResponse bedroomRelationships)
        {
            string? residenceId = bedroomRelationships.Residence?.Data?.Id;
        }
    }
    else if (include is DataInKitchenResponse kitchen)
    {
        // ...
    }
}

Unfortunately, this is unavoidable.

Implementation details

This PR improves the detection of unused component schemas in the output file. When tests run in DEBUG mode, they fail when unused component schemas appear in the OpenAPI output. It still does not detect types that only appear in discriminator mappings without being referenced elsewhere.

ResourceIdentifierSchemaGenerator and AbstractResourceDataSchemaGenerator got merged into DataSchemaGenerator.
AbstractAtomicOperationSchemaGenerator got merged into AtomicOperationsBodySchemaGenerator, which was renamed to AtomicOperationsDocumentSchemaGenerator.

DataSchemaGenerator, DataContainerSchemaGenerator and AtomicOperationsDocumentSchemaGenerator were heavily refactored to support inheritance.

SetSchemaTypeToObjectDocumentFilter was added to workaround a bug in NSwag where it's unable to infer the proper schema type, resulting in invalid generated code.

To workaround kiota bug microsoft/kiota#2432, the applicable discriminator mappings are repeated in every derived type in a type hierarchy (applies to types used in responses only).

An extensive suite of tests was added for several combinations with resource inheritance:

  • Everything: all endpoints, including operations
  • NoOperations: resource and relationship endpoints only
  • OnlyOperations: only the operations endpoint, with all operations enabled for all resource types
  • OnlyRelationships: only relationship endpoints, including the operations endpoint with relationship operations
  • OnlyAbstract: only endpoints for abstract resource types, include their operations equivalents
  • OnlyConcrete: only endpoints for non-abstract resource types, includes their operations equivalents
  • SubsetOfOperations: only the operations endpoint, with a subset that yields interesting test cases
  • SubsetOfVarious: another subset of endpoints that yields interesting test cases, including equivalent operations

Closes #1457.

QUALITY CHECKLIST

Renames in OAS output:
- [ResourceName] Primary Response Document -> Primary [ResourceName] Response Document
- [ResourceName] Secondary Response Document -> Secondary [ResourceName] Response Document
- Nullable [ResourceName] Secondary Response Document -> Nullable Secondary [ResourceName] Response Document
- Data In Response -> Resource In Response
- [ResourceName] Data In Response -> Data In [ResourceName] Response
- [ResourceName] Attributes In Response -> Attributes In [ResourceName] Response
- [ResourceName] Relationships In Response -> Relationships In [ResourceName] Response
Copy link

codecov bot commented Mar 24, 2025

Codecov Report

Attention: Patch coverage is 90.77615% with 82 lines in your changes missing coverage. Please review.

Project coverage is 90.41%. Comparing base (fcca17d) to head (c9bb868).
Report is 5 commits behind head on openapi.

Files with missing lines Patch % Lines
...cuments/AtomicOperationsDocumentSchemaGenerator.cs 93.06% 7 Missing and 7 partials ⚠️
...nApi.Swashbuckle/OpenApiResourceObjectConverter.cs 73.68% 5 Missing and 5 partials ⚠️
...SchemaGenerators/Components/DataSchemaGenerator.cs 96.59% 4 Missing and 4 partials ⚠️
...penApi.Swashbuckle/UnusedComponentSchemaCleaner.cs 86.20% 5 Missing and 3 partials ⚠️
...JsonApiObjects/AtomicOperations/UpdateOperation.cs 0.00% 2 Missing ⚠️
...ApiObjects/Documents/CollectionResponseDocument.cs 0.00% 2 Missing ⚠️
...cts/Documents/NullableSecondaryResponseDocument.cs 0.00% 2 Missing ⚠️
...sonApiObjects/Documents/PrimaryResponseDocument.cs 0.00% 2 Missing ⚠️
...nApiObjects/Documents/SecondaryResponseDocument.cs 0.00% 2 Missing ⚠️
...nApiObjects/ResourceObjects/DataInCreateRequest.cs 0.00% 2 Missing ⚠️
... and 26 more
Additional details and impacted files
@@             Coverage Diff             @@
##           openapi    #1704      +/-   ##
===========================================
+ Coverage    90.34%   90.41%   +0.07%     
===========================================
  Files          462      466       +4     
  Lines        14080    14434     +354     
  Branches      2198     2283      +85     
===========================================
+ Hits         12720    13050     +330     
- Misses         921      933      +12     
- Partials       439      451      +12     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bkoelman bkoelman force-pushed the openapi-inheritance branch from 3b2eb6b to c9bb868 Compare March 24, 2025 00:27
@bkoelman bkoelman marked this pull request as ready for review March 24, 2025 00:59
@bkoelman bkoelman merged commit fb78af3 into openapi Mar 24, 2025
16 checks passed
@bkoelman bkoelman deleted the openapi-inheritance branch March 24, 2025 00:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

1 participant