Skip to content
7 changes: 6 additions & 1 deletion src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Interfaces;

Expand Down Expand Up @@ -299,4 +299,9 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiSerializable
/// Annotations are NOT (de)serialized with the schema and can be used for custom properties.
/// </summary>
public IDictionary<string, object> Annotations { get; }

/// <summary>
/// Follow JSON Schema definition:https://tools.ietf.org/html/draft-handrews-json-schema-validation-02
/// </summary>
public IDictionary<string, ISet<string>> DependentRequired { get; }
}
5 changes: 5 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -693,17 +693,17 @@
/// <summary>
/// Field: V3 JsonSchema Reference Uri
/// </summary>
public const string V3ReferenceUri = "https://registry/components/schemas/";

Check warning on line 696 in src/Microsoft.OpenApi/Models/OpenApiConstants.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor your code not to use hardcoded absolute paths or URIs. (https://rules.sonarsource.com/csharp/RSPEC-1075)

/// <summary>
/// Field: V2 JsonSchema Reference Uri
/// </summary>
public const string V2ReferenceUri = "https://registry/definitions/";

Check warning on line 701 in src/Microsoft.OpenApi/Models/OpenApiConstants.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor your code not to use hardcoded absolute paths or URIs. (https://rules.sonarsource.com/csharp/RSPEC-1075)

/// <summary>
/// The default registry uri for OpenApi documents and workspaces
/// </summary>
public const string BaseRegistryUri = "https://openapi.net/";

Check warning on line 706 in src/Microsoft.OpenApi/Models/OpenApiConstants.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor your code not to use hardcoded absolute paths or URIs. (https://rules.sonarsource.com/csharp/RSPEC-1075)

/// <summary>
/// The components path segment in a $ref value.
Expand All @@ -720,6 +720,11 @@
/// </summary>
public const string NullableExtension = "x-nullable";

/// <summary>
/// Field: DependentRequired
/// </summary>
public const string DependentRequired = "dependentRequired";

#region V2.0

/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ public class OpenApiSchema : IOpenApiReferenceable, IOpenApiExtensible, IOpenApi
/// <inheritdoc />
public IDictionary<string, object> Annotations { get; set; }

/// <inheritdoc />
public IDictionary<string, ISet<string>> DependentRequired { get; set; } = new Dictionary<string, ISet<string>>();

/// <summary>
/// Parameterless constructor
/// </summary>
Expand Down Expand Up @@ -239,6 +242,7 @@ internal OpenApiSchema(IOpenApiSchema schema)
Extensions = schema.Extensions != null ? new Dictionary<string, IOpenApiExtension>(schema.Extensions) : null;
Annotations = schema.Annotations != null ? new Dictionary<string, object>(schema.Annotations) : null;
UnrecognizedKeywords = schema.UnrecognizedKeywords != null ? new Dictionary<string, JsonNode>(schema.UnrecognizedKeywords) : null;
DependentRequired = schema.DependentRequired != null ? new Dictionary<string, ISet<string>>(schema.DependentRequired) : null;
}

/// <inheritdoc />
Expand Down Expand Up @@ -408,6 +412,7 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
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));
writer.WriteOptionalMap(OpenApiConstants.DependentRequired, DependentRequired, (w, s) => w.WriteValue(s));
}

