Skip to content

Commit

Permalink
Merge pull request #2117 from microsoft/fix/no-more-nullable-property
Browse files Browse the repository at this point in the history
fix/no more nullable property
  • Loading branch information
andrueastman authored Feb 4, 2025
2 parents 6b636d5 + 2f171a3 commit 4821b92
Show file tree
Hide file tree
Showing 18 changed files with 127 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,6 @@ private static void CopySchema(OpenApiSchema schema, OpenApiSchema newSchema)
schema.Enum ??= newSchema.Enum;
schema.ReadOnly = !schema.ReadOnly ? newSchema.ReadOnly : schema.ReadOnly;
schema.WriteOnly = !schema.WriteOnly ? newSchema.WriteOnly : schema.WriteOnly;
schema.Nullable = !schema.Nullable ? newSchema.Nullable : schema.Nullable;
schema.Deprecated = !schema.Deprecated ? newSchema.Deprecated : schema.Deprecated;
}
}
Expand Down
84 changes: 42 additions & 42 deletions src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,19 @@ public static JsonSchemaType ToJsonSchemaType(this string identifier)
[typeof(char)] = () => new() { Type = JsonSchemaType.String },

// Nullable types
[typeof(bool?)] = () => new() { Type = JsonSchemaType.Boolean, Nullable = true },
[typeof(byte?)] = () => new() { Type = JsonSchemaType.String, Format = "byte", Nullable = true },
[typeof(int?)] = () => new() { Type = JsonSchemaType.Integer, Format = "int32", Nullable = true },
[typeof(uint?)] = () => new() { Type = JsonSchemaType.Integer, Format = "int32", Nullable = true },
[typeof(long?)] = () => new() { Type = JsonSchemaType.Integer, Format = "int64", Nullable = true },
[typeof(ulong?)] = () => new() { Type = JsonSchemaType.Integer, Format = "int64", Nullable = true },
[typeof(float?)] = () => new() { Type = JsonSchemaType.Number, Format = "float", Nullable = true },
[typeof(double?)] = () => new() { Type = JsonSchemaType.Number, Format = "double", Nullable = true },
[typeof(decimal?)] = () => new() { Type = JsonSchemaType.Number, Format = "double", Nullable = true },
[typeof(DateTime?)] = () => new() { Type = JsonSchemaType.String, Format = "date-time", Nullable = true },
[typeof(DateTimeOffset?)] = () => new() { Type = JsonSchemaType.String, Format = "date-time", Nullable = true },
[typeof(Guid?)] = () => new() { Type = JsonSchemaType.String, Format = "uuid", Nullable = true },
[typeof(char?)] = () => new() { Type = JsonSchemaType.String, Nullable = true },
[typeof(bool?)] = () => new() { Type = JsonSchemaType.Boolean | JsonSchemaType.Null },
[typeof(byte?)] = () => new() { Type = JsonSchemaType.String | JsonSchemaType.Null, Format = "byte" },
[typeof(int?)] = () => new() { Type = JsonSchemaType.Integer | JsonSchemaType.Null, Format = "int32" },
[typeof(uint?)] = () => new() { Type = JsonSchemaType.Integer | JsonSchemaType.Null, Format = "int32" },
[typeof(long?)] = () => new() { Type = JsonSchemaType.Integer | JsonSchemaType.Null, Format = "int64" },
[typeof(ulong?)] = () => new() { Type = JsonSchemaType.Integer | JsonSchemaType.Null, Format = "int64" },
[typeof(float?)] = () => new() { Type = JsonSchemaType.Number | JsonSchemaType.Null, Format = "float" },
[typeof(double?)] = () => new() { Type = JsonSchemaType.Number | JsonSchemaType.Null, Format = "double" },
[typeof(decimal?)] = () => new() { Type = JsonSchemaType.Number | JsonSchemaType.Null, Format = "double" },
[typeof(DateTime?)] = () => new() { Type = JsonSchemaType.String | JsonSchemaType.Null, Format = "date-time" },
[typeof(DateTimeOffset?)] = () => new() { Type = JsonSchemaType.String | JsonSchemaType.Null, Format = "date-time" },
[typeof(Guid?)] = () => new() { Type = JsonSchemaType.String | JsonSchemaType.Null, Format = "uuid" },
[typeof(char?)] = () => new() { Type = JsonSchemaType.String | JsonSchemaType.Null },

