|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using OpenApiConstants = Microsoft.AspNetCore.OpenApi.OpenApiConstants;
internal sealed partial class OpenApiJsonSchema
{
/// <summary>
/// Read a list from the given JSON reader instance.
/// </summary>
/// <typeparam name="T">The type of the elements that will populate the list.</typeparam>
/// <param name="reader">The <see cref="Utf8JsonReader"/> to consume the list from.</param>
/// <returns>A list parsed from the JSON array.</returns>
public static List<T>? ReadList<T>(ref Utf8JsonReader reader)
{
if (reader.TokenType == JsonTokenType.Null)
{
return default;
}
if (reader.TokenType == JsonTokenType.StartArray)
{
var values = new List<T>();
reader.Read();
while (reader.TokenType != JsonTokenType.EndArray)
{
values.Add((T)JsonSerializer.Deserialize(ref reader, typeof(T), OpenApiJsonSchemaContext.Default)!);
reader.Read();
}
return values;
}
if (reader.TokenType == JsonTokenType.Null)
{
return default;
}
return default;
}
/// <summary>
/// Read a dictionary from the given JSON reader instance.
/// </summary>
/// <typeparam name="T">The type associated with the values in the dictionary.</typeparam>
/// <param name="reader">The <see cref="Utf8JsonReader"/> to consume the dictionary from.</param>
/// <returns>A dictionary parsed from the JSON object.</returns>
/// <exception cref="JsonException">Thrown if JSON object is not valid.</exception>
public static Dictionary<string, T>? ReadDictionary<T>(ref Utf8JsonReader reader)
{
if (reader.TokenType == JsonTokenType.Null)
{
return default;
}
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("Expected StartObject or Null");
}
var values = new Dictionary<string, T>();
reader.Read();
while (reader.TokenType != JsonTokenType.EndObject)
{
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException("Expected PropertyName");
}
var key = reader.GetString()!;
reader.Read();
values[key] = (T)JsonSerializer.Deserialize(ref reader, typeof(T), OpenApiJsonSchemaContext.Default)!;
reader.Read();
}
return values;
}
internal static IOpenApiAny? ReadOpenApiAny(ref Utf8JsonReader reader)
=> ReadOpenApiAny(ref reader, out _);
internal static IOpenApiAny? ReadOpenApiAny(ref Utf8JsonReader reader, out string? type)
{
type = null;
if (reader.TokenType == JsonTokenType.Null)
{
return new OpenApiNull();
}
if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.False)
{
type = "boolean";
return new OpenApiBoolean(reader.GetBoolean());
}
if (reader.TokenType == JsonTokenType.Number)
{
if (reader.TryGetInt32(out var intValue))
{
type = "integer";
return new OpenApiInteger(intValue);
}
if (reader.TryGetInt64(out var longValue))
{
type = "integer";
return new OpenApiLong(longValue);
}
if (reader.TryGetSingle(out var floatValue) && !float.IsInfinity(floatValue))
{
type = "number";
return new OpenApiFloat(floatValue);
}
if (reader.TryGetDouble(out var doubleValue))
{
type = "number";
return new OpenApiDouble(doubleValue);
}
}
if (reader.TokenType == JsonTokenType.String)
{
type = "string";
return new OpenApiString(reader.GetString());
}
if (reader.TokenType == JsonTokenType.StartArray)
{
type = "array";
var array = new OpenApiArray();
while (reader.TokenType != JsonTokenType.EndArray)
{
array.Add(ReadOpenApiAny(ref reader));
reader.Read();
}
return array;
}
if (reader.TokenType == JsonTokenType.StartObject)
{
type = "object";
var obj = new OpenApiObject();
reader.Read();
while (reader.TokenType != JsonTokenType.EndObject)
{
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException("Expected PropertyName");
}
var key = reader.GetString()!;
reader.Read();
obj[key] = ReadOpenApiAny(ref reader);
reader.Read();
}
return obj;
}
return default;
}
/// <summary>
/// Read a property node from the given JSON reader instance.
/// </summary>
/// <param name="reader">The <see cref="Utf8JsonReader"/> to consume the property value from.</param>
/// <param name="propertyName">The name of the property the editor is currently consuming.</param>
/// <param name="schema">The <see cref="OpenApiSchema"/> to write the given values to.</param>
/// <param name="options">The <see cref="JsonSerializerOptions"/> instance.</param>
public static void ReadProperty(ref Utf8JsonReader reader, string propertyName, OpenApiSchema schema, JsonSerializerOptions options)
{
switch (propertyName)
{
case OpenApiSchemaKeywords.TypeKeyword:
reader.Read();
if (reader.TokenType == JsonTokenType.StartArray)
{
var types = ReadList<string>(ref reader);
foreach (var type in types ?? [])
{
// JSON Schema represents nullable types using an array consisting of
// the target type and "null". Since OpenAPI Schema does not support
// representing types within an array we must check for the "null" type
// and map it to OpenAPI's `nullable` property for OpenAPI v3.
if (type == "null")
{
schema.Nullable = true;
}
else
{
schema.Type = type;
}
}
}
else
{
var type = reader.GetString();
schema.Type = type;
}
break;
case OpenApiSchemaKeywords.EnumKeyword:
reader.Read();
var enumValues = ReadList<string>(ref reader);
if (enumValues is not null)
{
schema.Enum = enumValues.Select(v => new OpenApiString(v)).ToList<IOpenApiAny>();
}
break;
case OpenApiSchemaKeywords.DefaultKeyword:
reader.Read();
schema.Default = ReadOpenApiAny(ref reader);
break;
case OpenApiSchemaKeywords.ItemsKeyword:
reader.Read();
var valueConverter = (JsonConverter<OpenApiJsonSchema>)options.GetTypeInfo(typeof(OpenApiJsonSchema)).Converter;
schema.Items = valueConverter.Read(ref reader, typeof(OpenApiJsonSchema), options)?.Schema;
break;
case OpenApiSchemaKeywords.NullableKeyword:
reader.Read();
schema.Nullable = reader.GetBoolean();
break;
case OpenApiSchemaKeywords.DescriptionKeyword:
reader.Read();
schema.Description = reader.GetString();
break;
case OpenApiSchemaKeywords.FormatKeyword:
reader.Read();
schema.Format = reader.GetString();
break;
case OpenApiSchemaKeywords.RequiredKeyword:
reader.Read();
schema.Required = ReadList<string>(ref reader)?.ToHashSet();
break;
case OpenApiSchemaKeywords.MinLengthKeyword:
reader.Read();
var minLength = reader.GetInt32();
schema.MinLength = minLength;
break;
case OpenApiSchemaKeywords.MinItemsKeyword:
reader.Read();
var minItems = reader.GetInt32();
schema.MinItems = minItems;
break;
case OpenApiSchemaKeywords.MaxLengthKeyword:
reader.Read();
var maxLength = reader.GetInt32();
schema.MaxLength = maxLength;
break;
case OpenApiSchemaKeywords.MaxItemsKeyword:
reader.Read();
var maxItems = reader.GetInt32();
schema.MaxItems = maxItems;
break;
case OpenApiSchemaKeywords.MinimumKeyword:
reader.Read();
var minimum = reader.GetDecimal();
schema.Minimum = minimum;
break;
case OpenApiSchemaKeywords.MaximumKeyword:
reader.Read();
var maximum = reader.GetDecimal();
schema.Maximum = maximum;
break;
case OpenApiSchemaKeywords.PatternKeyword:
reader.Read();
var pattern = reader.GetString();
schema.Pattern = pattern;
break;
case OpenApiSchemaKeywords.PropertiesKeyword:
reader.Read();
var props = ReadDictionary<OpenApiJsonSchema>(ref reader);
schema.Properties = props?.ToDictionary(p => p.Key, p => p.Value.Schema);
break;
case OpenApiSchemaKeywords.AdditionalPropertiesKeyword:
reader.Read();
if (reader.TokenType == JsonTokenType.False)
{
schema.AdditionalPropertiesAllowed = false;
break;
}
var additionalPropsConverter = (JsonConverter<OpenApiJsonSchema>)options.GetTypeInfo(typeof(OpenApiJsonSchema)).Converter;
schema.AdditionalProperties = additionalPropsConverter.Read(ref reader, typeof(OpenApiJsonSchema), options)?.Schema;
break;
case OpenApiSchemaKeywords.AnyOfKeyword:
reader.Read();
schema.Type = "object";
var schemas = ReadList<OpenApiJsonSchema>(ref reader);
schema.AnyOf = schemas?.Select(s => s.Schema).ToList();
break;
case OpenApiSchemaKeywords.DiscriminatorKeyword:
reader.Read();
var discriminator = reader.GetString();
if (discriminator is not null)
{
schema.Discriminator = new OpenApiDiscriminator { PropertyName = discriminator };
}
break;
case OpenApiSchemaKeywords.DiscriminatorMappingKeyword:
reader.Read();
var mappings = ReadDictionary<string>(ref reader);
if (mappings is not null)
{
schema.Discriminator.Mapping = mappings;
}
break;
case OpenApiConstants.SchemaId:
reader.Read();
schema.Annotations ??= new Dictionary<string, object>();
schema.Annotations.Add(OpenApiConstants.SchemaId, reader.GetString());
break;
// OpenAPI does not support the `const` keyword in its schema implementation, so
// we map it to its closest approximation, an enum with a single value, here.
case OpenApiSchemaKeywords.ConstKeyword:
reader.Read();
schema.Enum = [ReadOpenApiAny(ref reader, out var constType)];
schema.Type = constType;
break;
case OpenApiSchemaKeywords.RefKeyword:
reader.Read();
schema.Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = reader.GetString() };
break;
default:
reader.Skip();
break;
}
}
}
|