internal void WriteAsItemsProperties(IOpenApiWriter writer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ public string Description
/// <inheritdoc/>
public IDictionary<string, object> Annotations { get => Target?.Annotations; }

/// <inheritdoc/>
public IDictionary<string, ISet<string>> DependentRequired { get => Target?.DependentRequired; }

/// <inheritdoc/>
public override void SerializeAsV31(IOpenApiWriter writer)
{
Expand Down
28 changes: 28 additions & 0 deletions src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,34 @@ public override Dictionary<string, T> CreateSimpleMap<T>(Func<ValueNode, T> map)
return nodes.ToDictionary(k => k.key, v => v.value);
}

public override Dictionary<string, ISet<T>> CreateArrayMap<T>(Func<ValueNode, OpenApiDocument, T> map, OpenApiDocument openApiDocument)
{
var jsonMap = _node ?? throw new OpenApiReaderException($"Expected map while parsing {typeof(T).Name}", Context);

var nodes = jsonMap.Select(n =>
{
var key = n.Key;
try
{
Context.StartObject(key);
JsonArray arrayNode = n.Value is JsonArray value
? value
: throw new OpenApiReaderException($"Expected array while parsing {typeof(T).Name}", Context);

ISet<T> values = new HashSet<T>(arrayNode.Select(item => map(new ValueNode(Context, item), openApiDocument)));

return (key, values);

}
finally
{
Context.EndObject();
}
});

return nodes.ToDictionary(kvp => kvp.key, kvp => kvp.values);
}

public IEnumerator<PropertyNode> GetEnumerator()
{
return _nodes.GetEnumerator();
Expand Down
7 changes: 6 additions & 1 deletion src/Microsoft.OpenApi/Reader/ParseNodes/ParseNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

public MapNode CheckMapNode(string nodeName)
{
if (this is not MapNode mapNode)

Check warning on line 26 in src/Microsoft.OpenApi/Reader/ParseNodes/ParseNode.cs

View workflow job for this annotation

GitHub Actions / Build

Offload the code that's conditional on this type test to the appropriate subclass and remove the condition. (https://rules.sonarsource.com/csharp/RSPEC-3060)
{
throw new OpenApiReaderException($"{nodeName} must be a map/object", Context);
}
Expand Down Expand Up @@ -84,6 +84,11 @@
public virtual List<JsonNode> CreateListOfAny()
{
throw new OpenApiReaderException("Cannot create a list from this type of node.", Context);
}
}

public virtual Dictionary<string, ISet<T>> CreateArrayMap<T>(Func<ValueNode, OpenApiDocument, T> map, OpenApiDocument openApiDocument)
{
throw new OpenApiReaderException("Cannot create array map from this type of node.", Context);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using Microsoft.OpenApi.Extensions;
Expand Down Expand Up @@ -234,6 +234,13 @@ internal static partial class OpenApiV31Deserializer
"deprecated",
(o, n, _) => o.Deprecated = bool.Parse(n.GetScalarValue())
},
{
"dependentRequired",
(o, n, doc) =>
{
o.DependentRequired = n.CreateArrayMap((n2, p) => n2.GetScalarValue(), doc);
}
},
};

private static readonly PatternFieldMap<OpenApiSchema> _openApiSchemaPatternFields = new()
Expand Down
20 changes: 20 additions & 0 deletions src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// Licensed under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.OpenApi.Exceptions;
Expand Down Expand Up @@ -136,7 +138,7 @@
/// Write float value.
/// </summary>
/// <param name="value">The float value.</param>
public virtual void WriteValue(float value)

Check warning on line 141 in src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs

View workflow job for this annotation

GitHub Actions / Build

All 'WriteValue' method overloads should be adjacent. (https://rules.sonarsource.com/csharp/RSPEC-4136)
{
WriteValueSeparator();
Writer.Write(value);
Expand Down Expand Up @@ -210,11 +212,25 @@
Writer.Write(value.ToString().ToLower());
}

/// <summary>
/// Write hashSet value.
/// </summary>
/// <param name="value">The HashSet value.</param>
public virtual void WriteHashSet(IEnumerable<object> value)
{
WriteStartArray();
foreach (var item in value)
{
WriteValue(item);
}
WriteEndArray();
}

/// <summary>
/// Write object value.
/// </summary>
/// <param name="value">The object value.</param>
public virtual void WriteValue(object value)

Check warning on line 233 in src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
if (value == null)
{
Expand Down Expand Up @@ -264,6 +280,10 @@
{
WriteValue((DateTimeOffset)value);
}
else if (value is IEnumerable<object> hashSet && value.GetType().GetGenericTypeDefinition() == typeof(HashSet<>))
{
WriteHashSet(hashSet);
}
else
{
throw new OpenApiWriterException(string.Format(SRResource.OpenApiUnsupportedValueType, type.FullName));
Expand Down
19 changes: 19 additions & 0 deletions src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
/// <param name="writer">The writer.</param>
/// <param name="name">The property name.</param>
/// <param name="value">The property value.</param>
public static void WriteProperty(this IOpenApiWriter writer, string name, string value)

Check warning on line 24 in src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs

View workflow job for this annotation

GitHub Actions / Build

All 'WriteProperty' method overloads should be adjacent. (https://rules.sonarsource.com/csharp/RSPEC-4136)
{
if (value == null)
{
Expand Down Expand Up @@ -141,7 +141,7 @@
T? value,
Action<IOpenApiWriter, T> action)
{
if (value != null)

Check warning on line 144 in src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs

View workflow job for this annotation

GitHub Actions / Build

Use a comparison to 'default(T)' instead or add a constraint to 'T' so that it can't be a value type. (https://rules.sonarsource.com/csharp/RSPEC-2955)
{
if (value is IEnumerable values && !values.GetEnumerator().MoveNext())
{
Expand Down Expand Up @@ -169,7 +169,7 @@
Utils.CheckArgumentNull(action);

writer.WritePropertyName(name);
if (value != null)

Check warning on line 172 in src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs

View workflow job for this annotation

GitHub Actions / Build

Use a comparison to 'default(T)' instead or add a constraint to 'T' so that it can't be a value type. (https://rules.sonarsource.com/csharp/RSPEC-2955)
{
action(writer, value);
}
Expand Down Expand Up @@ -245,7 +245,7 @@
/// <param name="name">The property name.</param>
/// <param name="elements">The map values.</param>
/// <param name="action">The map element writer action.</param>
public static void WriteRequiredMap(

Check warning on line 248 in src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs

View workflow job for this annotation

GitHub Actions / Build

All 'WriteRequiredMap' method overloads should be adjacent. (https://rules.sonarsource.com/csharp/RSPEC-4136)
this IOpenApiWriter writer,
string name,
IDictionary<string, string> elements,
Expand Down Expand Up @@ -311,6 +311,25 @@
}
}

/// <summary>
/// Write the optional Open API element map (string to array mapping).
/// </summary>
/// <param name="writer">The Open API writer.</param>
/// <param name="name">The property name.</param>
/// <param name="elements">The map values.</param>
/// <param name="action">The map element writer action.</param>
public static void WriteOptionalMap(
this IOpenApiWriter writer,
string name,
IDictionary<string, ISet<string>> elements,
Action<IOpenApiWriter, ISet<string>> action)
{
if (elements != null && elements.Any())
{
writer.WriteMapInternal(name, elements, action);
}
}

/// <summary>
/// Write the optional Open API element map.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public async Task ParseDocumentWithWebhooksShouldSucceed()
"id",
"name"
},
DependentRequired = new Dictionary<string, ISet<string>>
{
{ "tag", new HashSet<string> { "category" } }
},
Properties = new Dictionary<string, IOpenApiSchema>
{
["id"] = new OpenApiSchema()
Expand All @@ -61,6 +65,10 @@ public async Task ParseDocumentWithWebhooksShouldSucceed()
{
Type = JsonSchemaType.String
},
["category"] = new OpenApiSchema()
{
Type = JsonSchemaType.String,
},
}
},
["newPetSchema"] = new OpenApiSchema()
Expand All @@ -70,6 +78,10 @@ public async Task ParseDocumentWithWebhooksShouldSucceed()
{
"name"
},
DependentRequired = new Dictionary<string, ISet<string>>
{
{ "tag", new HashSet<string> { "category" } }
},
Properties = new Dictionary<string, IOpenApiSchema>
{
["id"] = new OpenApiSchema()
Expand All @@ -85,6 +97,10 @@ public async Task ParseDocumentWithWebhooksShouldSucceed()
{
Type = JsonSchemaType.String
},
["category"] = new OpenApiSchema()
{
Type = JsonSchemaType.String,
},
}
}
}
Expand Down Expand Up @@ -222,6 +238,10 @@ public async Task ParseDocumentsWithReusablePathItemInWebhooksSucceeds()
"id",
"name"
},
DependentRequired = new Dictionary<string, ISet<string>>
{
{ "tag", new HashSet<string> { "category" } }
},
Properties = new Dictionary<string, IOpenApiSchema>
{
["id"] = new OpenApiSchema()
Expand All @@ -237,6 +257,10 @@ public async Task ParseDocumentsWithReusablePathItemInWebhooksSucceeds()
{
Type = JsonSchemaType.String
},
["category"] = new OpenApiSchema()
{
Type = JsonSchemaType.String,
},
}
},
["newPetSchema"] = new OpenApiSchema()
Expand All @@ -246,6 +270,10 @@ public async Task ParseDocumentsWithReusablePathItemInWebhooksSucceeds()
{
"name"
},
DependentRequired = new Dictionary<string, ISet<string>>
{
{ "tag", new HashSet<string> { "category" } }
},
Properties = new Dictionary<string, IOpenApiSchema>
{
["id"] = new OpenApiSchema()
Expand All @@ -261,6 +289,10 @@ public async Task ParseDocumentsWithReusablePathItemInWebhooksSucceeds()
{
Type = JsonSchemaType.String
},
["category"] = new OpenApiSchema()
{
Type = JsonSchemaType.String,
},
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ public async Task ParseBasicV31SchemaShouldSucceed()
"veggieName",
"veggieLike"
},
DependentRequired = new Dictionary<string, ISet<string>>
{
{ "veggieType", new HashSet<string> { "veggieColor", "veggieSize" } }
},
Properties = new Dictionary<string, IOpenApiSchema>
{
["veggieName"] = new OpenApiSchema
Expand All @@ -79,6 +83,21 @@ public async Task ParseBasicV31SchemaShouldSucceed()
{
Type = JsonSchemaType.Boolean,
Description = "Do I like this vegetable?"
},
["veggieType"] = new OpenApiSchema
{
Type = JsonSchemaType.String,
Description = "The type of vegetable (e.g., root, leafy, etc.)."
},
["veggieColor"] = new OpenApiSchema
{
Type = JsonSchemaType.String,
Description = "The color of the vegetable."
},
["veggieSize"] = new OpenApiSchema
{
Type = JsonSchemaType.String,
Description = "The size of the vegetable."
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ components:
required:
- id
- name
dependentRequired:
tag:
- category
properties:
id:
type: integer
Expand All @@ -21,10 +24,15 @@ components:
type: string
tag:
type: string
category:
type: string
newPetSchema:
type: object
required:
- name
dependentRequired:
tag:
- category
properties:
id:
type: integer
Expand All @@ -33,6 +41,8 @@ components:
type: string
tag:
type: string
category:
type: string
pathItems:
pets:
get:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ components:
required:
- id
- name
dependentRequired:
tag:
- category
properties:
id:
type: integer
Expand All @@ -67,15 +70,22 @@ components:
type: string
tag:
type: string
category:
type: string
newPetSchema:
type: object
required:
- name
dependentRequired:
tag:
- category
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
category:
type: string
Loading