Skip to content

Commit 63b2b98

Browse files
committed
fix: adds a null value sentinel to enable roundtrip serializations of JsonNode typed properties
Signed-off-by: Vincent Biret <[email protected]>
1 parent 6e62de2 commit 63b2b98

File tree

20 files changed

+105
-38
lines changed

20 files changed

+105
-38
lines changed

src/Microsoft.OpenApi.YamlReader/YamlConverter.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public static class YamlConverter
1818
/// </summary>
1919
/// <param name="yaml">The YAML stream.</param>
2020
/// <returns>A collection of nodes representing the YAML documents in the stream.</returns>
21-
public static IEnumerable<JsonNode?> ToJsonNode(this YamlStream yaml)
21+
public static IEnumerable<JsonNode> ToJsonNode(this YamlStream yaml)
2222
{
2323
return yaml.Documents.Select(x => x.ToJsonNode());
2424
}
@@ -28,7 +28,7 @@ public static class YamlConverter
2828
/// </summary>
2929
/// <param name="yaml">The YAML document.</param>
3030
/// <returns>A `JsonNode` representative of the YAML document.</returns>
31-
public static JsonNode? ToJsonNode(this YamlDocument yaml)
31+
public static JsonNode ToJsonNode(this YamlDocument yaml)
3232
{
3333
return yaml.RootNode.ToJsonNode();
3434
}
@@ -39,7 +39,7 @@ public static class YamlConverter
3939
/// <param name="yaml">The YAML node.</param>
4040
/// <returns>A `JsonNode` representative of the YAML node.</returns>
4141
/// <exception cref="NotSupportedException">Thrown for YAML that is not compatible with JSON.</exception>
42-
public static JsonNode? ToJsonNode(this YamlNode yaml)
42+
public static JsonNode ToJsonNode(this YamlNode yaml)
4343
{
4444
return yaml switch
4545
{
@@ -118,13 +118,13 @@ private static YamlSequenceNode ToYamlSequence(this JsonArray arr)
118118
"NULL"
119119
};
120120

121-
private static JsonValue? ToJsonValue(this YamlScalarNode yaml)
121+
private static JsonValue ToJsonValue(this YamlScalarNode yaml)
122122
{
123123
return yaml.Style switch
124124
{
125125
ScalarStyle.Plain when decimal.TryParse(yaml.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => JsonValue.Create(d),
126126
ScalarStyle.Plain when bool.TryParse(yaml.Value, out var b) => JsonValue.Create(b),
127-
ScalarStyle.Plain when YamlNullRepresentations.Contains(yaml.Value) => null,
127+
ScalarStyle.Plain when YamlNullRepresentations.Contains(yaml.Value) => JsonNullSentinel.JsonNull,
128128
ScalarStyle.Plain => JsonValue.Create(yaml.Value),
129129
ScalarStyle.SingleQuoted or ScalarStyle.DoubleQuoted or ScalarStyle.Literal or ScalarStyle.Folded or ScalarStyle.Any => JsonValue.Create(yaml.Value),
130130
_ => throw new ArgumentOutOfRangeException(nameof(yaml)),
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System;
5+
using System.Text.Json;
6+
using System.Text.Json.Nodes;
7+
8+
namespace Microsoft.OpenApi;
9+
10+
/// <summary>
11+
/// A sentinel value representing JSON null.
12+
/// This can only be used for OpenAPI properties of type <see cref="JsonNode"/>
13+
/// </summary>
14+
public static class JsonNullSentinel
15+
{
16+
private const string SentinelValue = "openapi-json-null-sentinel-value-2BF93600-0FE4-4250-987A-E5DDB203E464";
17+
private static readonly JsonValue SentinelJsonValue = JsonValue.Create(SentinelValue)!;
18+
/// <summary>
19+
/// A sentinel value representing JSON null.
20+
/// This can only be used for OpenAPI properties of type <see cref="JsonNode"/>.
21+
/// This can only be used for the root level of a JSON structure.
22+
/// Any use outside of these constraints is unsupported and may lead to unexpected behavior.
23+
/// Because this is returning a cloned instance, so the value can be added in a tree, reference equality checks will not work.
24+
/// You must use the <see cref="IsJsonNullSentinel(JsonNode?)"/> method to check for this sentinel.
25+
/// </summary>
26+
public static JsonValue JsonNull => (JsonValue)SentinelJsonValue.DeepClone();
27+
28+
/// <summary>
29+
/// Determines if the given node is the JSON null sentinel.
30+
/// </summary>
31+
/// <param name="node">The JsonNode to check.</param>
32+
/// <returns>Whether or not the given node is the JSON null sentinel.</returns>
33+
public static bool IsJsonNullSentinel(this JsonNode? node)
34+
{
35+
return node is JsonValue jsonValue &&
36+
jsonValue.GetValueKind() == JsonValueKind.String &&
37+
jsonValue.TryGetValue<string>(out var value) &&
38+
SentinelValue.Equals(value, StringComparison.Ordinal);
39+
}
40+
}

src/Microsoft.OpenApi/Models/Interfaces/IOpenApiExample.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public interface IOpenApiExample : IOpenApiDescribedElement, IOpenApiSummarizedE
1212
/// Embedded literal example. The value field and externalValue field are mutually
1313
/// exclusive. To represent examples of media types that cannot naturally represented
1414
/// in JSON or YAML, use a string value to contain the example, escaping where necessary.
15+
/// You must use the <see cref="JsonNullSentinel.IsJsonNullSentinel(JsonNode?)"/> method to check whether Default was assigned a null value in the document.
16+
/// Assign <see cref="JsonNullSentinel.JsonNull"/> to use get null as a serialized value.
1517
/// </summary>
1618
public JsonNode? Value { get; }
1719

src/Microsoft.OpenApi/Models/Interfaces/IOpenApiHeader.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public interface IOpenApiHeader : IOpenApiDescribedElement, IOpenApiReadOnlyExte
4848

4949
/// <summary>
5050
/// Example of the media type.
51+
/// You must use the <see cref="JsonNullSentinel.IsJsonNullSentinel(JsonNode?)"/> method to check whether Default was assigned a null value in the document.
52+
/// Assign <see cref="JsonNullSentinel.JsonNull"/> to use get null as a serialized value.
5153
/// </summary>
5254
public JsonNode? Example { get; }
5355

src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ public interface IOpenApiParameter : IOpenApiDescribedElement, IOpenApiReadOnlyE
8989
/// the example value SHALL override the example provided by the schema.
9090
/// To represent examples of media types that cannot naturally be represented in JSON or YAML,
9191
/// a string value can contain the example with escaping where necessary.
92+
/// You must use the <see cref="JsonNullSentinel.IsJsonNullSentinel(JsonNode?)"/> method to check whether Default was assigned a null value in the document.
93+
/// Assign <see cref="JsonNullSentinel.JsonNull"/> to use get null as a serialized value.
9294
/// </summary>
9395
public JsonNode? Example { get; }
9496

src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
117117
/// The default value represents what would be assumed by the consumer of the input as the value of the schema if one is not provided.
118118
/// Unlike JSON Schema, the value MUST conform to the defined type for the Schema Object defined at the same level.
119119
/// For example, if type is string, then default can be "foo" but cannot be 1.
120+
/// You must use the <see cref="JsonNullSentinel.IsJsonNullSentinel(JsonNode?)"/> method to check whether Default was assigned a null value in the document.
121+
/// Assign <see cref="JsonNullSentinel.JsonNull"/> to use get null as a serialized value.
120122
/// </summary>
121123
public JsonNode? Default { get; }
122124

@@ -238,6 +240,8 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
238240
/// A free-form property to include an example of an instance for this schema.
239241
/// To represent examples that cannot be naturally represented in JSON or YAML,
240242
/// a string value can be used to contain the example with escaping where necessary.
243+
/// You must use the <see cref="JsonNullSentinel.IsJsonNullSentinel(JsonNode?)"/> method to check whether Default was assigned a null value in the document.
244+
/// Assign <see cref="JsonNullSentinel.JsonNull"/> to use get null as a serialized value.
241245
/// </summary>
242246
public JsonNode? Example { get; }
243247

src/Microsoft.OpenApi/Models/JsonSchemaReference.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public class JsonSchemaReference : OpenApiReferenceWithDescription
1717
/// <summary>
1818
/// A default value which by default SHOULD override that of the referenced component.
1919
/// If the referenced object-type does not allow a default field, then this field has no effect.
20+
/// You must use the <see cref="JsonNullSentinel.IsJsonNullSentinel(JsonNode?)"/> method to check whether Default was assigned a null value in the document.
21+
/// Assign <see cref="JsonNullSentinel.JsonNull"/> to use get null as a serialized value.
2022
/// </summary>
2123
public JsonNode? Default { get; set; }
2224

src/Microsoft.OpenApi/Models/OpenApiMediaType.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public class OpenApiMediaType : IOpenApiSerializable, IOpenApiExtensible
2121
/// <summary>
2222
/// Example of the media type.
2323
/// The example object SHOULD be in the correct format as specified by the media type.
24+
/// You must use the <see cref="JsonNullSentinel.IsJsonNullSentinel(JsonNode?)"/> method to check whether Default was assigned a null value in the document.
25+
/// Assign <see cref="JsonNullSentinel.JsonNull"/> to use get null as a serialized value.
2426
/// </summary>
2527
public JsonNode? Example { get; set; }
2628

src/Microsoft.OpenApi/Models/RuntimeExpressionAnyWrapper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public RuntimeExpressionAnyWrapper(RuntimeExpressionAnyWrapper runtimeExpression
2929

3030
/// <summary>
3131
/// Gets/Sets the <see cref="JsonNode"/>
32+
/// You must use the <see cref="JsonNullSentinel.IsJsonNullSentinel(JsonNode?)"/> method to check whether Default was assigned a null value in the document.
33+
/// Assign <see cref="JsonNullSentinel.JsonNull"/> to use get null as a serialized value.
3234
/// </summary>
3335
public JsonNode? Any
3436
{

src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,19 @@ public MapNode(ParsingContext context, JsonNode node) : base(
3030

3131
_node = mapNode;
3232
_nodes = _node.Where(p => p.Value is not null).OfType<KeyValuePair<string, JsonNode>>().Select(p => new PropertyNode(Context, p.Key, p.Value)).ToList();
33+
_nodes.AddRange(_node.Where(p => p.Value is null).Select(p => new PropertyNode(Context, p.Key, JsonNullSentinel.JsonNull)));
3334
}
3435

3536
public PropertyNode? this[string key]
3637
{
3738
get
3839
{
39-
if (_node.TryGetPropertyValue(key, out var node) && node is not null)
40+
if (_node.TryGetPropertyValue(key, out var node))
4041
{
41-
return new(Context, key, node);
42+
if (node is not null)
43+
return new(Context, key, node);
44+
else
45+
return new(Context, key, JsonNullSentinel.JsonNull);
4246
}
4347

4448
return null;

0 commit comments

Comments
 (0)