[typeof(Uri)] = () => new() { Type = JsonSchemaType.String, Format = "uri" }, // Uri is treated as simple string
[typeof(string)] = () => new() { Type = JsonSchemaType.String },
Expand Down Expand Up @@ -153,37 +153,37 @@ public static Type MapOpenApiPrimitiveTypeToSimpleType(this OpenApiSchema schema
throw new ArgumentNullException(nameof(schema));
}

var type = (schema.Type.ToIdentifier(), schema.Format?.ToLowerInvariant(), schema.Nullable) switch
var type = ((schema.Type & ~JsonSchemaType.Null).ToIdentifier(), schema.Format?.ToLowerInvariant(), schema.Type & JsonSchemaType.Null) switch
{
("boolean", null, false) => typeof(bool),
("integer" or "number", "int32", JsonSchemaType.Null) => typeof(int?),
("integer" or "number", "int64", JsonSchemaType.Null) => typeof(long?),
("integer", null, JsonSchemaType.Null) => typeof(long?),
("number", "float", JsonSchemaType.Null) => typeof(float?),
("number", "double", JsonSchemaType.Null) => typeof(double?),
("number", null, JsonSchemaType.Null) => typeof(double?),
("number", "decimal", JsonSchemaType.Null) => typeof(decimal?),
("string", "byte", JsonSchemaType.Null) => typeof(byte?),
("string", "date-time", JsonSchemaType.Null) => typeof(DateTimeOffset?),
("string", "uuid", JsonSchemaType.Null) => typeof(Guid?),
("string", "char", JsonSchemaType.Null) => typeof(char?),
("boolean", null, JsonSchemaType.Null) => typeof(bool?),
("boolean", null, _) => typeof(bool),
// integer is technically not valid with format, but we must provide some compatibility
("integer" or "number", "int32", false) => typeof(int),
("integer" or "number", "int64", false) => typeof(long),
("integer", null, false) => typeof(long),
("number", "float", false) => typeof(float),
("number", "double", false) => typeof(double),
("number", "decimal", false) => typeof(decimal),
("number", null, false) => typeof(double),
("string", "byte", false) => typeof(byte),
("string", "date-time", false) => typeof(DateTimeOffset),
("string", "uuid", false) => typeof(Guid),
("string", "duration", false) => typeof(TimeSpan),
("string", "char", false) => typeof(char),
("string", null, false) => typeof(string),
("object", null, false) => typeof(object),
("string", "uri", false) => typeof(Uri),
("integer" or "number", "int32", true) => typeof(int?),
("integer" or "number", "int64", true) => typeof(long?),
("integer", null, true) => typeof(long?),
("number", "float", true) => typeof(float?),
("number", "double", true) => typeof(double?),
("number", null, true) => typeof(double?),
("number", "decimal", true) => typeof(decimal?),
("string", "byte", true) => typeof(byte?),
("string", "date-time", true) => typeof(DateTimeOffset?),
("string", "uuid", true) => typeof(Guid?),
("string", "char", true) => typeof(char?),
("boolean", null, true) => typeof(bool?),
("integer" or "number", "int32", _) => typeof(int),
("integer" or "number", "int64", _) => typeof(long),
("integer", null, _) => typeof(long),
("number", "float", _) => typeof(float),
("number", "double", _) => typeof(double),
("number", "decimal", _) => typeof(decimal),
("number", null, _) => typeof(double),
("string", "byte", _) => typeof(byte),
("string", "date-time", _) => typeof(DateTimeOffset),
("string", "uuid", _) => typeof(Guid),
("string", "duration", _) => typeof(TimeSpan),
("string", "char", _) => typeof(char),
("string", null, _) => typeof(string),
("object", null, _) => typeof(object),
("string", "uri", _) => typeof(Uri),
_ => typeof(string),
};

Expand Down
5 changes: 0 additions & 5 deletions src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,11 +267,6 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiSerializable
/// </summary>
public IList<JsonNode> Enum { get; }

/// <summary>
/// Allows sending a null value for the defined schema. Default value is false.
/// </summary>
public bool Nullable { get; }

