Skip to content

fix: deduplicates exclusive min/max properties in the object model #2172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiSerializable
/// <summary>
/// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00
/// </summary>
public decimal? V31ExclusiveMaximum { get; }
public decimal? ExclusiveMaximum { get; }

/// <summary>
/// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00
/// </summary>
public decimal? V31ExclusiveMinimum { get; }
public decimal? ExclusiveMinimum { get; }

/// <summary>
/// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00
Expand Down Expand Up @@ -90,21 +90,11 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiSerializable
/// </summary>
public decimal? Maximum { get; }

/// <summary>
/// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00
/// </summary>
public bool? ExclusiveMaximum { get; }

/// <summary>
/// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00
/// </summary>
public decimal? Minimum { get; }

/// <summary>
/// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00
/// </summary>
public bool? ExclusiveMinimum { get; }

/// <summary>
/// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00
/// </summary>
Expand Down
170 changes: 137 additions & 33 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,63 @@
/// <inheritdoc />
public IDictionary<string, IOpenApiSchema> Definitions { get; set; }

private decimal? _exclusiveMaximum;
/// <inheritdoc />
public decimal? V31ExclusiveMaximum { get; set; }
public decimal? ExclusiveMaximum
{
get
{
if (_exclusiveMaximum.HasValue)
{
return _exclusiveMaximum;
}
if (IsExclusiveMaximum == true && _maximum.HasValue)
{
return _maximum;
}
return null;
}
set
{
_exclusiveMaximum = value;
IsExclusiveMaximum = value != null;
}
}

/// <summary>
/// Compatibility property for OpenAPI 3.0 or earlier serialization of the exclusive maximum value.
/// </summary>
/// DO NOT CHANGE THE VISIBILITY OF THIS PROPERTY TO PUBLIC
internal bool? IsExclusiveMaximum { get; set; }

private decimal? _exclusiveMinimum;
/// <inheritdoc />
public decimal? V31ExclusiveMinimum { get; set; }
public decimal? ExclusiveMinimum
{
get
{
if (_exclusiveMinimum.HasValue)
{
return _exclusiveMinimum;
}
if (IsExclusiveMinimum == true && _minimum.HasValue)
{
return _minimum;
}
return null;
}
set
{
_exclusiveMinimum = value;
IsExclusiveMinimum = value != null;
}
}

/// <summary>
/// Compatibility property for OpenAPI 3.0 or earlier serialization of the exclusive minimum value.
/// </summary>
/// DO NOT CHANGE THE VISIBILITY OF THIS PROPERTY TO PUBLIC
internal bool? IsExclusiveMinimum { get; set; }

/// <inheritdoc />
public bool UnEvaluatedProperties { get; set; }
Expand All @@ -65,17 +117,42 @@
/// <inheritdoc />
public string Description { get; set; }

private decimal? _maximum;
/// <inheritdoc />
public decimal? Maximum { get; set; }

/// <inheritdoc />
public bool? ExclusiveMaximum { get; set; }
public decimal? Maximum
{
get
{
if (IsExclusiveMaximum == true)
{
return null;
}
return _maximum;
}
set
{
_maximum = value;
}
}

/// <inheritdoc />
public decimal? Minimum { get; set; }
private decimal? _minimum;

/// <inheritdoc />
public bool? ExclusiveMinimum { get; set; }
public decimal? Minimum
{
get
{
if (IsExclusiveMinimum == true)
{
return null;
}
return _minimum;
}
set
{
_minimum = value;
}
}

/// <inheritdoc />
public int? MaxLength { get; set; }
Expand Down Expand Up @@ -188,7 +265,7 @@
/// Initializes a copy of <see cref="IOpenApiSchema"/> object
/// </summary>
/// <param name="schema">The schema object to copy from.</param>
internal OpenApiSchema(IOpenApiSchema schema)

