diff --git a/JsonApiDotNetCore.sln.DotSettings b/JsonApiDotNetCore.sln.DotSettings
index bd9975440c..9902bb57d9 100644
--- a/JsonApiDotNetCore.sln.DotSettings
+++ b/JsonApiDotNetCore.sln.DotSettings
@@ -15,6 +15,8 @@ JsonApiDotNetCore.ArgumentGuard.NotNull($EXPR$, $NAME$);
50
False
True
+ True
+
swagger.g.json
swagger.json
SOLUTION
diff --git a/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs b/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs
deleted file mode 100644
index a6f46ccb9d..0000000000
--- a/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Reflection;
-using Swashbuckle.AspNetCore.SwaggerGen;
-
-namespace JsonApiDotNetCore.OpenApi;
-
-internal static class MemberInfoExtensions
-{
- public static TypeCategory GetTypeCategory(this MemberInfo source)
- {
- ArgumentGuard.NotNull(source, nameof(source));
-
- Type memberType;
-
- if (source.MemberType.HasFlag(MemberTypes.Field))
- {
- memberType = ((FieldInfo)source).FieldType;
- }
- else if (source.MemberType.HasFlag(MemberTypes.Property))
- {
- memberType = ((PropertyInfo)source).PropertyType;
- }
- else
- {
- throw new NotSupportedException($"Member type '{source.MemberType}' must be a property or field.");
- }
-
- if (memberType.IsValueType)
- {
- return Nullable.GetUnderlyingType(memberType) != null ? TypeCategory.NullableValueType : TypeCategory.ValueType;
- }
-
- // Once we switch to .NET 6, we should rely instead on the built-in reflection APIs for nullability information.
- // See https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-7/#libraries-reflection-apis-for-nullability-information.
- return source.IsNonNullableReferenceType() ? TypeCategory.NonNullableReferenceType : TypeCategory.NullableReferenceType;
- }
-}
diff --git a/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs
index 32bf9f5187..d4b9c40fea 100644
--- a/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs
+++ b/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using System.Reflection;
using JsonApiDotNetCore.Resources.Annotations;
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -6,16 +7,34 @@ namespace JsonApiDotNetCore.OpenApi;
internal static class ResourceFieldAttributeExtensions
{
+ private static readonly NullabilityInfoContext NullabilityInfoContext = new();
+
public static bool IsNullable(this ResourceFieldAttribute source)
{
- TypeCategory fieldTypeCategory = source.Property.GetTypeCategory();
bool hasRequiredAttribute = source.Property.HasAttribute();
- return fieldTypeCategory switch
+ if (hasRequiredAttribute)
{
- TypeCategory.NonNullableReferenceType or TypeCategory.ValueType => false,
- TypeCategory.NullableReferenceType or TypeCategory.NullableValueType => !hasRequiredAttribute,
- _ => throw new UnreachableCodeException()
- };
+ // Reflects the following cases, independent of NRT setting
+ // `[Required] int? Number` => not nullable
+ // `[Required] int Number` => not nullable
+ // `[Required] string Text` => not nullable
+ // `[Required] string? Text` => not nullable
+ // `[Required] string Text` => not nullable
+ return false;
+ }
+
+ NullabilityInfo nullabilityInfo = NullabilityInfoContext.Create(source.Property);
+
+ // Reflects the following cases:
+ // Independent of NRT:
+ // `int? Number` => nullable
+ // `int Number` => not nullable
+ // If NRT is enabled:
+ // `string? Text` => nullable
+ // `string Text` => not nullable
+ // If NRT is disabled:
+ // `string Text` => nullable
+ return nullabilityInfo.ReadState is not NullabilityState.NotNull;
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs
index a0e7159a89..c5fe2b6a2d 100644
--- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs
+++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs
@@ -26,6 +26,13 @@ internal sealed class JsonApiSchemaGenerator : ISchemaGenerator
typeof(NullableToOneRelationshipInRequest<>)
};
+ private static readonly Type[] JsonApiDocumentWithNullableDataOpenTypes =
+ {
+ typeof(NullableSecondaryResourceResponseDocument<>),
+ typeof(NullableResourceIdentifierResponseDocument<>),
+ typeof(NullableToOneRelationshipInRequest<>)
+ };
+
private readonly ISchemaGenerator _defaultSchemaGenerator;
private readonly ResourceObjectSchemaGenerator _resourceObjectSchemaGenerator;
private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator;
@@ -58,7 +65,7 @@ public OpenApiSchema GenerateSchema(Type type, SchemaRepository schemaRepository
{
OpenApiSchema schema = GenerateJsonApiDocumentSchema(type);
- if (IsDataPropertyNullable(type))
+ if (IsDataPropertyNullableInDocument(type))
{
SetDataObjectSchemaToNullable(schema);
}
@@ -98,18 +105,11 @@ private static bool IsManyDataDocument(Type documentType)
return documentType.BaseType!.GetGenericTypeDefinition() == typeof(ManyData<>);
}
- private static bool IsDataPropertyNullable(Type type)
+ private static bool IsDataPropertyNullableInDocument(Type documentType)
{
- PropertyInfo? dataProperty = type.GetProperty(nameof(JsonApiObjectPropertyName.Data));
-
- if (dataProperty == null)
- {
- throw new UnreachableCodeException();
- }
-
- TypeCategory typeCategory = dataProperty.GetTypeCategory();
+ Type documentOpenType = documentType.GetGenericTypeDefinition();
- return typeCategory == TypeCategory.NullableReferenceType;
+ return JsonApiDocumentWithNullableDataOpenTypes.Contains(documentOpenType);
}
private void SetDataObjectSchemaToNullable(OpenApiSchema referenceSchemaForDocument)
diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs
index ef8c705424..6dbf35dd1e 100644
--- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs
+++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
-using System.Text.Json;
+using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.OpenApi.JsonApiObjects;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
@@ -12,35 +12,46 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents;
internal sealed class ResourceFieldObjectSchemaBuilder
{
- private static readonly Type[] RelationshipInResponseOpenTypes =
+ private static readonly NullabilityInfoContext NullabilityInfoContext = new();
+
+ private static readonly Type[] RelationshipSchemaInResponseOpenTypes =
{
typeof(ToOneRelationshipInResponse<>),
typeof(ToManyRelationshipInResponse<>),
typeof(NullableToOneRelationshipInResponse<>)
};
+ private static readonly Type[] NullableRelationshipSchemaOpenTypes =
+ {
+ typeof(NullableToOneRelationshipInRequest<>),
+ typeof(NullableToOneRelationshipInResponse<>)
+ };
+
private readonly ResourceTypeInfo _resourceTypeInfo;
private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor;
private readonly SchemaGenerator _defaultSchemaGenerator;
private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator;
+ private readonly IJsonApiOptions _options;
private readonly SchemaRepository _resourceSchemaRepository = new();
private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator;
private readonly IDictionary _schemasForResourceFields;
public ResourceFieldObjectSchemaBuilder(ResourceTypeInfo resourceTypeInfo, ISchemaRepositoryAccessor schemaRepositoryAccessor,
- SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, JsonNamingPolicy? namingPolicy)
+ SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, IJsonApiOptions options)
{
ArgumentGuard.NotNull(resourceTypeInfo, nameof(resourceTypeInfo));
ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor));
ArgumentGuard.NotNull(defaultSchemaGenerator, nameof(defaultSchemaGenerator));
ArgumentGuard.NotNull(resourceTypeSchemaGenerator, nameof(resourceTypeSchemaGenerator));
+ ArgumentGuard.NotNull(options, nameof(options));
_resourceTypeInfo = resourceTypeInfo;
_schemaRepositoryAccessor = schemaRepositoryAccessor;
_defaultSchemaGenerator = defaultSchemaGenerator;
_resourceTypeSchemaGenerator = resourceTypeSchemaGenerator;
+ _options = options;
- _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor, namingPolicy);
+ _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor, options.SerializerOptions.PropertyNamingPolicy);
_schemasForResourceFields = GetFieldSchemas();
}
@@ -108,15 +119,14 @@ private bool IsFieldRequired(ResourceFieldAttribute field)
return false;
}
- TypeCategory fieldTypeCategory = field.Property.GetTypeCategory();
bool hasRequiredAttribute = field.Property.HasAttribute();
- return fieldTypeCategory switch
+ NullabilityInfo nullabilityInfo = NullabilityInfoContext.Create(field.Property);
+
+ return field.Property.PropertyType.IsValueType switch
{
- TypeCategory.NonNullableReferenceType => true,
- TypeCategory.ValueType => hasRequiredAttribute,
- TypeCategory.NullableReferenceType or TypeCategory.NullableValueType => hasRequiredAttribute,
- _ => throw new UnreachableCodeException()
+ true => hasRequiredAttribute,
+ false => _options.ValidateModelState ? nullabilityInfo.ReadState == NullabilityState.NotNull || hasRequiredAttribute : hasRequiredAttribute
};
}
@@ -194,7 +204,9 @@ private OpenApiSchema CreateRelationshipSchema(Type relationshipSchemaType)
OpenApiSchema fullSchema = _schemaRepositoryAccessor.Current.Schemas[referenceSchema.Reference.Id];
- if (IsDataPropertyNullable(relationshipSchemaType))
+ Console.WriteLine(relationshipSchemaType.FullName);
+
+ if (IsDataPropertyNullableInRelationshipSchemaType(relationshipSchemaType))
{
fullSchema.Properties[JsonApiObjectPropertyName.Data] =
_nullableReferenceSchemaGenerator.GenerateSchema(fullSchema.Properties[JsonApiObjectPropertyName.Data]);
@@ -212,18 +224,12 @@ private static bool IsRelationshipInResponseType(Type relationshipSchemaType)
{
Type relationshipSchemaOpenType = relationshipSchemaType.GetGenericTypeDefinition();
- return RelationshipInResponseOpenTypes.Contains(relationshipSchemaOpenType);
+ return RelationshipSchemaInResponseOpenTypes.Contains(relationshipSchemaOpenType);
}
- private static bool IsDataPropertyNullable(Type type)
+ private static bool IsDataPropertyNullableInRelationshipSchemaType(Type relationshipSchemaType)
{
- PropertyInfo? dataProperty = type.GetProperty(nameof(JsonApiObjectPropertyName.Data));
-
- if (dataProperty == null)
- {
- throw new UnreachableCodeException();
- }
-
- return dataProperty.GetTypeCategory() == TypeCategory.NullableReferenceType;
+ Type relationshipSchemaOpenType = relationshipSchemaType.GetGenericTypeDefinition();
+ return NullableRelationshipSchemaOpenTypes.Contains(relationshipSchemaOpenType);
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs
index 616b98ef63..a4fda8d7d0 100644
--- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs
+++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs
@@ -31,7 +31,7 @@ public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IRe
_allowClientGeneratedIds = options.AllowClientGeneratedIds;
_resourceFieldObjectSchemaBuilderFactory = resourceTypeInfo => new ResourceFieldObjectSchemaBuilder(resourceTypeInfo, schemaRepositoryAccessor,
- defaultSchemaGenerator, _resourceTypeSchemaGenerator, options.SerializerOptions.PropertyNamingPolicy);
+ defaultSchemaGenerator, _resourceTypeSchemaGenerator, options);
}
public OpenApiSchema GenerateSchema(Type resourceObjectType)
diff --git a/src/JsonApiDotNetCore.OpenApi/TypeCategory.cs b/src/JsonApiDotNetCore.OpenApi/TypeCategory.cs
deleted file mode 100644
index 1641e31775..0000000000
--- a/src/JsonApiDotNetCore.OpenApi/TypeCategory.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace JsonApiDotNetCore.OpenApi;
-
-internal enum TypeCategory
-{
- NonNullableReferenceType,
- NullableReferenceType,
- ValueType,
- NullableValueType
-}
diff --git a/test/OpenApiClientTests/LegacyClient/ApiResponse.cs b/test/OpenApiClientTests/ApiResponse.cs
similarity index 94%
rename from test/OpenApiClientTests/LegacyClient/ApiResponse.cs
rename to test/OpenApiClientTests/ApiResponse.cs
index d009d2acee..0c353ea5c0 100644
--- a/test/OpenApiClientTests/LegacyClient/ApiResponse.cs
+++ b/test/OpenApiClientTests/ApiResponse.cs
@@ -3,7 +3,7 @@
#pragma warning disable AV1008 // Class should not be static
-namespace OpenApiClientTests.LegacyClient;
+namespace OpenApiClientTests;
internal static class ApiResponse
{
diff --git a/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs b/test/OpenApiClientTests/FakeHttpClientWrapper.cs
similarity index 98%
rename from test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs
rename to test/OpenApiClientTests/FakeHttpClientWrapper.cs
index 254f7f31cd..7240daf029 100644
--- a/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs
+++ b/test/OpenApiClientTests/FakeHttpClientWrapper.cs
@@ -3,7 +3,7 @@
using System.Text;
using JsonApiDotNetCore.OpenApi.Client;
-namespace OpenApiClientTests.LegacyClient;
+namespace OpenApiClientTests;
///
/// Enables to inject an outgoing response body and inspect the incoming request.
diff --git a/test/OpenApiClientTests/OpenApiClientTests.csproj b/test/OpenApiClientTests/OpenApiClientTests.csproj
index 3f54c161b8..b16a6f3947 100644
--- a/test/OpenApiClientTests/OpenApiClientTests.csproj
+++ b/test/OpenApiClientTests/OpenApiClientTests.csproj
@@ -19,10 +19,6 @@
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -62,5 +58,19 @@
NSwagCSharp
/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions
+
+ OpenApiClientTests.SchemaProperties.NullableReferenceTypesEnabled.GeneratedCode
+ NullableReferenceTypesEnabledClient
+ NullableReferenceTypesEnabledClient.cs
+ NSwagCSharp
+ /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:true
+
+
+ OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode
+ NullableReferenceTypesDisabledClient
+ NullableReferenceTypesDisabledClient.cs
+ NSwagCSharp
+ /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:false
+
\ No newline at end of file
diff --git a/test/OpenApiClientTests/PropertyInfoAssertionsExtension.cs b/test/OpenApiClientTests/PropertyInfoAssertionsExtension.cs
new file mode 100644
index 0000000000..c3338a300b
--- /dev/null
+++ b/test/OpenApiClientTests/PropertyInfoAssertionsExtension.cs
@@ -0,0 +1,30 @@
+using System.Reflection;
+using FluentAssertions;
+using FluentAssertions.Types;
+
+namespace OpenApiClientTests;
+
+internal static class PropertyInfoAssertionsExtensions
+{
+ private static readonly NullabilityInfoContext NullabilityInfoContext = new();
+
+ [CustomAssertion]
+ public static void BeNullable(this PropertyInfoAssertions source, string because = "", params object[] becauseArgs)
+ {
+ PropertyInfo propertyInfo = source.Subject;
+
+ NullabilityInfo nullabilityInfo = NullabilityInfoContext.Create(propertyInfo);
+
+ nullabilityInfo.ReadState.Should().NotBe(NullabilityState.NotNull, because, becauseArgs);
+ }
+
+ [CustomAssertion]
+ public static void BeNonNullable(this PropertyInfoAssertions source, string because = "", params object[] becauseArgs)
+ {
+ PropertyInfo propertyInfo = source.Subject;
+
+ NullabilityInfo nullabilityInfo = NullabilityInfoContext.Create(propertyInfo);
+
+ nullabilityInfo.ReadState.Should().Be(NullabilityState.NotNull, because, becauseArgs);
+ }
+}
diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/GeneratedCode/NullableReferenceTypesDisabledClient.cs b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/GeneratedCode/NullableReferenceTypesDisabledClient.cs
new file mode 100644
index 0000000000..c1a4eebab5
--- /dev/null
+++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/GeneratedCode/NullableReferenceTypesDisabledClient.cs
@@ -0,0 +1,14 @@
+using JsonApiDotNetCore.OpenApi.Client;
+using Newtonsoft.Json;
+
+namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode;
+
+internal partial class NullableReferenceTypesDisabledClient : JsonApiClient
+{
+ partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings)
+ {
+ SetSerializerSettingsForJsonApi(settings);
+
+ settings.Formatting = Formatting.Indented;
+ }
+}
diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/NullabilityTests.cs b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/NullabilityTests.cs
new file mode 100644
index 0000000000..136c729537
--- /dev/null
+++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/NullabilityTests.cs
@@ -0,0 +1,27 @@
+using System.Reflection;
+using FluentAssertions;
+using OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode;
+using Xunit;
+
+namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled;
+
+public sealed class NullabilityTests
+{
+ [Fact]
+ public void Nullability_of_generated_types_is_as_expected()
+ {
+ PropertyInfo[] propertyInfos = typeof(ChickenAttributesInResponse).GetProperties();
+
+ PropertyInfo? propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(ChickenAttributesInResponse.Name));
+ propertyInfo.Should().BeNullable();
+
+ propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(ChickenAttributesInResponse.NameOfCurrentFarm));
+ propertyInfo.Should().BeNullable();
+
+ propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(ChickenAttributesInResponse.Age));
+ propertyInfo.Should().BeNonNullable();
+
+ propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(ChickenAttributesInResponse.TimeAtCurrentFarmInDays));
+ propertyInfo.Should().BeNullable();
+ }
+}
diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/RequiredAttributesTests.cs b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/RequiredAttributesTests.cs
new file mode 100644
index 0000000000..b97535f908
--- /dev/null
+++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/RequiredAttributesTests.cs
@@ -0,0 +1,113 @@
+using System.Net;
+using FluentAssertions;
+using FluentAssertions.Specialized;
+using JsonApiDotNetCore.Middleware;
+using Microsoft.Net.Http.Headers;
+using Newtonsoft.Json;
+using OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode;
+using TestBuildingBlocks;
+using Xunit;
+
+namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled;
+
+public sealed class RequiredAttributesTests
+{
+ private const string HostPrefix = "http://localhost/";
+
+ [Fact]
+ public async Task Partial_posting_resource_with_explicitly_omitting_required_fields_produces_expected_request()
+ {
+ // Arrange
+ using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null);
+ var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient);
+
+ var requestDocument = new ChickenPostRequestDocument
+ {
+ Data = new ChickenDataInPostRequest
+ {
+ Attributes = new ChickenAttributesInPostRequest
+ {
+ HasProducedEggs = true
+ }
+ }
+ };
+
+ using (apiClient.RegisterAttributesForRequestDocument(requestDocument,
+ chicken => chicken.HasProducedEggs))
+ {
+ // Act
+ await ApiResponse.TranslateAsync(async () => await apiClient.PostChickenAsync(requestDocument));
+ }
+
+ // Assert
+ wrapper.Request.ShouldNotBeNull();
+ wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType);
+ wrapper.Request.Method.Should().Be(HttpMethod.Post);
+ wrapper.Request.RequestUri.Should().Be(HostPrefix + "chickens");
+ wrapper.Request.Content.Should().NotBeNull();
+ wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull();
+ wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType);
+
+ wrapper.RequestBody.Should().BeJson(@"{
+ ""data"": {
+ ""type"": ""chickens"",
+ ""attributes"": {
+ ""hasProducedEggs"": true
+ }
+ }
+}");
+ }
+
+ [Fact]
+ public async Task Partial_posting_resource_without_explicitly_omitting_required_fields_fails()
+ {
+ // Arrange
+ using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null);
+ var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient);
+
+ var requestDocument = new ChickenPostRequestDocument
+ {
+ Data = new ChickenDataInPostRequest
+ {
+ Attributes = new ChickenAttributesInPostRequest
+ {
+ Weight = 3
+ }
+ }
+ };
+
+ // Act
+ Func> action = async () =>
+ await ApiResponse.TranslateAsync(async () => await apiClient.PostChickenAsync(requestDocument));
+
+ // Assert
+ ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync();
+ JsonSerializationException exception = assertion.Subject.Single();
+
+ exception.Message.Should().Be("Cannot write a null value for property 'nameOfCurrentFarm'. Property requires a value. Path 'data.attributes'.");
+ }
+
+ [Fact]
+ public async Task Patching_resource_with_missing_id_fails()
+ {
+ // Arrange
+ using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null);
+ var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient);
+
+ var requestDocument = new ChickenPatchRequestDocument
+ {
+ Data = new ChickenDataInPatchRequest
+ {
+ Attributes = new ChickenAttributesInPatchRequest
+ {
+ Age = 1
+ }
+ }
+ };
+
+ Func action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PatchChickenAsync(1, requestDocument));
+
+ // Assert
+ await action.Should().ThrowAsync();
+ }
+}
diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/swagger.g.json b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/swagger.g.json
new file mode 100644
index 0000000000..76771a1783
--- /dev/null
+++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/swagger.g.json
@@ -0,0 +1,516 @@
+{
+ "openapi": "3.0.1",
+ "info": {
+ "title": "OpenApiTests",
+ "version": "1.0"
+ },
+ "paths": {
+ "/chickens": {
+ "get": {
+ "tags": [
+ "chickens"
+ ],
+ "operationId": "getChickenCollection",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/chickenCollectionResponseDocument"
+ }
+ }
+ }
+ }
+ }
+ },
+ "head": {
+ "tags": [
+ "chickens"
+ ],
+ "operationId": "headChickenCollection",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/chickenCollectionResponseDocument"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "chickens"
+ ],
+ "operationId": "postChicken",
+ "requestBody": {
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/chickenPostRequestDocument"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Success",
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/chickenPrimaryResponseDocument"
+ }
+ }
+ }
+ },
+ "204": {
+ "description": "Success"
+ }
+ }
+ }
+ },
+ "/chickens/{id}": {
+ "get": {
+ "tags": [
+ "chickens"
+ ],
+ "operationId": "getChicken",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/chickenPrimaryResponseDocument"
+ }
+ }
+ }
+ }
+ }
+ },
+ "head": {
+ "tags": [
+ "chickens"
+ ],
+ "operationId": "headChicken",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/chickenPrimaryResponseDocument"
+ }
+ }
+ }
+ }
+ }
+ },
+ "patch": {
+ "tags": [
+ "chickens"
+ ],
+ "operationId": "patchChicken",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/chickenPatchRequestDocument"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/chickenPrimaryResponseDocument"
+ }
+ }
+ }
+ },
+ "204": {
+ "description": "Success"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "chickens"
+ ],
+ "operationId": "deleteChicken",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "Success"
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "chickenAttributesInPatchRequest": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "nullable": true
+ },
+ "nameOfCurrentFarm": {
+ "type": "string"
+ },
+ "age": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "weight": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "timeAtCurrentFarmInDays": {
+ "type": "integer",
+ "format": "int32",
+ "nullable": true
+ },
+ "hasProducedEggs": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
+ "chickenAttributesInPostRequest": {
+ "required": [
+ "hasProducedEggs",
+ "nameOfCurrentFarm",
+ "weight"
+ ],
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "nullable": true
+ },
+ "nameOfCurrentFarm": {
+ "type": "string"
+ },
+ "age": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "weight": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "timeAtCurrentFarmInDays": {
+ "type": "integer",
+ "format": "int32",
+ "nullable": true
+ },
+ "hasProducedEggs": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
+ "chickenAttributesInResponse": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "nullable": true
+ },
+ "nameOfCurrentFarm": {
+ "type": "string"
+ },
+ "age": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "weight": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "timeAtCurrentFarmInDays": {
+ "type": "integer",
+ "format": "int32",
+ "nullable": true
+ },
+ "hasProducedEggs": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
+ "chickenCollectionResponseDocument": {
+ "required": [
+ "data",
+ "links"
+ ],
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/chickenDataInResponse"
+ }
+ },
+ "meta": {
+ "type": "object",
+ "additionalProperties": { }
+ },
+ "jsonapi": {
+ "$ref": "#/components/schemas/jsonapiObject"
+ },
+ "links": {
+ "$ref": "#/components/schemas/linksInResourceCollectionDocument"
+ }
+ },
+ "additionalProperties": false
+ },
+ "chickenDataInPatchRequest": {
+ "required": [
+ "id",
+ "type"
+ ],
+ "type": "object",
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/chickenResourceType"
+ },
+ "id": {
+ "type": "string"
+ },
+ "attributes": {
+ "$ref": "#/components/schemas/chickenAttributesInPatchRequest"
+ }
+ },
+ "additionalProperties": false
+ },
+ "chickenDataInPostRequest": {
+ "required": [
+ "type"
+ ],
+ "type": "object",
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/chickenResourceType"
+ },
+ "attributes": {
+ "$ref": "#/components/schemas/chickenAttributesInPostRequest"
+ }
+ },
+ "additionalProperties": false
+ },
+ "chickenDataInResponse": {
+ "required": [
+ "id",
+ "links",
+ "type"
+ ],
+ "type": "object",
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/chickenResourceType"
+ },
+ "id": {
+ "type": "string"
+ },
+ "attributes": {
+ "$ref": "#/components/schemas/chickenAttributesInResponse"
+ },
+ "links": {
+ "$ref": "#/components/schemas/linksInResourceObject"
+ },
+ "meta": {
+ "type": "object",
+ "additionalProperties": { }
+ }
+ },
+ "additionalProperties": false
+ },
+ "chickenPatchRequestDocument": {
+ "required": [
+ "data"
+ ],
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/components/schemas/chickenDataInPatchRequest"
+ }
+ },
+ "additionalProperties": false
+ },
+ "chickenPostRequestDocument": {
+ "required": [
+ "data"
+ ],
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/components/schemas/chickenDataInPostRequest"
+ }
+ },
+ "additionalProperties": false
+ },
+ "chickenPrimaryResponseDocument": {
+ "required": [
+ "data",
+ "links"
+ ],
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/components/schemas/chickenDataInResponse"
+ },
+ "meta": {
+ "type": "object",
+ "additionalProperties": { }
+ },
+ "jsonapi": {
+ "$ref": "#/components/schemas/jsonapiObject"
+ },
+ "links": {
+ "$ref": "#/components/schemas/linksInResourceDocument"
+ }
+ },
+ "additionalProperties": false
+ },
+ "chickenResourceType": {
+ "enum": [
+ "chickens"
+ ],
+ "type": "string"
+ },
+ "jsonapiObject": {
+ "type": "object",
+ "properties": {
+ "version": {
+ "type": "string"
+ },
+ "ext": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "profile": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "meta": {
+ "type": "object",
+ "additionalProperties": { }
+ }
+ },
+ "additionalProperties": false
+ },
+ "linksInResourceCollectionDocument": {
+ "required": [
+ "first",
+ "self"
+ ],
+ "type": "object",
+ "properties": {
+ "self": {
+ "type": "string"
+ },
+ "describedby": {
+ "type": "string"
+ },
+ "first": {
+ "type": "string"
+ },
+ "last": {
+ "type": "string"
+ },
+ "prev": {
+ "type": "string"
+ },
+ "next": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "linksInResourceDocument": {
+ "required": [
+ "self"
+ ],
+ "type": "object",
+ "properties": {
+ "self": {
+ "type": "string"
+ },
+ "describedby": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "linksInResourceObject": {
+ "required": [
+ "self"
+ ],
+ "type": "object",
+ "properties": {
+ "self": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/GeneratedCode/NullableReferenceTypesEnabledClient.cs b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/GeneratedCode/NullableReferenceTypesEnabledClient.cs
new file mode 100644
index 0000000000..17b822f56d
--- /dev/null
+++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/GeneratedCode/NullableReferenceTypesEnabledClient.cs
@@ -0,0 +1,14 @@
+using JsonApiDotNetCore.OpenApi.Client;
+using Newtonsoft.Json;
+
+namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesEnabled.GeneratedCode;
+
+internal partial class NullableReferenceTypesEnabledClient : JsonApiClient
+{
+ partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings)
+ {
+ SetSerializerSettingsForJsonApi(settings);
+
+ settings.Formatting = Formatting.Indented;
+ }
+}
diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/NullabilityTests.cs b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/NullabilityTests.cs
new file mode 100644
index 0000000000..dfb1f0a934
--- /dev/null
+++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/NullabilityTests.cs
@@ -0,0 +1,27 @@
+using System.Reflection;
+using FluentAssertions;
+using OpenApiClientTests.SchemaProperties.NullableReferenceTypesEnabled.GeneratedCode;
+using Xunit;
+
+namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesEnabled;
+
+public sealed class NullabilityTests
+{
+ [Fact]
+ public void Nullability_of_generated_types_is_as_expected()
+ {
+ PropertyInfo[] propertyInfos = typeof(CowAttributesInResponse).GetProperties();
+
+ PropertyInfo? propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(CowAttributesInResponse.Name));
+ propertyInfo.Should().BeNonNullable();
+
+ propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(CowAttributesInResponse.NameOfPreviousFarm));
+ propertyInfo.Should().BeNullable();
+
+ propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(CowAttributesInResponse.Age));
+ propertyInfo.Should().BeNonNullable();
+
+ propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(CowAttributesInResponse.TimeAtCurrentFarmInDays));
+ propertyInfo.Should().BeNullable();
+ }
+}
diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/RequiredAttributesTests.cs b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/RequiredAttributesTests.cs
new file mode 100644
index 0000000000..5f35ec7f29
--- /dev/null
+++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/RequiredAttributesTests.cs
@@ -0,0 +1,94 @@
+using System.Net;
+using FluentAssertions;
+using FluentAssertions.Specialized;
+using JsonApiDotNetCore.Middleware;
+using Microsoft.Net.Http.Headers;
+using Newtonsoft.Json;
+using OpenApiClientTests.SchemaProperties.NullableReferenceTypesEnabled.GeneratedCode;
+using TestBuildingBlocks;
+using Xunit;
+
+namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesEnabled;
+
+public sealed class RequiredAttributesTests
+{
+ private const string HostPrefix = "http://localhost/";
+
+ [Fact]
+ public async Task Partial_posting_resource_with_explicitly_omitting_required_fields_produces_expected_request()
+ {
+ // Arrange
+ using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null);
+ var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient);
+
+ var requestDocument = new CowPostRequestDocument
+ {
+ Data = new CowDataInPostRequest
+ {
+ Attributes = new CowAttributesInPostRequest
+ {
+ HasProducedMilk = true,
+ Weight = 1100
+ }
+ }
+ };
+
+ using (apiClient.RegisterAttributesForRequestDocument(requestDocument))
+ {
+ // Act
+ await ApiResponse.TranslateAsync(async () => await apiClient.PostCowAsync(requestDocument));
+ }
+
+ // Assert
+ wrapper.Request.ShouldNotBeNull();
+ wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType);
+ wrapper.Request.Method.Should().Be(HttpMethod.Post);
+ wrapper.Request.RequestUri.Should().Be(HostPrefix + "cows");
+ wrapper.Request.Content.Should().NotBeNull();
+ wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull();
+ wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType);
+
+ wrapper.RequestBody.Should().BeJson(@"{
+ ""data"": {
+ ""type"": ""cows"",
+ ""attributes"": {
+ ""weight"": 1100,
+ ""hasProducedMilk"": true
+ }
+ }
+}");
+ }
+
+ [Fact]
+ public async Task Partial_posting_resource_without_explicitly_omitting_required_fields_produces_expected_request()
+ {
+ // Arrange
+ using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null);
+ var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient);
+
+ var requestDocument = new CowPostRequestDocument
+ {
+ Data = new CowDataInPostRequest
+ {
+ Attributes = new CowAttributesInPostRequest
+ {
+ Weight = 1100,
+ Age = 5,
+ Name = "Cow 1",
+ NameOfCurrentFarm = "123",
+ NameOfPreviousFarm = "123"
+ }
+ }
+ };
+
+ // Act
+ Func>
+ action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PostCowAsync(requestDocument));
+
+ // Assert
+ ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync();
+ JsonSerializationException exception = assertion.Subject.Single();
+
+ exception.Message.Should().Be("Cannot write a null value for property 'nickname'. Property requires a value. Path 'data.attributes'.");
+ }
+}
diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/swagger.g.json b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/swagger.g.json
new file mode 100644
index 0000000000..78e6dc1a73
--- /dev/null
+++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/swagger.g.json
@@ -0,0 +1,536 @@
+{
+ "openapi": "3.0.1",
+ "info": {
+ "title": "OpenApiTests",
+ "version": "1.0"
+ },
+ "paths": {
+ "/cows": {
+ "get": {
+ "tags": [
+ "cows"
+ ],
+ "operationId": "getCowCollection",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/cowCollectionResponseDocument"
+ }
+ }
+ }
+ }
+ }
+ },
+ "head": {
+ "tags": [
+ "cows"
+ ],
+ "operationId": "headCowCollection",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/cowCollectionResponseDocument"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "cows"
+ ],
+ "operationId": "postCow",
+ "requestBody": {
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/cowPostRequestDocument"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Success",
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/cowPrimaryResponseDocument"
+ }
+ }
+ }
+ },
+ "204": {
+ "description": "Success"
+ }
+ }
+ }
+ },
+ "/cows/{id}": {
+ "get": {
+ "tags": [
+ "cows"
+ ],
+ "operationId": "getCow",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/cowPrimaryResponseDocument"
+ }
+ }
+ }
+ }
+ }
+ },
+ "head": {
+ "tags": [
+ "cows"
+ ],
+ "operationId": "headCow",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/cowPrimaryResponseDocument"
+ }
+ }
+ }
+ }
+ }
+ },
+ "patch": {
+ "tags": [
+ "cows"
+ ],
+ "operationId": "patchCow",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/cowPatchRequestDocument"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/vnd.api+json": {
+ "schema": {
+ "$ref": "#/components/schemas/cowPrimaryResponseDocument"
+ }
+ }
+ }
+ },
+ "204": {
+ "description": "Success"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "cows"
+ ],
+ "operationId": "deleteCow",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "Success"
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "cowAttributesInPatchRequest": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "nameOfCurrentFarm": {
+ "type": "string"
+ },
+ "nameOfPreviousFarm": {
+ "type": "string",
+ "nullable": true
+ },
+ "nickname": {
+ "type": "string"
+ },
+ "age": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "weight": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "timeAtCurrentFarmInDays": {
+ "type": "integer",
+ "format": "int32",
+ "nullable": true
+ },
+ "hasProducedMilk": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
+ "cowAttributesInPostRequest": {
+ "required": [
+ "hasProducedMilk",
+ "name",
+ "nameOfCurrentFarm",
+ "nickname",
+ "weight"
+ ],
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "nameOfCurrentFarm": {
+ "type": "string"
+ },
+ "nameOfPreviousFarm": {
+ "type": "string",
+ "nullable": true
+ },
+ "nickname": {
+ "type": "string"
+ },
+ "age": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "weight": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "timeAtCurrentFarmInDays": {
+ "type": "integer",
+ "format": "int32",
+ "nullable": true
+ },
+ "hasProducedMilk": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
+ "cowAttributesInResponse": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "nameOfCurrentFarm": {
+ "type": "string"
+ },
+ "nameOfPreviousFarm": {
+ "type": "string",
+ "nullable": true
+ },
+ "nickname": {
+ "type": "string"
+ },
+ "age": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "weight": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "timeAtCurrentFarmInDays": {
+ "type": "integer",
+ "format": "int32",
+ "nullable": true
+ },
+ "hasProducedMilk": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
+ "cowCollectionResponseDocument": {
+ "required": [
+ "data",
+ "links"
+ ],
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/cowDataInResponse"
+ }
+ },
+ "meta": {
+ "type": "object",
+ "additionalProperties": { }
+ },
+ "jsonapi": {
+ "$ref": "#/components/schemas/jsonapiObject"
+ },
+ "links": {
+ "$ref": "#/components/schemas/linksInResourceCollectionDocument"
+ }
+ },
+ "additionalProperties": false
+ },
+ "cowDataInPatchRequest": {
+ "required": [
+ "id",
+ "type"
+ ],
+ "type": "object",
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/cowResourceType"
+ },
+ "id": {
+ "type": "string"
+ },
+ "attributes": {
+ "$ref": "#/components/schemas/cowAttributesInPatchRequest"
+ }
+ },
+ "additionalProperties": false
+ },
+ "cowDataInPostRequest": {
+ "required": [
+ "type"
+ ],
+ "type": "object",
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/cowResourceType"
+ },
+ "attributes": {
+ "$ref": "#/components/schemas/cowAttributesInPostRequest"
+ }
+ },
+ "additionalProperties": false
+ },
+ "cowDataInResponse": {
+ "required": [
+ "id",
+ "links",
+ "type"
+ ],
+ "type": "object",
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/cowResourceType"
+ },
+ "id": {
+ "type": "string"
+ },
+ "attributes": {
+ "$ref": "#/components/schemas/cowAttributesInResponse"
+ },
+ "links": {
+ "$ref": "#/components/schemas/linksInResourceObject"
+ },
+ "meta": {
+ "type": "object",
+ "additionalProperties": { }
+ }
+ },
+ "additionalProperties": false
+ },
+ "cowPatchRequestDocument": {
+ "required": [
+ "data"
+ ],
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/components/schemas/cowDataInPatchRequest"
+ }
+ },
+ "additionalProperties": false
+ },
+ "cowPostRequestDocument": {
+ "required": [
+ "data"
+ ],
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/components/schemas/cowDataInPostRequest"
+ }
+ },
+ "additionalProperties": false
+ },
+ "cowPrimaryResponseDocument": {
+ "required": [
+ "data",
+ "links"
+ ],
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/components/schemas/cowDataInResponse"
+ },
+ "meta": {
+ "type": "object",
+ "additionalProperties": { }
+ },
+ "jsonapi": {
+ "$ref": "#/components/schemas/jsonapiObject"
+ },
+ "links": {
+ "$ref": "#/components/schemas/linksInResourceDocument"
+ }
+ },
+ "additionalProperties": false
+ },
+ "cowResourceType": {
+ "enum": [
+ "cows"
+ ],
+ "type": "string"
+ },
+ "jsonapiObject": {
+ "type": "object",
+ "properties": {
+ "version": {
+ "type": "string"
+ },
+ "ext": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "profile": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "meta": {
+ "type": "object",
+ "additionalProperties": { }
+ }
+ },
+ "additionalProperties": false
+ },
+ "linksInResourceCollectionDocument": {
+ "required": [
+ "first",
+ "self"
+ ],
+ "type": "object",
+ "properties": {
+ "self": {
+ "type": "string"
+ },
+ "describedby": {
+ "type": "string"
+ },
+ "first": {
+ "type": "string"
+ },
+ "last": {
+ "type": "string"
+ },
+ "prev": {
+ "type": "string"
+ },
+ "next": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "linksInResourceDocument": {
+ "required": [
+ "self"
+ ],
+ "type": "object",
+ "properties": {
+ "self": {
+ "type": "string"
+ },
+ "describedby": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "linksInResourceObject": {
+ "required": [
+ "self"
+ ],
+ "type": "object",
+ "properties": {
+ "self": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/OpenApiTests/JsonElementExtensions.cs b/test/OpenApiTests/JsonElementExtensions.cs
index d8dc7d4e20..6f1ffe1432 100644
--- a/test/OpenApiTests/JsonElementExtensions.cs
+++ b/test/OpenApiTests/JsonElementExtensions.cs
@@ -19,6 +19,13 @@ public static JsonElement ShouldContainPath(this JsonElement source, string path
return elementSelector.Should().NotThrow().Subject;
}
+ public static void ShouldNotContainPath(this JsonElement source, string path)
+ {
+ JsonElement? pathToken = source.SelectToken(path);
+
+ pathToken.Should().BeNull();
+ }
+
public static void ShouldBeString(this JsonElement source, string value)
{
source.ValueKind.Should().Be(JsonValueKind.String);
diff --git a/test/OpenApiTests/OpenApiTestContext.cs b/test/OpenApiTests/OpenApiTestContext.cs
index a750929827..9d4a95c33d 100644
--- a/test/OpenApiTests/OpenApiTestContext.cs
+++ b/test/OpenApiTests/OpenApiTestContext.cs
@@ -28,34 +28,27 @@ internal async Task GetSwaggerDocumentAsync()
private async Task CreateSwaggerDocumentAsync()
{
- string absoluteOutputPath = GetSwaggerDocumentAbsoluteOutputPath(SwaggerDocumentOutputPath);
-
string content = await GetAsync("swagger/v1/swagger.json");
JsonElement rootElement = ParseSwaggerDocument(content);
- await WriteToDiskAsync(absoluteOutputPath, rootElement);
+
+ if (SwaggerDocumentOutputPath != null)
+ {
+ string absoluteOutputPath = GetSwaggerDocumentAbsoluteOutputPath(SwaggerDocumentOutputPath);
+ await WriteToDiskAsync(absoluteOutputPath, rootElement);
+ }
return rootElement;
}
- private static string GetSwaggerDocumentAbsoluteOutputPath(string? relativePath)
+ private static string GetSwaggerDocumentAbsoluteOutputPath(string relativePath)
{
- AssertHasSwaggerDocumentOutputPath(relativePath);
-
string solutionRoot = Path.Combine(Assembly.GetExecutingAssembly().Location, "../../../../../../");
string outputPath = Path.Combine(solutionRoot, relativePath, "swagger.g.json");
return Path.GetFullPath(outputPath);
}
- private static void AssertHasSwaggerDocumentOutputPath([SysNotNull] string? relativePath)
- {
- if (relativePath is null)
- {
- throw new Exception($"Property '{nameof(OpenApiTestContext