diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 340f52a..ed6876d 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -8,7 +8,7 @@
"rollForward": false
},
"swashbuckle.aspnetcore.cli": {
- "version": "7.0.0",
+ "version": "10.2.1",
"commands": ["swagger"],
"rollForward": false
}
diff --git a/examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj b/examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj
index 6bcb941..320edab 100644
--- a/examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj
+++ b/examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net10.0
enable
enable
@@ -10,19 +10,21 @@
-
+
-
+
-
-
+
+
-
+
\ No newline at end of file
diff --git a/examples/OptionalValues.Examples.Swashbuckle/Program.cs b/examples/OptionalValues.Examples.Swashbuckle/Program.cs
index 824292b..0e4726c 100644
--- a/examples/OptionalValues.Examples.Swashbuckle/Program.cs
+++ b/examples/OptionalValues.Examples.Swashbuckle/Program.cs
@@ -1,4 +1,4 @@
-using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi;
using OptionalValues;
using OptionalValues.DataAnnotations;
@@ -9,6 +9,7 @@
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "OptionalValues.Examples.Swashbuckle", Version = "1.0" });
+ options.SchemaGeneratorOptions.SupportNonNullableReferenceTypes = true;
});
// Add OptionalValue support to Swashbuckle
builder.Services.AddSwaggerGenOptionalValueSupport();
diff --git a/examples/OptionalValues.Examples.Swashbuckle/openapi.yaml b/examples/OptionalValues.Examples.Swashbuckle/openapi.yaml
index 8650523..2edb425 100644
--- a/examples/OptionalValues.Examples.Swashbuckle/openapi.yaml
+++ b/examples/OptionalValues.Examples.Swashbuckle/openapi.yaml
@@ -1,4 +1,4 @@
-openapi: 3.0.4
+openapi: '3.1.1'
info:
title: OptionalValues.Examples.Swashbuckle
version: '1.0'
@@ -31,7 +31,6 @@ components:
properties:
street:
type: string
- nullable: true
city:
type: string
state:
@@ -39,7 +38,6 @@ components:
type: string
zip:
type: string
- nullable: true
additionalProperties: false
Company:
required:
@@ -55,7 +53,6 @@ components:
maxLength: 50
minLength: 0
type: string
- nullable: true
contact:
$ref: '#/components/schemas/Person'
additionalProperties: false
@@ -64,7 +61,6 @@ components:
properties:
name:
type: string
- nullable: true
age:
maximum: 120
minimum: 0
@@ -72,4 +68,6 @@ components:
format: int32
address:
$ref: '#/components/schemas/Address'
- additionalProperties: false
\ No newline at end of file
+ additionalProperties: false
+tags:
+ - name: Example
\ No newline at end of file
diff --git a/src/OptionalValues.Swashbuckle/OptionalValueDataContractResolver.cs b/src/OptionalValues.Swashbuckle/OptionalValueDataContractResolver.cs
index 18f962d..179ea75 100644
--- a/src/OptionalValues.Swashbuckle/OptionalValueDataContractResolver.cs
+++ b/src/OptionalValues.Swashbuckle/OptionalValueDataContractResolver.cs
@@ -44,60 +44,76 @@ public virtual DataContract GetDataContractForType(Type type)
}
DataContract? dataContract = _inner.GetDataContractForType(effectiveType);
- if (dataContract is { DataType: DataType.Object })
+ if (dataContract is not
+ { DataType: DataType.Object })
{
- var effectiveProperties = new List();
- foreach (DataProperty property in dataContract.ObjectProperties)
- {
- DataProperty effectiveProperty = property;
- if (OptionalValue.IsOptionalValueType(property.MemberType))
- {
- Type underLyingType = OptionalValue.GetUnderlyingType(property.MemberType);
- var isNullable = Nullable.GetUnderlyingType(underLyingType) != null || GetNullabilityFromRuntimeInformationFlags(property.MemberInfo);
+ return dataContract;
+ }
- var isRequired = property.MemberInfo.GetCustomAttributes(true).Any(a => a.GetType().FullName == "OptionalValues.DataAnnotations.SpecifiedAttribute");
- effectiveProperty = new DataProperty(property.Name, property.MemberType, isRequired, isNullable, property.IsReadOnly, property.IsWriteOnly, property.MemberInfo);
- }
+ var effectiveProperties = new List();
+ foreach (DataProperty property in dataContract.ObjectProperties)
+ {
+ DataProperty effectiveProperty = property;
+ if (OptionalValue.IsOptionalValueType(property.MemberType))
+ {
+ Type underLyingType = OptionalValue.GetUnderlyingType(property.MemberType);
+ var isNullable = Nullable.GetUnderlyingType(underLyingType) != null
+ || GetOptionalValueIsNullable(property.MemberInfo);
- effectiveProperties.Add(effectiveProperty);
+ var isRequired = property.MemberInfo.GetCustomAttributes(true).Any(a => a.GetType().FullName == "OptionalValues.DataAnnotations.SpecifiedAttribute");
+ effectiveProperty = new DataProperty(property.Name, property.MemberType, isRequired, isNullable, property.IsReadOnly, property.IsWriteOnly, property.MemberInfo);
}
- dataContract = DataContract.ForObject(
- dataContract.UnderlyingType,
- effectiveProperties,
- dataContract.ObjectExtensionDataType,
- dataContract.ObjectTypeNameProperty,
- dataContract.ObjectTypeNameProperty,
- dataContract.JsonConverter);
+ effectiveProperties.Add(effectiveProperty);
}
+ dataContract = DataContract.ForObject(
+ dataContract.UnderlyingType,
+ effectiveProperties,
+ dataContract.ObjectExtensionDataType,
+ dataContract.ObjectTypeNameProperty,
+ dataContract.ObjectTypeNameProperty,
+ dataContract.JsonConverter);
+
return dataContract;
}
- private static bool GetNullabilityFromRuntimeInformationFlags(MemberInfo memberInfo)
+ private static bool GetOptionalValueIsNullable(ICustomAttributeProvider? memberInfo)
{
- NullabilityInfo? nullabilityInfo = GetNullabilityInfo(memberInfo);
+ NullabilityInfo? nullabilityInfo = GetOptionalValueNullabilityInfo(memberInfo);
if (nullabilityInfo == null)
{
return false;
}
- if (OptionalValue.IsOptionalValueType(nullabilityInfo.Type))
+ return nullabilityInfo.ReadState == NullabilityState.Nullable;
+ }
+
+ private static NullabilityInfo? GetOptionalValueNullabilityInfo(ICustomAttributeProvider? memberInfo)
+ {
+ if (memberInfo == null)
+ {
+ return null;
+ }
+
+ NullabilityInfo? nullabilityInfo = GetNullabilityInfo(memberInfo);
+ if (nullabilityInfo == null
+ || !OptionalValue.IsOptionalValueType(nullabilityInfo.Type))
{
- return nullabilityInfo.GenericTypeArguments[0].ReadState == NullabilityState.Nullable;
+ return null;
}
- return nullabilityInfo.ReadState == NullabilityState.Nullable;
+ return nullabilityInfo.GenericTypeArguments[0];
}
- private static NullabilityInfo? GetNullabilityInfo(MemberInfo memberInfo)
+ private static NullabilityInfo? GetNullabilityInfo(ICustomAttributeProvider memberInfo)
{
var nullabilityInfoContext = new NullabilityInfoContext();
-
return memberInfo switch
{
PropertyInfo propertyInfo => nullabilityInfoContext.Create(propertyInfo),
FieldInfo fieldInfo => nullabilityInfoContext.Create(fieldInfo),
+ ParameterInfo parameterInfo => nullabilityInfoContext.Create(parameterInfo),
_ => null,
};
}
diff --git a/test/OptionalValues.Swashbuckle.V10.Tests/SchemaGeneratorTest.cs b/test/OptionalValues.Swashbuckle.V10.Tests/SchemaGeneratorTest.cs
index 09c3fcf..3b1a9ea 100644
--- a/test/OptionalValues.Swashbuckle.V10.Tests/SchemaGeneratorTest.cs
+++ b/test/OptionalValues.Swashbuckle.V10.Tests/SchemaGeneratorTest.cs
@@ -50,16 +50,46 @@ public void Should_Generate_The_Same_Schema_With_OptionalValues()
schemaDefault.Properties?.Count.ShouldBe(4);
}
+ [Fact(Skip = "Skip this test until Swashbuckle does not override nullability for ValueTypes, (that is, OptionalValue)")]
+ public void Should_Generate_The_Same_Schema_For_Nullable_Reference_Types_With_OptionalValues()
+ {
+ var schemaRepositoryForOptionalValues = new SchemaRepository();
+ var schemaRepositoryForDefault = new SchemaRepository();
+
+ IOpenApiSchema schemaOptionalValuesAsRef = SchemaGeneratorOptionalValues.GenerateSchema(typeof(ExamplesOptionalValues.NullableReferences), schemaRepositoryForOptionalValues);
+ schemaOptionalValuesAsRef.ShouldNotBeNull();
+
+ IOpenApiSchema schemaDefaultAsRef = SchemaGeneratorDefault.GenerateSchema(typeof(ExamplesPlain.NullableReferences), schemaRepositoryForDefault);
+ schemaDefaultAsRef.ShouldNotBeNull();
+
+ var refId1 = ((OpenApiSchemaReference)schemaOptionalValuesAsRef).Reference.Id;
+ var refId2 = ((OpenApiSchemaReference)schemaDefaultAsRef).Reference.Id;
+
+ IOpenApiSchema schemaOptionalValues = schemaRepositoryForOptionalValues.Schemas[refId1!];
+ IOpenApiSchema schemaDefault = schemaRepositoryForDefault.Schemas[refId2!];
+
+ var schemaOptionalValuesJson = SerializeSchema(schemaOptionalValues);
+ var schemaDefaultJson = SerializeSchema(schemaDefault);
+
+ schemaOptionalValuesJson.ShouldBe(schemaDefaultJson);
+ schemaOptionalValues.Properties?.Count.ShouldBe(2);
+ schemaDefault.Properties?.Count.ShouldBe(2);
+ }
+
[Fact]
- public void OptionalValue_Support_Should_Not_Change_Behavior()
+ public void OptionalValue_Support_Should_Not_Change_Default_Behavior()
{
var schemaRepositoryForOptionalValues = new SchemaRepository();
var schemaRepositoryForDefault = new SchemaRepository();
- IOpenApiSchema schemaOptionalValuesAsRef = SchemaGeneratorOptionalValues.GenerateSchema(typeof(ExamplesPlain.Primitives), schemaRepositoryForOptionalValues);
+ // Generate the schema for the ExamplesPlain.Primitives type with both SchemaGenerators.
+ // They should generate the same schema, because the OptionalValue support should not change the behavior for types that are not OptionalValue
+ IOpenApiSchema schemaOptionalValuesAsRef = SchemaGeneratorOptionalValues
+ .GenerateSchema(typeof(ExamplesPlain.Primitives), schemaRepositoryForOptionalValues);
schemaOptionalValuesAsRef.ShouldNotBeNull();
- IOpenApiSchema schemaDefaultAsRef = SchemaGeneratorDefault.GenerateSchema(typeof(ExamplesPlain.Primitives), schemaRepositoryForDefault);
+ IOpenApiSchema schemaDefaultAsRef = SchemaGeneratorDefault
+ .GenerateSchema(typeof(ExamplesPlain.Primitives), schemaRepositoryForDefault);
schemaDefaultAsRef.ShouldNotBeNull();
var refId1 = ((OpenApiSchemaReference)schemaOptionalValuesAsRef).Reference.Id;
@@ -92,6 +122,12 @@ public class Primitives
public OptionalValue BoolValue { get; set; }
public OptionalValue GuidValue { get; set; }
}
+
+ public class NullableReferences
+ {
+ public OptionalValue StringValue { get; set; }
+ public OptionalValue NullableStringValue { get; set; }
+ }
}
private static class ExamplesPlain
@@ -103,5 +139,11 @@ public class Primitives
public bool BoolValue { get; set; }
public Guid GuidValue { get; set; }
}
+
+ public class NullableReferences
+ {
+ public string StringValue { get; set; } = null!;
+ public string? NullableStringValue { get; set; }
+ }
}
}