Check warning on line 268 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this constructor to reduce its Cognitive Complexity from 20 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 268 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this constructor to reduce its Cognitive Complexity from 20 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
Utils.CheckArgumentNull(schema);
Title = schema.Title ?? Title;
Expand All @@ -201,15 +278,18 @@
DynamicRef = schema.DynamicRef ?? DynamicRef;
Definitions = schema.Definitions != null ? new Dictionary<string, IOpenApiSchema>(schema.Definitions) : null;
UnevaluatedProperties = schema.UnevaluatedProperties;
V31ExclusiveMaximum = schema.V31ExclusiveMaximum ?? V31ExclusiveMaximum;
V31ExclusiveMinimum = schema.V31ExclusiveMinimum ?? V31ExclusiveMinimum;
ExclusiveMaximum = schema.ExclusiveMaximum ?? ExclusiveMaximum;
ExclusiveMinimum = schema.ExclusiveMinimum ?? ExclusiveMinimum;
if (schema is OpenApiSchema eMSchema)
{
IsExclusiveMaximum = eMSchema.IsExclusiveMaximum;
IsExclusiveMinimum = eMSchema.IsExclusiveMinimum;
}
Type = schema.Type ?? Type;
Format = schema.Format ?? Format;
Description = schema.Description ?? Description;
Maximum = schema.Maximum ?? Maximum;
ExclusiveMaximum = schema.ExclusiveMaximum ?? ExclusiveMaximum;
Minimum = schema.Minimum ?? Minimum;
ExclusiveMinimum = schema.ExclusiveMinimum ?? ExclusiveMinimum;
MaxLength = schema.MaxLength ?? MaxLength;
MinLength = schema.MinLength ?? MinLength;
Pattern = schema.Pattern ?? Pattern;
Expand Down Expand Up @@ -257,6 +337,44 @@
SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer));
}

private static void SerializeBounds(IOpenApiWriter writer, OpenApiSpecVersion version, string propertyName, string exclusivePropertyName, string isExclusivePropertyName, decimal? value, decimal? exclusiveValue, bool? isExclusiveValue)

Check warning on line 340 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Method has 8 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)

Check warning on line 340 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Method has 8 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)
{
if (version >= OpenApiSpecVersion.OpenApi3_1)
{
if (exclusiveValue.HasValue)
{
// was explicitly set in the document or object model
writer.WriteProperty(exclusivePropertyName, exclusiveValue.Value);
}
else if (isExclusiveValue == true && value.HasValue)
{
// came from parsing an old document
writer.WriteProperty(exclusivePropertyName, value);
}
else if (value.HasValue)
{
// was explicitly set in the document or object model
writer.WriteProperty(propertyName, value);
}
}
else
{
if (exclusiveValue.HasValue)
{
// was explicitly set in a new document being downcast or object model
writer.WriteProperty(propertyName, exclusiveValue.Value);
writer.WriteProperty(isExclusivePropertyName, true);
}
else if (value.HasValue)
{
// came from parsing an old document, we're just mirroring the information
writer.WriteProperty(propertyName, value);
if (isExclusiveValue.HasValue)
writer.WriteProperty(isExclusivePropertyName, isExclusiveValue.Value);
}
}
}

private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
Action<IOpenApiWriter, IOpenApiSerializable> callback)
{
Expand All @@ -274,16 +392,12 @@
writer.WriteProperty(OpenApiConstants.MultipleOf, MultipleOf);

// maximum
writer.WriteProperty(OpenApiConstants.Maximum, Maximum);

// exclusiveMaximum
writer.WriteProperty(OpenApiConstants.ExclusiveMaximum, ExclusiveMaximum);
SerializeBounds(writer, version, OpenApiConstants.Maximum, OpenApiConstants.ExclusiveMaximum, OpenApiConstants.V31ExclusiveMaximum, Maximum, ExclusiveMaximum, IsExclusiveMaximum);

// minimum
writer.WriteProperty(OpenApiConstants.Minimum, Minimum);

// exclusiveMinimum
writer.WriteProperty(OpenApiConstants.ExclusiveMinimum, ExclusiveMinimum);
SerializeBounds(writer, version, OpenApiConstants.Minimum, OpenApiConstants.ExclusiveMinimum, OpenApiConstants.V31ExclusiveMinimum, Minimum, ExclusiveMinimum, IsExclusiveMinimum);

// maxLength
writer.WriteProperty(OpenApiConstants.MaxLength, MaxLength);
Expand Down Expand Up @@ -407,8 +521,6 @@
writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV31(w));
writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);
writer.WriteProperty(OpenApiConstants.V31ExclusiveMaximum, V31ExclusiveMaximum);
writer.WriteProperty(OpenApiConstants.V31ExclusiveMinimum, V31ExclusiveMinimum);
writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties, false);
writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (nodeWriter, s) => nodeWriter.WriteAny(s));
writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, (w, s) => s.SerializeAsV31(w));
Expand Down Expand Up @@ -438,16 +550,12 @@
writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d));

