diff --git a/docs/usage/openapi-client.md b/docs/usage/openapi-client.md index 0ba44d20d6..bb88381e79 100644 --- a/docs/usage/openapi-client.md +++ b/docs/usage/openapi-client.md @@ -1,8 +1,12 @@ # OpenAPI clients -After [enabling OpenAPI](~/usage/openapi.md), you can generate a JSON:API client for your API in various programming languages. +After [enabling OpenAPI](~/usage/openapi.md), you can generate a typed JSON:API client for your API in various programming languages. -The following generators are supported, though you may try others as well: +> [!NOTE] +> If you prefer a generic JSON:API client instead of a typed one, choose from the existing +> [client libraries](https://jsonapi.org/implementations/#client-libraries). + +The following code generators are supported, though you may try others as well: - [NSwag](https://github.com/RicoSuter/NSwag): Produces clients for C# and TypeScript - [Kiota](https://learn.microsoft.com/en-us/openapi/kiota/overview): Produces clients for C#, Go, Java, PHP, Python, Ruby, Swift and TypeScript @@ -51,7 +55,8 @@ The following steps describe how to generate and use a JSON:API client in C#, us 3. Although not strictly required, we recommend running package update now, which fixes some issues. > [!WARNING] - > NSwag v14 is currently *incompatible* with JsonApiDotNetCore (tracked [here](https://github.com/RicoSuter/NSwag/issues/4662)). Stick with v13.x for the moment. + > NSwag v14 is currently *incompatible* with JsonApiDotNetCore (tracked [here](https://github.com/RicoSuter/NSwag/issues/4662)). + > Stick with v13.x for the moment. 4. Add our client package to your project: @@ -141,8 +146,11 @@ The following steps describe how to generate and use a JSON:API client in C#, us ``` > [!TIP] -> The [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/openapi/src/Examples/OpenApiNSwagClientExample) contains an enhanced version that uses `IHttpClientFactory` for [scalability](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) and [resiliency](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests#use-polly-based-handlers) and logs the HTTP requests and responses. -> Additionally, the example shows how to write the swagger.json file to disk when building the server, which is imported from the client project. This keeps the server and client automatically in sync, which is handy when both are in the same solution. +> The [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/openapi/src/Examples/OpenApiNSwagClientExample) contains an enhanced version +> that uses `IHttpClientFactory` for [scalability](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) and +> [resiliency](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests#use-polly-based-handlers) and logs the HTTP requests and responses. +> Additionally, the example shows how to write the swagger.json file to disk when building the server, which is imported from the client project. +> This keeps the server and client automatically in sync, which is handy when both are in the same solution. ### Other IDEs @@ -215,7 +223,8 @@ Likewise, you can enable nullable reference types by adding `/GenerateNullableRe The available command-line switches for Kiota are described [here](https://learn.microsoft.com/en-us/openapi/kiota/using#client-generation). At the time of writing, Kiota provides [no official integration](https://github.com/microsoft/kiota/issues/3005) with MSBuild. -Our [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/openapi/src/Examples/OpenApiKiotaClientExample) takes a stab at it, although it has glitches. If you're an MSBuild expert, please help out! +Our [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/openapi/src/Examples/OpenApiKiotaClientExample) takes a stab at it, +although it has glitches. If you're an MSBuild expert, please help out! ```xml diff --git a/docs/usage/openapi-documentation.md b/docs/usage/openapi-documentation.md new file mode 100644 index 0000000000..2e0af9340d --- /dev/null +++ b/docs/usage/openapi-documentation.md @@ -0,0 +1,20 @@ +# OpenAPI documentation + +After [enabling OpenAPI](~/usage/openapi.md), you can expose a documentation website with SwaggerUI or Redoc. + +### SwaggerUI + +Swashbuckle ships with [SwaggerUI](https://swagger.io/tools/swagger-ui/), which enables to visualize and interact with the JSON:API endpoints through a web page. +This can be enabled by installing the `Swashbuckle.AspNetCore.SwaggerUI` NuGet package and adding the following to your `Program.cs` file: + +```c# +app.UseSwaggerUI(); +``` + +By default, SwaggerUI will be available at `http://localhost:/swagger`. + +### Redoc + +[Redoc](https://github.com/Redocly/redoc) is another popular tool that generates a documentation website from an OpenAPI document. +It lists the endpoints and their schemas, but doesn't provide the ability to execute requests. +The `Swashbuckle.AspNetCore.ReDoc` NuGet package provides integration with Swashbuckle. diff --git a/docs/usage/openapi.md b/docs/usage/openapi.md index e26754d99b..43d32d8c8e 100644 --- a/docs/usage/openapi.md +++ b/docs/usage/openapi.md @@ -1,9 +1,11 @@ # OpenAPI -JsonApiDotNetCore provides an extension package that enables you to produce an [OpenAPI specification](https://swagger.io/specification/) for your JSON:API endpoints. -This can be used to generate a [documentation website](https://swagger.io/tools/swagger-ui/) or to generate [client libraries](https://openapi-generator.tech/docs/generators/) in various languages. -The package provides an integration with [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore). +Exposing an [OpenAPI document](https://swagger.io/specification/) for your JSON:API endpoints enables to provide a +[documentation website](https://swagger.io/tools/swagger-ui/) and to generate typed +[client libraries](https://openapi-generator.tech/docs/generators/) in various languages. +The [JsonApiDotNetCore.OpenApi](https://github.com/json-api-dotnet/JsonApiDotNetCore/pkgs/nuget/JsonApiDotNetCore.OpenApi) NuGet package +provides OpenAPI support for JSON:API by integrating with [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore). ## Getting started @@ -13,7 +15,11 @@ The package provides an integration with [Swashbuckle](https://github.com/domain dotnet add package JsonApiDotNetCore.OpenApi ``` -2. Add the integration in your `Program.cs` file. + > [!NOTE] + > Because this package is still experimental, it's not yet available on NuGet. + > Use the steps [here](https://github.com/json-api-dotnet/JsonApiDotNetCore?tab=readme-ov-file#trying-out-the-latest-build) to install. + +2. Add the JSON:API support to your `Program.cs` file. ```c# builder.Services.AddJsonApi(); @@ -30,22 +36,37 @@ The package provides an integration with [Swashbuckle](https://github.com/domain app.UseSwagger(); ``` -By default, the OpenAPI specification will be available at `http://localhost:/swagger/v1/swagger.json`. +By default, the OpenAPI document will be available at `http://localhost:/swagger/v1/swagger.json`. + +### Customizing the Route Template -## Documentation +Because Swashbuckle doesn't properly implement the ASP.NET Options pattern, you must *not* use its +[documented way](https://github.com/domaindrivendev/Swashbuckle.AspNetCore?tab=readme-ov-file#change-the-path-for-swagger-json-endpoints) +to change the route template: -### SwaggerUI +```c# +// DO NOT USE THIS! INCOMPATIBLE WITH JSON:API! +app.UseSwagger(options => options.RouteTemplate = "api-docs/{documentName}/swagger.yaml"); +``` -Swashbuckle also ships with [SwaggerUI](https://swagger.io/tools/swagger-ui/), which enables to visualize and interact with the API endpoints through a web page. -This can be enabled by installing the `Swashbuckle.AspNetCore.SwaggerUI` NuGet package and adding the following to your `Program.cs` file: +Instead, always call `UseSwagger()` *without parameters*. To change the route template, use the code below: ```c# -app.UseSwaggerUI(); +builder.Services.Configure(options => options.RouteTemplate = "api-docs/{documentName}/swagger.yaml"); ``` -By default, SwaggerUI will be available at `http://localhost:/swagger`. +If you want to inject dependencies to set the route template, use: + +```c# +builder.Services.AddOptions().Configure((options, serviceProvider) => +{ + var webHostEnvironment = serviceProvider.GetRequiredService(); + string appName = webHostEnvironment.ApplicationName; + options.RouteTemplate = $"api-docs/{{documentName}}/{appName}-swagger.yaml"; +}); +``` -### Triple-slash comments +## Triple-slash comments Documentation for JSON:API endpoints is provided out of the box, which shows in SwaggerUI and through IDE IntelliSense in auto-generated clients. To also get documentation for your resource classes and their properties, add the following to your project file. @@ -58,5 +79,6 @@ The `NoWarn` line is optional, which suppresses build warnings for undocumented ``` -You can combine this with the documentation that Swagger itself supports, by enabling it as described [here](https://github.com/domaindrivendev/Swashbuckle.AspNetCore#include-descriptions-from-xml-comments). +You can combine this with the documentation that Swagger itself supports, by enabling it as described +[here](https://github.com/domaindrivendev/Swashbuckle.AspNetCore#include-descriptions-from-xml-comments). This adds documentation for additional types, such as triple-slash comments on enums used in your resource models. diff --git a/docs/usage/toc.md b/docs/usage/toc.md index 925aa660a2..bdeb0e4958 100644 --- a/docs/usage/toc.md +++ b/docs/usage/toc.md @@ -26,6 +26,7 @@ # [Common Pitfalls](common-pitfalls.md) # [OpenAPI](openapi.md) +## [Documentation](openapi-documentation.md) ## [Clients](openapi-client.md) # Extensibility diff --git a/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json b/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json index 36dfff6ef6..863e9cd5a9 100644 --- a/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json +++ b/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json @@ -4781,15 +4781,30 @@ }, "errorResponseDocument": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInErrorDocument" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/errorObject" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -4812,6 +4827,22 @@ }, "additionalProperties": false }, + "linksInErrorDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "linksInRelationship": { "required": [ "related", diff --git a/src/Examples/OpenApiKiotaClientExample/GeneratedCode/Models/ErrorResponseDocument.cs b/src/Examples/OpenApiKiotaClientExample/GeneratedCode/Models/ErrorResponseDocument.cs index 32e5a9ff73..2a4711b7e3 100644 --- a/src/Examples/OpenApiKiotaClientExample/GeneratedCode/Models/ErrorResponseDocument.cs +++ b/src/Examples/OpenApiKiotaClientExample/GeneratedCode/Models/ErrorResponseDocument.cs @@ -23,9 +23,37 @@ public List Errors { get { return BackingStore?.Get>("errors"); } set { BackingStore?.Set("errors", value); } } +#endif + /// The links property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public LinksInErrorDocument? Links { + get { return BackingStore?.Get("links"); } + set { BackingStore?.Set("links", value); } + } +#nullable restore +#else + public LinksInErrorDocument Links { + get { return BackingStore?.Get("links"); } + set { BackingStore?.Set("links", value); } + } #endif /// The primary error message. public override string Message { get => base.Message; } + /// The meta property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public ErrorResponseDocument_meta? Meta { + get { return BackingStore?.Get("meta"); } + set { BackingStore?.Set("meta", value); } + } +#nullable restore +#else + public ErrorResponseDocument_meta Meta { + get { return BackingStore?.Get("meta"); } + set { BackingStore?.Set("meta", value); } + } +#endif /// /// Instantiates a new errorResponseDocument and sets the default values. /// @@ -46,6 +74,8 @@ public static ErrorResponseDocument CreateFromDiscriminatorValue(IParseNode pars public virtual IDictionary> GetFieldDeserializers() { return new Dictionary> { {"errors", n => { Errors = n.GetCollectionOfObjectValues(ErrorObject.CreateFromDiscriminatorValue)?.ToList(); } }, + {"links", n => { Links = n.GetObjectValue(LinksInErrorDocument.CreateFromDiscriminatorValue); } }, + {"meta", n => { Meta = n.GetObjectValue(ErrorResponseDocument_meta.CreateFromDiscriminatorValue); } }, }; } /// @@ -55,6 +85,8 @@ public virtual IDictionary> GetFieldDeserializers() { public virtual void Serialize(ISerializationWriter writer) { _ = writer ?? throw new ArgumentNullException(nameof(writer)); writer.WriteCollectionOfObjectValues("errors", Errors); + writer.WriteObjectValue("links", Links); + writer.WriteObjectValue("meta", Meta); } } } diff --git a/src/Examples/OpenApiKiotaClientExample/GeneratedCode/Models/ErrorResponseDocument_meta.cs b/src/Examples/OpenApiKiotaClientExample/GeneratedCode/Models/ErrorResponseDocument_meta.cs new file mode 100644 index 0000000000..000b92e69f --- /dev/null +++ b/src/Examples/OpenApiKiotaClientExample/GeneratedCode/Models/ErrorResponseDocument_meta.cs @@ -0,0 +1,48 @@ +// +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions.Store; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System; +namespace OpenApiKiotaClientExample.GeneratedCode.Models { + public class ErrorResponseDocument_meta : IAdditionalDataHolder, IBackedModel, IParsable { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { + get { return BackingStore?.Get>("AdditionalData"); } + set { BackingStore?.Set("AdditionalData", value); } + } + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + /// + /// Instantiates a new errorResponseDocument_meta and sets the default values. + /// + public ErrorResponseDocument_meta() { + BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore(); + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// The parse node to use to read the discriminator value and create the object + public static ErrorResponseDocument_meta CreateFromDiscriminatorValue(IParseNode parseNode) { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new ErrorResponseDocument_meta(); + } + /// + /// The deserialization information for the current model + /// + public virtual IDictionary> GetFieldDeserializers() { + return new Dictionary> { + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteAdditionalData(AdditionalData); + } + } +} diff --git a/src/Examples/OpenApiKiotaClientExample/GeneratedCode/Models/LinksInErrorDocument.cs b/src/Examples/OpenApiKiotaClientExample/GeneratedCode/Models/LinksInErrorDocument.cs new file mode 100644 index 0000000000..d9fba50feb --- /dev/null +++ b/src/Examples/OpenApiKiotaClientExample/GeneratedCode/Models/LinksInErrorDocument.cs @@ -0,0 +1,73 @@ +// +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions.Store; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System; +namespace OpenApiKiotaClientExample.GeneratedCode.Models { + public class LinksInErrorDocument : IBackedModel, IParsable { + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + /// The describedby property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Describedby { + get { return BackingStore?.Get("describedby"); } + set { BackingStore?.Set("describedby", value); } + } +#nullable restore +#else + public string Describedby { + get { return BackingStore?.Get("describedby"); } + set { BackingStore?.Set("describedby", value); } + } +#endif + /// The self property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Self { + get { return BackingStore?.Get("self"); } + set { BackingStore?.Set("self", value); } + } +#nullable restore +#else + public string Self { + get { return BackingStore?.Get("self"); } + set { BackingStore?.Set("self", value); } + } +#endif + /// + /// Instantiates a new linksInErrorDocument and sets the default values. + /// + public LinksInErrorDocument() { + BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// The parse node to use to read the discriminator value and create the object + public static LinksInErrorDocument CreateFromDiscriminatorValue(IParseNode parseNode) { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new LinksInErrorDocument(); + } + /// + /// The deserialization information for the current model + /// + public virtual IDictionary> GetFieldDeserializers() { + return new Dictionary> { + {"describedby", n => { Describedby = n.GetStringValue(); } }, + {"self", n => { Self = n.GetStringValue(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("describedby", Describedby); + writer.WriteStringValue("self", Self); + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ErrorResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ErrorResponseDocument.cs index 4804d95c25..c4b79fa9f7 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ErrorResponseDocument.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ErrorResponseDocument.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using JetBrains.Annotations; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.Links; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; @@ -8,7 +9,17 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; [UsedImplicitly(ImplicitUseTargetFlags.Members)] internal sealed class ErrorResponseDocument { + [JsonPropertyName("jsonapi")] + public Jsonapi Jsonapi { get; set; } = null!; + + [Required] + [JsonPropertyName("links")] + public LinksInErrorDocument Links { get; set; } = null!; + [Required] [JsonPropertyName("errors")] public IList Errors { get; set; } = new List(); + + [JsonPropertyName("meta")] + public IDictionary Meta { get; set; } = null!; } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInErrorDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInErrorDocument.cs new file mode 100644 index 0000000000..2d3c075050 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInErrorDocument.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Links; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +internal sealed class LinksInErrorDocument +{ + [Required] + [JsonPropertyName("self")] + public string Self { get; set; } = null!; + + [JsonPropertyName("describedby")] + public string Describedby { get; set; } = null!; +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs index 97aed20050..011696cc51 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs @@ -36,7 +36,8 @@ public IReadOnlyList GetSupportedContentTypes(string contentType, Type o ArgumentGuard.NotNullNorEmpty(contentType); ArgumentGuard.NotNull(objectType); - if (contentType == HeaderConstants.MediaType && objectType.IsGenericType && JsonApiRequestOpenTypes.Contains(objectType.GetGenericTypeDefinition())) + if (contentType == HeaderConstants.MediaType && objectType.IsConstructedGenericType && + JsonApiRequestOpenTypes.Contains(objectType.GetGenericTypeDefinition())) { return new MediaTypeCollection { diff --git a/src/JsonApiDotNetCore.OpenApi/OpenApiDescriptionLinkProvider.cs b/src/JsonApiDotNetCore.OpenApi/OpenApiDescriptionLinkProvider.cs new file mode 100644 index 0000000000..cad1abb17b --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/OpenApiDescriptionLinkProvider.cs @@ -0,0 +1,41 @@ +using JsonApiDotNetCore.Serialization.Response; +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.Swagger; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCore.OpenApi; + +/// +/// Provides the OpenAPI URL for the "describedby" link in https://jsonapi.org/format/#document-top-level. +/// +internal sealed class OpenApiDescriptionLinkProvider : IDocumentDescriptionLinkProvider +{ + private readonly IOptionsMonitor _swaggerGeneratorOptionsMonitor; + private readonly IOptionsMonitor _swaggerOptionsMonitor; + + public OpenApiDescriptionLinkProvider(IOptionsMonitor swaggerGeneratorOptionsMonitor, + IOptionsMonitor swaggerOptionsMonitor) + { + ArgumentGuard.NotNull(swaggerGeneratorOptionsMonitor); + ArgumentGuard.NotNull(swaggerOptionsMonitor); + + _swaggerGeneratorOptionsMonitor = swaggerGeneratorOptionsMonitor; + _swaggerOptionsMonitor = swaggerOptionsMonitor; + } + + /// + public string? GetUrl() + { + SwaggerGeneratorOptions swaggerGeneratorOptions = _swaggerGeneratorOptionsMonitor.CurrentValue; + + if (swaggerGeneratorOptions.SwaggerDocs.Count > 0) + { + string latestVersionDocumentName = swaggerGeneratorOptions.SwaggerDocs.Last().Key; + + SwaggerOptions swaggerOptions = _swaggerOptionsMonitor.CurrentValue; + return swaggerOptions.RouteTemplate.Replace("{documentName}", latestVersionDocumentName).Replace("{json|yaml}", "json"); + } + + return null; + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs index ad976b6fc7..a605e1daba 100644 --- a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using JsonApiDotNetCore.OpenApi.JsonApiMetadata; using JsonApiDotNetCore.OpenApi.SwaggerComponents; +using JsonApiDotNetCore.Serialization.Response; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.DependencyInjection; @@ -75,6 +76,7 @@ private static void AddSwaggerGenerator(IServiceCollection services) AddSchemaGenerators(services); services.TryAddSingleton(); + services.AddSingleton(); services.AddSwaggerGen(); services.AddSingleton, ConfigureSwaggerGenOptions>(); diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs index dc3c9e463e..5df0994903 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs @@ -59,13 +59,11 @@ public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepos ArgumentGuard.NotNull(modelType); ArgumentGuard.NotNull(schemaRepository); - OpenApiSchema referenceSchemaForDocument = GenerateJsonApiDocumentSchema(modelType, schemaRepository); - OpenApiSchema fullSchemaForDocument = schemaRepository.Schemas[referenceSchemaForDocument.Reference.Id]; + OpenApiSchema referenceSchemaForDocument = modelType.IsConstructedGenericType + ? GenerateJsonApiDocumentSchema(modelType, schemaRepository) + : _defaultSchemaGenerator.GenerateSchema(modelType, schemaRepository); - if (IsDataPropertyNullableInDocument(modelType)) - { - SetDataSchemaToNullable(fullSchemaForDocument); - } + OpenApiSchema fullSchemaForDocument = schemaRepository.Schemas[referenceSchemaForDocument.Reference.Id]; fullSchemaForDocument.SetValuesInMetaToNullable(); @@ -97,6 +95,11 @@ private OpenApiSchema GenerateJsonApiDocumentSchema(Type documentType, SchemaRep ? CreateArrayTypeDataSchema(referenceSchemaForResourceData) : CreateExtendedReferenceSchema(referenceSchemaForResourceData); + if (IsDataPropertyNullableInDocument(documentType)) + { + SetDataSchemaToNullable(fullSchemaForDocument); + } + fullSchemaForDocument.ReorderProperties(DocumentPropertyNamesInOrder); return referenceSchemaForDocument; diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs index 9811fa783f..d935a63e78 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -10,7 +10,7 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents; internal sealed class JsonApiSchemaGenerator : ISchemaGenerator { - private static readonly Type[] JsonApiDocumentOpenTypes = + private static readonly Type[] JsonApiDocumentTypes = [ typeof(ResourceCollectionResponseDocument<>), typeof(PrimaryResourceResponseDocument<>), @@ -20,7 +20,8 @@ internal sealed class JsonApiSchemaGenerator : ISchemaGenerator typeof(ResourcePatchRequestDocument<>), typeof(ResourceIdentifierCollectionResponseDocument<>), typeof(ResourceIdentifierResponseDocument<>), - typeof(NullableResourceIdentifierResponseDocument<>) + typeof(NullableResourceIdentifierResponseDocument<>), + typeof(ErrorResponseDocument) ]; private static readonly OpenApiSchema IdTypeSchema = new() @@ -77,6 +78,7 @@ private static bool IsJsonApiParameter(ParameterInfo parameter) private static bool IsJsonApiDocument(Type type) { - return type.IsConstructedGenericType && JsonApiDocumentOpenTypes.Contains(type.GetGenericTypeDefinition()); + Type documentType = type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type; + return JsonApiDocumentTypes.Contains(documentType); } } diff --git a/test/OpenApiKiotaEndToEndTests/Headers/GeneratedCode/Models/ErrorResponseDocument.cs b/test/OpenApiKiotaEndToEndTests/Headers/GeneratedCode/Models/ErrorResponseDocument.cs index d5b954dcb1..75b4e5a6bd 100644 --- a/test/OpenApiKiotaEndToEndTests/Headers/GeneratedCode/Models/ErrorResponseDocument.cs +++ b/test/OpenApiKiotaEndToEndTests/Headers/GeneratedCode/Models/ErrorResponseDocument.cs @@ -23,9 +23,37 @@ public List Errors { get { return BackingStore?.Get>("errors"); } set { BackingStore?.Set("errors", value); } } +#endif + /// The links property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public LinksInErrorDocument? Links { + get { return BackingStore?.Get("links"); } + set { BackingStore?.Set("links", value); } + } +#nullable restore +#else + public LinksInErrorDocument Links { + get { return BackingStore?.Get("links"); } + set { BackingStore?.Set("links", value); } + } #endif /// The primary error message. public override string Message { get => base.Message; } + /// The meta property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public ErrorResponseDocument_meta? Meta { + get { return BackingStore?.Get("meta"); } + set { BackingStore?.Set("meta", value); } + } +#nullable restore +#else + public ErrorResponseDocument_meta Meta { + get { return BackingStore?.Get("meta"); } + set { BackingStore?.Set("meta", value); } + } +#endif /// /// Instantiates a new errorResponseDocument and sets the default values. /// @@ -46,6 +74,8 @@ public static ErrorResponseDocument CreateFromDiscriminatorValue(IParseNode pars public virtual IDictionary> GetFieldDeserializers() { return new Dictionary> { {"errors", n => { Errors = n.GetCollectionOfObjectValues(ErrorObject.CreateFromDiscriminatorValue)?.ToList(); } }, + {"links", n => { Links = n.GetObjectValue(LinksInErrorDocument.CreateFromDiscriminatorValue); } }, + {"meta", n => { Meta = n.GetObjectValue(ErrorResponseDocument_meta.CreateFromDiscriminatorValue); } }, }; } /// @@ -55,6 +85,8 @@ public virtual IDictionary> GetFieldDeserializers() { public virtual void Serialize(ISerializationWriter writer) { _ = writer ?? throw new ArgumentNullException(nameof(writer)); writer.WriteCollectionOfObjectValues("errors", Errors); + writer.WriteObjectValue("links", Links); + writer.WriteObjectValue("meta", Meta); } } } diff --git a/test/OpenApiKiotaEndToEndTests/Headers/GeneratedCode/Models/ErrorResponseDocument_meta.cs b/test/OpenApiKiotaEndToEndTests/Headers/GeneratedCode/Models/ErrorResponseDocument_meta.cs new file mode 100644 index 0000000000..f46c8476af --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/Headers/GeneratedCode/Models/ErrorResponseDocument_meta.cs @@ -0,0 +1,48 @@ +// +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions.Store; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System; +namespace OpenApiKiotaEndToEndTests.Headers.GeneratedCode.Models { + public class ErrorResponseDocument_meta : IAdditionalDataHolder, IBackedModel, IParsable { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { + get { return BackingStore?.Get>("AdditionalData"); } + set { BackingStore?.Set("AdditionalData", value); } + } + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + /// + /// Instantiates a new errorResponseDocument_meta and sets the default values. + /// + public ErrorResponseDocument_meta() { + BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore(); + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// The parse node to use to read the discriminator value and create the object + public static ErrorResponseDocument_meta CreateFromDiscriminatorValue(IParseNode parseNode) { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new ErrorResponseDocument_meta(); + } + /// + /// The deserialization information for the current model + /// + public virtual IDictionary> GetFieldDeserializers() { + return new Dictionary> { + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteAdditionalData(AdditionalData); + } + } +} diff --git a/test/OpenApiKiotaEndToEndTests/Headers/GeneratedCode/Models/LinksInErrorDocument.cs b/test/OpenApiKiotaEndToEndTests/Headers/GeneratedCode/Models/LinksInErrorDocument.cs new file mode 100644 index 0000000000..46416586bd --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/Headers/GeneratedCode/Models/LinksInErrorDocument.cs @@ -0,0 +1,73 @@ +// +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions.Store; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System; +namespace OpenApiKiotaEndToEndTests.Headers.GeneratedCode.Models { + public class LinksInErrorDocument : IBackedModel, IParsable { + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + /// The describedby property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Describedby { + get { return BackingStore?.Get("describedby"); } + set { BackingStore?.Set("describedby", value); } + } +#nullable restore +#else + public string Describedby { + get { return BackingStore?.Get("describedby"); } + set { BackingStore?.Set("describedby", value); } + } +#endif + /// The self property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Self { + get { return BackingStore?.Get("self"); } + set { BackingStore?.Set("self", value); } + } +#nullable restore +#else + public string Self { + get { return BackingStore?.Get("self"); } + set { BackingStore?.Set("self", value); } + } +#endif + /// + /// Instantiates a new linksInErrorDocument and sets the default values. + /// + public LinksInErrorDocument() { + BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// The parse node to use to read the discriminator value and create the object + public static LinksInErrorDocument CreateFromDiscriminatorValue(IParseNode parseNode) { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new LinksInErrorDocument(); + } + /// + /// The deserialization information for the current model + /// + public virtual IDictionary> GetFieldDeserializers() { + return new Dictionary> { + {"describedby", n => { Describedby = n.GetStringValue(); } }, + {"self", n => { Self = n.GetStringValue(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("describedby", Describedby); + writer.WriteStringValue("self", Self); + } + } +} diff --git a/test/OpenApiKiotaEndToEndTests/QueryStrings/FilterTests.cs b/test/OpenApiKiotaEndToEndTests/QueryStrings/FilterTests.cs index 8ad34de5c7..89485e11a3 100644 --- a/test/OpenApiKiotaEndToEndTests/QueryStrings/FilterTests.cs +++ b/test/OpenApiKiotaEndToEndTests/QueryStrings/FilterTests.cs @@ -139,6 +139,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => response.Data.ElementAt(0).Id.Should().Be(node.Children.ElementAt(1).StringId); response.Meta.ShouldNotBeNull(); response.Meta.AdditionalData.ShouldContainKey("total").With(total => total.Should().Be(1)); + response.Links.ShouldNotBeNull(); + response.Links.Describedby.Should().Be("swagger/v1/swagger.json"); } } @@ -162,6 +164,8 @@ public async Task Cannot_use_empty_filter() // Assert ErrorResponseDocument exception = (await action.Should().ThrowExactlyAsync()).Which; exception.ResponseStatusCode.Should().Be((int)HttpStatusCode.BadRequest); + exception.Links.ShouldNotBeNull(); + exception.Links.Describedby.Should().Be("swagger/v1/swagger.json"); exception.Errors.ShouldHaveCount(1); ErrorObject error = exception.Errors[0]; diff --git a/test/OpenApiKiotaEndToEndTests/QueryStrings/GeneratedCode/Models/ErrorResponseDocument.cs b/test/OpenApiKiotaEndToEndTests/QueryStrings/GeneratedCode/Models/ErrorResponseDocument.cs index 277dded362..b5cd8d4c03 100644 --- a/test/OpenApiKiotaEndToEndTests/QueryStrings/GeneratedCode/Models/ErrorResponseDocument.cs +++ b/test/OpenApiKiotaEndToEndTests/QueryStrings/GeneratedCode/Models/ErrorResponseDocument.cs @@ -23,9 +23,37 @@ public List Errors { get { return BackingStore?.Get>("errors"); } set { BackingStore?.Set("errors", value); } } +#endif + /// The links property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public LinksInErrorDocument? Links { + get { return BackingStore?.Get("links"); } + set { BackingStore?.Set("links", value); } + } +#nullable restore +#else + public LinksInErrorDocument Links { + get { return BackingStore?.Get("links"); } + set { BackingStore?.Set("links", value); } + } #endif /// The primary error message. public override string Message { get => base.Message; } + /// The meta property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public ErrorResponseDocument_meta? Meta { + get { return BackingStore?.Get("meta"); } + set { BackingStore?.Set("meta", value); } + } +#nullable restore +#else + public ErrorResponseDocument_meta Meta { + get { return BackingStore?.Get("meta"); } + set { BackingStore?.Set("meta", value); } + } +#endif /// /// Instantiates a new errorResponseDocument and sets the default values. /// @@ -46,6 +74,8 @@ public static ErrorResponseDocument CreateFromDiscriminatorValue(IParseNode pars public virtual IDictionary> GetFieldDeserializers() { return new Dictionary> { {"errors", n => { Errors = n.GetCollectionOfObjectValues(ErrorObject.CreateFromDiscriminatorValue)?.ToList(); } }, + {"links", n => { Links = n.GetObjectValue(LinksInErrorDocument.CreateFromDiscriminatorValue); } }, + {"meta", n => { Meta = n.GetObjectValue(ErrorResponseDocument_meta.CreateFromDiscriminatorValue); } }, }; } /// @@ -55,6 +85,8 @@ public virtual IDictionary> GetFieldDeserializers() { public virtual void Serialize(ISerializationWriter writer) { _ = writer ?? throw new ArgumentNullException(nameof(writer)); writer.WriteCollectionOfObjectValues("errors", Errors); + writer.WriteObjectValue("links", Links); + writer.WriteObjectValue("meta", Meta); } } } diff --git a/test/OpenApiKiotaEndToEndTests/QueryStrings/GeneratedCode/Models/ErrorResponseDocument_meta.cs b/test/OpenApiKiotaEndToEndTests/QueryStrings/GeneratedCode/Models/ErrorResponseDocument_meta.cs new file mode 100644 index 0000000000..55c333691a --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/QueryStrings/GeneratedCode/Models/ErrorResponseDocument_meta.cs @@ -0,0 +1,48 @@ +// +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions.Store; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System; +namespace OpenApiKiotaEndToEndTests.QueryStrings.GeneratedCode.Models { + public class ErrorResponseDocument_meta : IAdditionalDataHolder, IBackedModel, IParsable { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { + get { return BackingStore?.Get>("AdditionalData"); } + set { BackingStore?.Set("AdditionalData", value); } + } + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + /// + /// Instantiates a new errorResponseDocument_meta and sets the default values. + /// + public ErrorResponseDocument_meta() { + BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore(); + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// The parse node to use to read the discriminator value and create the object + public static ErrorResponseDocument_meta CreateFromDiscriminatorValue(IParseNode parseNode) { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new ErrorResponseDocument_meta(); + } + /// + /// The deserialization information for the current model + /// + public virtual IDictionary> GetFieldDeserializers() { + return new Dictionary> { + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteAdditionalData(AdditionalData); + } + } +} diff --git a/test/OpenApiKiotaEndToEndTests/QueryStrings/GeneratedCode/Models/LinksInErrorDocument.cs b/test/OpenApiKiotaEndToEndTests/QueryStrings/GeneratedCode/Models/LinksInErrorDocument.cs new file mode 100644 index 0000000000..55aba20a25 --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/QueryStrings/GeneratedCode/Models/LinksInErrorDocument.cs @@ -0,0 +1,73 @@ +// +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions.Store; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System; +namespace OpenApiKiotaEndToEndTests.QueryStrings.GeneratedCode.Models { + public class LinksInErrorDocument : IBackedModel, IParsable { + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + /// The describedby property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Describedby { + get { return BackingStore?.Get("describedby"); } + set { BackingStore?.Set("describedby", value); } + } +#nullable restore +#else + public string Describedby { + get { return BackingStore?.Get("describedby"); } + set { BackingStore?.Set("describedby", value); } + } +#endif + /// The self property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Self { + get { return BackingStore?.Get("self"); } + set { BackingStore?.Set("self", value); } + } +#nullable restore +#else + public string Self { + get { return BackingStore?.Get("self"); } + set { BackingStore?.Set("self", value); } + } +#endif + /// + /// Instantiates a new linksInErrorDocument and sets the default values. + /// + public LinksInErrorDocument() { + BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// The parse node to use to read the discriminator value and create the object + public static LinksInErrorDocument CreateFromDiscriminatorValue(IParseNode parseNode) { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new LinksInErrorDocument(); + } + /// + /// The deserialization information for the current model + /// + public virtual IDictionary> GetFieldDeserializers() { + return new Dictionary> { + {"describedby", n => { Describedby = n.GetStringValue(); } }, + {"self", n => { Self = n.GetStringValue(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("describedby", Describedby); + writer.WriteStringValue("self", Self); + } + } +} diff --git a/test/OpenApiNSwagClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiNSwagClientTests/LegacyClient/ResponseTests.cs index c2b949456c..414ed985de 100644 --- a/test/OpenApiNSwagClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiNSwagClientTests/LegacyClient/ResponseTests.cs @@ -202,6 +202,10 @@ public async Task Getting_unknown_resource_translates_error_response() const string responseBody = $$""" { + "links": { + "self": "http://localhost/api/flights/ZvuH1", + "describedby": "swagger/v1/swagger.json" + }, "errors": [ { "id": "f1a520ac-02a0-466b-94ea-86cbaa86f02f", @@ -222,6 +226,9 @@ public async Task Getting_unknown_resource_translates_error_response() // Assert ApiException exception = (await action.Should().ThrowExactlyAsync>()).Which; exception.StatusCode.Should().Be((int)HttpStatusCode.NotFound); + exception.Result.Links.ShouldNotBeNull(); + exception.Result.Links.Self.Should().Be("http://localhost/api/flights/ZvuH1"); + exception.Result.Links.Describedby.Should().Be("swagger/v1/swagger.json"); exception.Result.Errors.ShouldHaveCount(1); ErrorObject? error = exception.Result.Errors.ElementAt(0); diff --git a/test/OpenApiNSwagClientTests/NamingConventions/CamelCase/GeneratedTypesTests.cs b/test/OpenApiNSwagClientTests/NamingConventions/CamelCase/GeneratedTypesTests.cs index c46e126b25..0b2bf9ad0c 100644 --- a/test/OpenApiNSwagClientTests/NamingConventions/CamelCase/GeneratedTypesTests.cs +++ b/test/OpenApiNSwagClientTests/NamingConventions/CamelCase/GeneratedTypesTests.cs @@ -60,6 +60,7 @@ public void Generated_top_level_document_types_are_named_as_expected() _ = nameof(StaffMemberSecondaryResponseDocument); _ = nameof(NullableStaffMemberSecondaryResponseDocument); _ = nameof(NullableStaffMemberIdentifierResponseDocument); + _ = nameof(ErrorResponseDocument); } [Fact] @@ -71,6 +72,7 @@ public void Generated_link_types_are_named_as_expected() _ = nameof(LinksInResourceIdentifierDocument); _ = nameof(LinksInResourceData); _ = nameof(LinksInRelationship); + _ = nameof(LinksInErrorDocument); } [Fact] diff --git a/test/OpenApiNSwagClientTests/NamingConventions/KebabCase/GeneratedTypesTests.cs b/test/OpenApiNSwagClientTests/NamingConventions/KebabCase/GeneratedTypesTests.cs index be720ca21f..c4784dd404 100644 --- a/test/OpenApiNSwagClientTests/NamingConventions/KebabCase/GeneratedTypesTests.cs +++ b/test/OpenApiNSwagClientTests/NamingConventions/KebabCase/GeneratedTypesTests.cs @@ -60,6 +60,7 @@ public void Generated_top_level_document_types_are_named_as_expected() _ = nameof(StaffMemberSecondaryResponseDocument); _ = nameof(NullableStaffMemberSecondaryResponseDocument); _ = nameof(NullableStaffMemberIdentifierResponseDocument); + _ = nameof(ErrorResponseDocument); } [Fact] @@ -71,6 +72,7 @@ public void Generated_link_types_are_named_as_expected() _ = nameof(LinksInResourceIdentifierDocument); _ = nameof(LinksInResourceData); _ = nameof(LinksInRelationship); + _ = nameof(LinksInErrorDocument); } [Fact] diff --git a/test/OpenApiNSwagClientTests/NamingConventions/PascalCase/GeneratedTypesTests.cs b/test/OpenApiNSwagClientTests/NamingConventions/PascalCase/GeneratedTypesTests.cs index a6c40f8254..d7c7ecfcc3 100644 --- a/test/OpenApiNSwagClientTests/NamingConventions/PascalCase/GeneratedTypesTests.cs +++ b/test/OpenApiNSwagClientTests/NamingConventions/PascalCase/GeneratedTypesTests.cs @@ -60,6 +60,7 @@ public void Generated_top_level_document_types_are_named_as_expected() _ = nameof(StaffMemberSecondaryResponseDocument); _ = nameof(NullableStaffMemberSecondaryResponseDocument); _ = nameof(NullableStaffMemberIdentifierResponseDocument); + _ = nameof(ErrorResponseDocument); } [Fact] @@ -71,6 +72,7 @@ public void Generated_link_types_are_named_as_expected() _ = nameof(LinksInResourceIdentifierDocument); _ = nameof(LinksInResourceData); _ = nameof(LinksInRelationship); + _ = nameof(LinksInErrorDocument); } [Fact] diff --git a/test/OpenApiNSwagEndToEndTests/QueryStrings/FilterTests.cs b/test/OpenApiNSwagEndToEndTests/QueryStrings/FilterTests.cs index 70b9edabf6..4468ecaf35 100644 --- a/test/OpenApiNSwagEndToEndTests/QueryStrings/FilterTests.cs +++ b/test/OpenApiNSwagEndToEndTests/QueryStrings/FilterTests.cs @@ -127,6 +127,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => response.Data.ElementAt(0).Id.Should().Be(node.Children.ElementAt(1).StringId); response.Meta.ShouldNotBeNull(); response.Meta.ShouldContainKey("total").With(total => total.Should().Be(1)); + response.Links.ShouldNotBeNull(); + response.Links.Describedby.Should().Be("swagger/v1/swagger.json"); } [Fact] @@ -148,6 +150,8 @@ public async Task Cannot_use_empty_filter() ApiException exception = (await action.Should().ThrowExactlyAsync>()).Which; exception.StatusCode.Should().Be((int)HttpStatusCode.BadRequest); exception.Message.Should().Be("HTTP 400: The query string is invalid."); + exception.Result.Links.ShouldNotBeNull(); + exception.Result.Links.Describedby.Should().Be("swagger/v1/swagger.json"); exception.Result.Errors.ShouldHaveCount(1); ErrorObject error = exception.Result.Errors.ElementAt(0); diff --git a/test/OpenApiTests/ClientIdGenerationModes/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/ClientIdGenerationModes/GeneratedSwagger/swagger.g.json index 9583a743b6..115402244f 100644 --- a/test/OpenApiTests/ClientIdGenerationModes/GeneratedSwagger/swagger.g.json +++ b/test/OpenApiTests/ClientIdGenerationModes/GeneratedSwagger/swagger.g.json @@ -391,15 +391,30 @@ }, "errorResponseDocument": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInErrorDocument" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/errorObject" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -589,6 +604,22 @@ "type": "string", "additionalProperties": false }, + "linksInErrorDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "linksInRelationship": { "required": [ "related", diff --git a/test/OpenApiTests/Headers/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/Headers/GeneratedSwagger/swagger.g.json index 2e56437cb0..dc5ad972f3 100644 --- a/test/OpenApiTests/Headers/GeneratedSwagger/swagger.g.json +++ b/test/OpenApiTests/Headers/GeneratedSwagger/swagger.g.json @@ -1496,15 +1496,30 @@ }, "errorResponseDocument": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInErrorDocument" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/errorObject" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -1667,6 +1682,22 @@ "type": "string", "additionalProperties": false }, + "linksInErrorDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "linksInRelationship": { "required": [ "related", diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/LegacyOpenApiIntegration/GeneratedSwagger/swagger.g.json index fac1314112..b27522b927 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/GeneratedSwagger/swagger.g.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/GeneratedSwagger/swagger.g.json @@ -6281,15 +6281,37 @@ }, "error-response-document": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/links-in-error-document" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/error-object" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -7271,6 +7293,22 @@ }, "additionalProperties": false }, + "links-in-error-document": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "links-in-relationship": { "required": [ "related", diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json index fac1314112..b27522b927 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json @@ -6281,15 +6281,37 @@ }, "error-response-document": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/links-in-error-document" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/error-object" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -7271,6 +7293,22 @@ }, "additionalProperties": false }, + "links-in-error-document": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "links-in-relationship": { "required": [ "related", diff --git a/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseTests.cs b/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseTests.cs index e3498ed6cc..1bb2772a08 100644 --- a/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseTests.cs +++ b/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseTests.cs @@ -559,5 +559,6 @@ public async Task Casing_convention_is_applied_to_error_schema() // Assert document.Should().ContainPath("components.schemas.errorResponseDocument"); + document.Should().ContainPath("components.schemas.linksInErrorDocument"); } } diff --git a/test/OpenApiTests/NamingConventions/CamelCase/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/NamingConventions/CamelCase/GeneratedSwagger/swagger.g.json index 545231baa0..d8e8997345 100644 --- a/test/OpenApiTests/NamingConventions/CamelCase/GeneratedSwagger/swagger.g.json +++ b/test/OpenApiTests/NamingConventions/CamelCase/GeneratedSwagger/swagger.g.json @@ -2582,15 +2582,37 @@ }, "errorResponseDocument": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInErrorDocument" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/errorObject" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -2641,6 +2663,22 @@ }, "additionalProperties": false }, + "linksInErrorDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "linksInRelationship": { "required": [ "related", diff --git a/test/OpenApiTests/NamingConventions/KebabCase/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/NamingConventions/KebabCase/GeneratedSwagger/swagger.g.json index 22e59e773f..ae147e39dc 100644 --- a/test/OpenApiTests/NamingConventions/KebabCase/GeneratedSwagger/swagger.g.json +++ b/test/OpenApiTests/NamingConventions/KebabCase/GeneratedSwagger/swagger.g.json @@ -2582,15 +2582,37 @@ }, "error-response-document": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/links-in-error-document" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/error-object" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -2641,6 +2663,22 @@ }, "additionalProperties": false }, + "links-in-error-document": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "links-in-relationship": { "required": [ "related", diff --git a/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseTests.cs b/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseTests.cs index 6e516da190..95901620d2 100644 --- a/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseTests.cs +++ b/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseTests.cs @@ -561,5 +561,6 @@ public async Task Casing_convention_is_applied_to_error_schema() // Assert document.Should().ContainPath("components.schemas.error-response-document"); + document.Should().ContainPath("components.schemas.links-in-error-document"); } } diff --git a/test/OpenApiTests/NamingConventions/PascalCase/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/NamingConventions/PascalCase/GeneratedSwagger/swagger.g.json index 384bb5017f..fe05f25095 100644 --- a/test/OpenApiTests/NamingConventions/PascalCase/GeneratedSwagger/swagger.g.json +++ b/test/OpenApiTests/NamingConventions/PascalCase/GeneratedSwagger/swagger.g.json @@ -2582,15 +2582,37 @@ }, "ErrorResponseDocument": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/Jsonapi" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/LinksInErrorDocument" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/ErrorObject" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -2641,6 +2663,22 @@ }, "additionalProperties": false }, + "LinksInErrorDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "LinksInRelationship": { "required": [ "related", diff --git a/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseTests.cs b/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseTests.cs index 91a367d64a..7484ff12c5 100644 --- a/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseTests.cs +++ b/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseTests.cs @@ -560,5 +560,6 @@ public async Task Casing_convention_is_applied_to_error_schema() // Assert document.Should().ContainPath("components.schemas.ErrorResponseDocument"); + document.Should().ContainPath("components.schemas.LinksInErrorDocument"); } } diff --git a/test/OpenApiTests/QueryStrings/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/QueryStrings/GeneratedSwagger/swagger.g.json index 3b04c1abc9..a3118dc50d 100644 --- a/test/OpenApiTests/QueryStrings/GeneratedSwagger/swagger.g.json +++ b/test/OpenApiTests/QueryStrings/GeneratedSwagger/swagger.g.json @@ -3131,15 +3131,30 @@ }, "errorResponseDocument": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInErrorDocument" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/errorObject" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -3162,6 +3177,22 @@ }, "additionalProperties": false }, + "linksInErrorDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "linksInRelationship": { "required": [ "related", diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/GeneratedSwagger/swagger.g.json index a2d386cefd..9b3dda8aac 100644 --- a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/GeneratedSwagger/swagger.g.json +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/GeneratedSwagger/swagger.g.json @@ -2701,15 +2701,30 @@ }, "errorResponseDocument": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInErrorDocument" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/errorObject" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -2732,6 +2747,22 @@ }, "additionalProperties": false }, + "linksInErrorDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "linksInRelationship": { "required": [ "related", diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/GeneratedSwagger/swagger.g.json index 7afb42a553..91a4259052 100644 --- a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/GeneratedSwagger/swagger.g.json +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/GeneratedSwagger/swagger.g.json @@ -2769,15 +2769,30 @@ }, "errorResponseDocument": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInErrorDocument" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/errorObject" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -2800,6 +2815,22 @@ }, "additionalProperties": false }, + "linksInErrorDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "linksInRelationship": { "required": [ "related", diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/GeneratedSwagger/swagger.g.json index 02ccee3eb3..832c851f75 100644 --- a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/GeneratedSwagger/swagger.g.json +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/GeneratedSwagger/swagger.g.json @@ -3599,15 +3599,30 @@ }, "errorResponseDocument": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInErrorDocument" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/errorObject" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -3630,6 +3645,22 @@ }, "additionalProperties": false }, + "linksInErrorDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "linksInRelationship": { "required": [ "related", diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/GeneratedSwagger/swagger.g.json index 6cda95634b..b453d1536b 100644 --- a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/GeneratedSwagger/swagger.g.json +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/GeneratedSwagger/swagger.g.json @@ -3599,15 +3599,30 @@ }, "errorResponseDocument": { "required": [ - "errors" + "errors", + "links" ], "type": "object", "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInErrorDocument" + } + ] + }, "errors": { "type": "array", "items": { "$ref": "#/components/schemas/errorObject" } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + } } }, "additionalProperties": false @@ -3630,6 +3645,22 @@ }, "additionalProperties": false }, + "linksInErrorDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, "linksInRelationship": { "required": [ "related",