/// <summary>
/// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00
/// </summary>
Expand Down
18 changes: 3 additions & 15 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,6 @@ public class OpenApiSchema : IOpenApiReferenceable, IOpenApiExtensible, IOpenApi
/// <inheritdoc />
public IList<JsonNode> Enum { get; set; } = new List<JsonNode>();

/// <inheritdoc />
public bool Nullable { get; set; }

/// <inheritdoc />
public bool UnevaluatedProperties { get; set;}

Expand Down Expand Up @@ -236,7 +233,6 @@ internal OpenApiSchema(IOpenApiSchema schema)
Example = schema.Example != null ? JsonNodeCloneHelper.Clone(schema.Example) : null;
Examples = schema.Examples != null ? new List<JsonNode>(schema.Examples) : null;
Enum = schema.Enum != null ? new List<JsonNode>(schema.Enum) : null;
Nullable = schema.Nullable;
ExternalDocs = schema.ExternalDocs != null ? new(schema.ExternalDocs) : null;
Deprecated = schema.Deprecated;
Xml = schema.Xml != null ? new(schema.Xml) : null;
Expand Down Expand Up @@ -633,8 +629,7 @@ private void SerializeAsV2(
private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer, OpenApiSpecVersion version)
{
// check whether nullable is true for upcasting purposes
var isNullable = Nullable ||
(Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) ||
var isNullable = (Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) ||
Extensions is not null &&
Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) &&
nullExtRawValue is OpenApiAny { Node: JsonNode jsonNode} &&
Expand Down Expand Up @@ -679,10 +674,6 @@ Extensions is not null &&
var list = (from JsonSchemaType flag in jsonSchemaTypeValues
where type.Value.HasFlag(flag)
select flag).ToList();
if (Nullable && !list.Contains(JsonSchemaType.Null))
{
list.Add(JsonSchemaType.Null);
}
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s.ToIdentifier()));
}
}
Expand Down Expand Up @@ -735,7 +726,7 @@ private void DowncastTypeArrayToV2OrV3(JsonSchemaType schemaType, IOpenApiWriter
? OpenApiConstants.NullableExtension
: OpenApiConstants.Nullable;

if (!HasMultipleTypes(schemaType ^ JsonSchemaType.Null) && (schemaType & JsonSchemaType.Null) == JsonSchemaType.Null) // checks for two values and one is null
if (!HasMultipleTypes(schemaType & ~JsonSchemaType.Null) && (schemaType & JsonSchemaType.Null) == JsonSchemaType.Null) // checks for two values and one is null
{
foreach (JsonSchemaType flag in jsonSchemaTypeValues)
{
Expand All @@ -746,10 +737,7 @@ private void DowncastTypeArrayToV2OrV3(JsonSchemaType schemaType, IOpenApiWriter
writer.WriteProperty(OpenApiConstants.Type, flag.ToIdentifier());
}
}
if (!Nullable)
{
writer.WriteProperty(nullableProp, true);
}
writer.WriteProperty(nullableProp, true);
}
else if (!HasMultipleTypes(schemaType))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,6 @@ public string Description
/// <inheritdoc/>
public IList<JsonNode> Enum { get => Target?.Enum; }
/// <inheritdoc/>
public bool Nullable { get => Target?.Nullable ?? false; }
/// <inheritdoc/>
public bool UnevaluatedProperties { get => Target?.UnevaluatedProperties ?? false; }
/// <inheritdoc/>
public OpenApiExternalDocs ExternalDocs { get => Target?.ExternalDocs; }
Expand Down
20 changes: 18 additions & 2 deletions src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,14 @@ internal static partial class OpenApiV3Deserializer
},
{
"type",
(o, n, _) => o.Type = n.GetScalarValue().ToJsonSchemaType()
(o, n, _) => {
var type = n.GetScalarValue().ToJsonSchemaType();
// so we don't loose the value from nullable
if (o.Type.HasValue)
o.Type |= type;
else
o.Type = type;
}
},
{
"allOf",
Expand Down Expand Up @@ -139,7 +146,16 @@ internal static partial class OpenApiV3Deserializer
},
{
"nullable",
(o, n, _) => o.Nullable = bool.Parse(n.GetScalarValue())
(o, n, _) =>
{
if (bool.TryParse(n.GetScalarValue(), out var parsed) && parsed)
{
if (o.Type.HasValue)
o.Type |= JsonSchemaType.Null;
else
o.Type = JsonSchemaType.Null;
}
}
},
{
"discriminator",
Expand Down
3 changes: 1 addition & 2 deletions src/Microsoft.OpenApi/Validations/Rules/RuleHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,10 @@ public static void ValidateDataTypeMismatch(

var type = schema.Type.ToIdentifier();
var format = schema.Format;
var nullable = schema.Nullable;

// Before checking the type, check first if the schema allows null.
// If so and the data given is also null, this is allowed for any type.
if (nullable && valueKind is JsonValueKind.Null)
if ((schema.Type.Value & JsonSchemaType.Null) is JsonSchemaType.Null && valueKind is JsonValueKind.Null)
{
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,23 @@ public void RemoveAnyOfAndOneOfFromSchema()
var walker = new OpenApiWalker(powerShellFormatter);
walker.Walk(openApiDocument);

var testSchema = openApiDocument.Components?.Schemas?["TestSchema"];
var averageAudioDegradationProperty = testSchema?.Properties["averageAudioDegradation"];
var defaultPriceProperty = testSchema?.Properties["defaultPrice"];
Assert.NotNull(openApiDocument.Components);
Assert.NotNull(openApiDocument.Components.Schemas);
var testSchema = openApiDocument.Components.Schemas["TestSchema"];
var averageAudioDegradationProperty = testSchema.Properties["averageAudioDegradation"];
var defaultPriceProperty = testSchema.Properties["defaultPrice"];

// Assert
Assert.NotNull(openApiDocument.Components);
Assert.NotNull(openApiDocument.Components.Schemas);
Assert.NotNull(testSchema);
Assert.Null(averageAudioDegradationProperty?.AnyOf);
Assert.Equal(JsonSchemaType.Number, averageAudioDegradationProperty?.Type);
Assert.Equal("float", averageAudioDegradationProperty?.Format);
Assert.True(averageAudioDegradationProperty?.Nullable);
Assert.Null(defaultPriceProperty?.OneOf);
Assert.Equal(JsonSchemaType.Number, defaultPriceProperty?.Type);
Assert.Equal("double", defaultPriceProperty?.Format);
Assert.Null(averageAudioDegradationProperty.AnyOf);
Assert.Equal(JsonSchemaType.Number | JsonSchemaType.Null, averageAudioDegradationProperty.Type);
Assert.Equal("float", averageAudioDegradationProperty.Format);
Assert.Equal(JsonSchemaType.Null, averageAudioDegradationProperty.Type & JsonSchemaType.Null);
Assert.Null(defaultPriceProperty.OneOf);
Assert.Equal(JsonSchemaType.Number, defaultPriceProperty.Type);
Assert.Equal("double", defaultPriceProperty.Format);
Assert.NotNull(testSchema.AdditionalProperties);
}

Expand Down Expand Up @@ -161,11 +163,10 @@ private static OpenApiDocument GetSampleOpenApiDocument()
{
AnyOf = new List<IOpenApiSchema>
{
new OpenApiSchema() { Type = JsonSchemaType.Number },
new OpenApiSchema() { Type = JsonSchemaType.Number | JsonSchemaType.Null },
new OpenApiSchema() { Type = JsonSchemaType.String }
},
Format = "float",
Nullable = true
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,14 +377,7 @@ public static OpenApiDocument CreateOpenApiDocument()
{
Schema = new OpenApiSchema()
{
AnyOf = new List<IOpenApiSchema>
{
new OpenApiSchema()
{
Type = JsonSchemaType.String
}
},
Nullable = true
Type = JsonSchemaType.String | JsonSchemaType.Null
}
}
}
Expand Down Expand Up @@ -627,9 +620,8 @@ public static OpenApiDocument CreateOpenApiDocument()
{
"description", new OpenApiSchema
{
Type = JsonSchemaType.String,
Type = JsonSchemaType.String | JsonSchemaType.Null,
Description = "Description of the NIC (e.g. Ethernet adapter, Wireless LAN adapter Local Area Connection <#>, etc.).",
Nullable = true
}
}
}
Expand Down
Loading

0 comments on commit 4821b92

Please # to comment.