// maximum
writer.WriteProperty(OpenApiConstants.Maximum, Maximum);

// exclusiveMaximum
writer.WriteProperty(OpenApiConstants.ExclusiveMaximum, ExclusiveMaximum);
SerializeBounds(writer, OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Maximum, OpenApiConstants.ExclusiveMaximum, OpenApiConstants.V31ExclusiveMaximum, Maximum, ExclusiveMaximum, IsExclusiveMaximum);

// minimum
writer.WriteProperty(OpenApiConstants.Minimum, Minimum);

// exclusiveMinimum
writer.WriteProperty(OpenApiConstants.ExclusiveMinimum, ExclusiveMinimum);
SerializeBounds(writer, OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Minimum, OpenApiConstants.ExclusiveMinimum, OpenApiConstants.V31ExclusiveMinimum, Minimum, ExclusiveMinimum, IsExclusiveMinimum);

// maxLength
writer.WriteProperty(OpenApiConstants.MaxLength, MaxLength);
Expand Down Expand Up @@ -522,16 +630,12 @@
writer.WriteProperty(OpenApiConstants.MultipleOf, MultipleOf);

// maximum
writer.WriteProperty(OpenApiConstants.Maximum, Maximum);

// exclusiveMaximum
writer.WriteProperty(OpenApiConstants.ExclusiveMaximum, ExclusiveMaximum);
SerializeBounds(writer, OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Maximum, OpenApiConstants.ExclusiveMaximum, OpenApiConstants.V31ExclusiveMaximum, Maximum, ExclusiveMaximum, IsExclusiveMaximum);

// minimum
writer.WriteProperty(OpenApiConstants.Minimum, Minimum);

// exclusiveMinimum
writer.WriteProperty(OpenApiConstants.ExclusiveMinimum, ExclusiveMinimum);
SerializeBounds(writer, OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Minimum, OpenApiConstants.ExclusiveMinimum, OpenApiConstants.V31ExclusiveMinimum, Minimum, ExclusiveMinimum, IsExclusiveMinimum);

// maxLength
writer.WriteProperty(OpenApiConstants.MaxLength, MaxLength);
Expand Down Expand Up @@ -631,7 +735,7 @@
writer.WriteEndObject();
}

private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer, OpenApiSpecVersion version)

Check warning on line 738 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 738 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
// check whether nullable is true for upcasting purposes
var isNullable = (Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) ||
Expand Down Expand Up @@ -725,7 +829,7 @@
private static readonly Array jsonSchemaTypeValues = System.Enum.GetValues(typeof(JsonSchemaType));
#endif

private void DowncastTypeArrayToV2OrV3(JsonSchemaType schemaType, IOpenApiWriter writer, OpenApiSpecVersion version)

Check warning on line 832 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'DowncastTypeArrayToV2OrV3' a static method. (https://rules.sonarsource.com/csharp/RSPEC-2325)

Check warning on line 832 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'DowncastTypeArrayToV2OrV3' a static method. (https://rules.sonarsource.com/csharp/RSPEC-2325)
{
/* If the array has one non-null value, emit Type as string
* If the array has one null value, emit x-nullable as true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ public string Description
/// <inheritdoc/>
public IDictionary<string, IOpenApiSchema> Definitions { get => Target?.Definitions; }
/// <inheritdoc/>
public decimal? V31ExclusiveMaximum { get => Target?.V31ExclusiveMaximum; }
public decimal? ExclusiveMaximum { get => Target?.ExclusiveMaximum; }
/// <inheritdoc/>
public decimal? V31ExclusiveMinimum { get => Target?.V31ExclusiveMinimum; }
public decimal? ExclusiveMinimum { get => Target?.ExclusiveMinimum; }
/// <inheritdoc/>
public bool UnEvaluatedProperties { get => Target?.UnEvaluatedProperties ?? false; }
/// <inheritdoc/>
Expand All @@ -80,12 +80,8 @@ public string Description
/// <inheritdoc/>
public decimal? Maximum { get => Target?.Maximum; }
/// <inheritdoc/>
public bool? ExclusiveMaximum { get => Target?.ExclusiveMaximum; }
/// <inheritdoc/>
public decimal? Minimum { get => Target?.Minimum; }
/// <inheritdoc/>
public bool? ExclusiveMinimum { get => Target?.ExclusiveMinimum; }
/// <inheritdoc/>
public int? MaxLength { get => Target?.MaxLength; }
/// <inheritdoc/>
public int? MinLength { get => Target?.MinLength; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@
},
{
"exclusiveMaximum",
(o, n, _) => GetOrCreateSchema(o).ExclusiveMaximum = bool.Parse(n.GetScalarValue())
(o, n, _) => GetOrCreateSchema(o).IsExclusiveMaximum = bool.Parse(n.GetScalarValue())
},
{
"minimum",
(o, n, _) => GetOrCreateSchema(o).Minimum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MinValue)
},
{
"exclusiveMinimum",
(o, n, _) => GetOrCreateSchema(o).ExclusiveMinimum = bool.Parse(n.GetScalarValue())
(o, n, _) => GetOrCreateSchema(o).IsExclusiveMinimum = bool.Parse(n.GetScalarValue())
},
{
"maxLength",
Expand Down Expand Up @@ -102,7 +102,7 @@
{
return p.Schema switch {
OpenApiSchema schema => schema,
_ => (OpenApiSchema)(p.Schema = new OpenApiSchema()),

Check warning on line 105 in src/Microsoft.OpenApi/Reader/V2/OpenApiHeaderDeserializer.cs

View workflow job for this annotation

GitHub Actions / Build

Extract the assignment of 'p.Schema' from this expression. (https://rules.sonarsource.com/csharp/RSPEC-1121)

Check warning on line 105 in src/Microsoft.OpenApi/Reader/V2/OpenApiHeaderDeserializer.cs

View workflow job for this annotation

GitHub Actions / Build

Extract the assignment of 'p.Schema' from this expression. (https://rules.sonarsource.com/csharp/RSPEC-1121)
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ internal static partial class OpenApiV2Deserializer
},
{
"exclusiveMaximum",
(o, n, _) => o.ExclusiveMaximum = bool.Parse(n.GetScalarValue())
(o, n, _) => o.IsExclusiveMaximum = bool.Parse(n.GetScalarValue())
},
{
"minimum",
(o, n, _) => o.Minimum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MinValue)
},
{
"exclusiveMinimum",
(o, n, _) => o.ExclusiveMinimum = bool.Parse(n.GetScalarValue())
(o, n, _) => o.IsExclusiveMinimum = bool.Parse(n.GetScalarValue())
},
{
"maxLength",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ internal static partial class OpenApiV3Deserializer
},
{
"exclusiveMaximum",
(o, n, _) => o.ExclusiveMaximum = bool.Parse(n.GetScalarValue())
(o, n, _) => o.IsExclusiveMaximum = bool.Parse(n.GetScalarValue())
},
{
"minimum",
(o, n, _) => o.Minimum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MinValue)
},
{
"exclusiveMinimum",
(o, n, _) => o.ExclusiveMinimum = bool.Parse(n.GetScalarValue())
(o, n, _) => o.IsExclusiveMinimum = bool.Parse(n.GetScalarValue())
},
{
"maxLength",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ internal static partial class OpenApiV31Deserializer
},
{
"exclusiveMaximum",
(o, n, _) => o.V31ExclusiveMaximum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MaxValue)
(o, n, _) => o.ExclusiveMaximum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MaxValue)
},
{
"minimum",
(o, n, _) => o.Minimum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MinValue)
},
{
"exclusiveMinimum",
(o, n, _) => o.V31ExclusiveMinimum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MaxValue)
(o, n, _) => o.ExclusiveMinimum = ParserHelper.ParseDecimalWithFallbackOnOverflow(n.GetScalarValue(), decimal.MaxValue)
},
{
"maxLength",
Expand Down
Loading