Skip to content
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

Define JSON schema's Type property as a flaggable enum to allow storing multiple values #1897

Merged
merged 19 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
8 changes: 4 additions & 4 deletions src/Microsoft.OpenApi.Hidi/Formatters/PowerShellFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,10 @@ private static IList<OpenApiParameter> ResolveFunctionParameters(IList<OpenApiPa
parameter.Content = null;
parameter.Schema = new()
{
Type = "array",
Type = JsonSchemaType.Array,
Items = new()
{
Type = "string"
Type = JsonSchemaType.String
}
};
}
Expand All @@ -178,9 +178,9 @@ private static IList<OpenApiParameter> ResolveFunctionParameters(IList<OpenApiPa

private void AddAdditionalPropertiesToSchema(OpenApiSchema schema)
{
if (schema != null && !_schemaLoop.Contains(schema) && "object".Equals((string)schema.Type, StringComparison.OrdinalIgnoreCase))
if (schema != null && !_schemaLoop.Contains(schema) && schema.Type.Equals(JsonSchemaType.Object))
{
schema.AdditionalProperties = new() { Type = "object" };
schema.AdditionalProperties = new() { Type = JsonSchemaType.Object };

/* Because 'additionalProperties' are now being walked,
* we need a way to keep track of visited schemas to avoid
Expand Down
104 changes: 73 additions & 31 deletions src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using Microsoft.OpenApi.Exceptions;
using Microsoft.OpenApi.Models;

namespace Microsoft.OpenApi.Extensions
Expand All @@ -12,40 +13,81 @@ namespace Microsoft.OpenApi.Extensions
/// </summary>
public static class OpenApiTypeMapper
{
/// <summary>
/// Maps a JsonSchema data type to an identifier.
/// </summary>
/// <param name="schemaType"></param>
/// <returns></returns>
public static string ToIdentifier(this JsonSchemaType? schemaType)
{
return schemaType switch
{
JsonSchemaType.Null => "null",
JsonSchemaType.Boolean => "boolean",
JsonSchemaType.Integer => "integer",
JsonSchemaType.Number => "number",
JsonSchemaType.String => "string",
JsonSchemaType.Array => "array",
JsonSchemaType.Object => "object",
_ => null,
};
}

/// <summary>
/// Converts a schema type's identifier into the enum equivalent
/// </summary>
/// <param name="identifier"></param>
/// <returns></returns>
public static JsonSchemaType ToJsonSchemaType(this string identifier)
{
return identifier switch
{
"null" => JsonSchemaType.Null,
"boolean" => JsonSchemaType.Boolean,
"integer" or "int" => JsonSchemaType.Integer,
"number" or "double" or "float" or "decimal"=> JsonSchemaType.Number,
"string" => JsonSchemaType.String,
"array" => JsonSchemaType.Array,
"object" => JsonSchemaType.Object,
"file" => JsonSchemaType.String, // File is treated as string
_ => throw new OpenApiException(string.Format("Invalid schema type identifier: {0}", identifier))
};
}

private static readonly Dictionary<Type, Func<OpenApiSchema>> _simpleTypeToOpenApiSchema = new()
{
[typeof(bool)] = () => new() { Type = "boolean" },
[typeof(byte)] = () => new() { Type = "string", Format = "byte" },
[typeof(int)] = () => new() { Type = "number", Format = "int32" },
[typeof(uint)] = () => new() { Type = "number", Format = "int32" },
[typeof(long)] = () => new() { Type = "number", Format = "int64" },
[typeof(ulong)] = () => new() { Type = "number", Format = "int64" },
[typeof(float)] = () => new() { Type = "number", Format = "float" },
[typeof(double)] = () => new() { Type = "number", Format = "double" },
[typeof(decimal)] = () => new() { Type = "number", Format = "double" },
[typeof(DateTime)] = () => new() { Type = "string", Format = "date-time" },
[typeof(DateTimeOffset)] = () => new() { Type = "string", Format = "date-time" },
[typeof(Guid)] = () => new() { Type = "string", Format = "uuid" },
[typeof(char)] = () => new() { Type = "string" },
[typeof(bool)] = () => new() { Type = JsonSchemaType.Boolean },
[typeof(byte)] = () => new() { Type = JsonSchemaType.String, Format = "byte" },
[typeof(int)] = () => new() { Type = JsonSchemaType.Integer, Format = "int32" },
[typeof(uint)] = () => new() { Type = JsonSchemaType.Integer, Format = "int32" },
[typeof(long)] = () => new() { Type = JsonSchemaType.Integer, Format = "int64" },
[typeof(ulong)] = () => new() { Type = JsonSchemaType.Integer, Format = "int64" },
[typeof(float)] = () => new() { Type = JsonSchemaType.Number, Format = "float" },
[typeof(double)] = () => new() { Type = JsonSchemaType.Number, Format = "double" },
[typeof(decimal)] = () => new() { Type = JsonSchemaType.Number, Format = "double" },
[typeof(DateTime)] = () => new() { Type = JsonSchemaType.String, Format = "date-time" },
[typeof(DateTimeOffset)] = () => new() { Type = JsonSchemaType.String, Format = "date-time" },
[typeof(Guid)] = () => new() { Type = JsonSchemaType.String, Format = "uuid" },
[typeof(char)] = () => new() { Type = JsonSchemaType.String },

// Nullable types
[typeof(bool?)] = () => new() { Type = "boolean", Nullable = true },
[typeof(byte?)] = () => new() { Type = "string", Format = "byte", Nullable = true },
[typeof(int?)] = () => new() { Type = "number", Format = "int32", Nullable = true },
[typeof(uint?)] = () => new() { Type = "number", Format = "int32", Nullable = true },
[typeof(long?)] = () => new() { Type = "number", Format = "int64", Nullable = true },
[typeof(ulong?)] = () => new() { Type = "number", Format = "int64", Nullable = true },
[typeof(float?)] = () => new() { Type = "number", Format = "float", Nullable = true },
[typeof(double?)] = () => new() { Type = "number", Format = "double", Nullable = true },
[typeof(decimal?)] = () => new() { Type = "number", Format = "double", Nullable = true },
[typeof(DateTime?)] = () => new() { Type = "string", Format = "date-time", Nullable = true },
[typeof(DateTimeOffset?)] = () => new() { Type = "string", Format = "date-time", Nullable = true },
[typeof(Guid?)] = () => new() { Type = "string", Format = "uuid", Nullable = true },
[typeof(char?)] = () => new() { Type = "string", Nullable = true },
[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(Uri)] = () => new() { Type = "string", Format = "uri" }, // Uri is treated as simple string
[typeof(string)] = () => new() { Type = "string" },
[typeof(object)] = () => new() { Type = "object" }
[typeof(Uri)] = () => new() { Type = JsonSchemaType.String, Format = "uri" }, // Uri is treated as simple string
[typeof(string)] = () => new() { Type = JsonSchemaType.String },
[typeof(object)] = () => new() { Type = JsonSchemaType.Object }
};

/// <summary>
Expand Down Expand Up @@ -79,7 +121,7 @@ public static OpenApiSchema MapTypeToOpenApiPrimitiveType(this Type type)

return _simpleTypeToOpenApiSchema.TryGetValue(type, out var result)
? result()
: new() { Type = "string" };
: new() { Type = JsonSchemaType.String };
}

/// <summary>
Expand All @@ -95,7 +137,7 @@ public static Type MapOpenApiPrimitiveTypeToSimpleType(this OpenApiSchema schema
throw new ArgumentNullException(nameof(schema));
}

var type = (schema.Type?.ToString().ToLowerInvariant(), schema.Format?.ToLowerInvariant(), schema.Nullable) switch
var type = (schema.Type.ToIdentifier(), schema.Format?.ToLowerInvariant(), schema.Nullable) switch
{
("boolean", null, false) => typeof(bool),
// integer is technically not valid with format, but we must provide some compatibility
Expand Down
49 changes: 49 additions & 0 deletions src/Microsoft.OpenApi/Models/JsonSchemaType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;

namespace Microsoft.OpenApi.Models
{
/// <summary>
/// Represents the type of a JSON schema.
/// </summary>
[Flags]
public enum JsonSchemaType

Check warning on line 12 in src/Microsoft.OpenApi/Models/JsonSchemaType.cs

View workflow job for this annotation

GitHub Actions / Build

Rename this enumeration to match the regular expression: '^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$'. (https://rules.sonarsource.com/csharp/RSPEC-2342)
{
/// <summary>
/// Represents a null type.
/// </summary>
Null = 1,

/// <summary>
/// Represents a boolean type.
/// </summary>
Boolean = 2,

/// <summary>
/// Represents an integer type.
/// </summary>
Integer = 4,

/// <summary>
/// Represents a number type.
/// </summary>
Number = 8,

/// <summary>
/// Represents a string type.
/// </summary>
String = 16,

/// <summary>
/// Represents an object type.
/// </summary>
Object = 32,

/// <summary>
/// Represents an array type.
/// </summary>
Array = 64,
}
}
6 changes: 3 additions & 3 deletions src/Microsoft.OpenApi/Models/OpenApiParameter.cs
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 System;
Expand Down Expand Up @@ -292,7 +292,7 @@ public virtual void SerializeAsV2(IOpenApiWriter writer)
}
// In V2 parameter's type can't be a reference to a custom object schema or can't be of type object
// So in that case map the type as string.
else if (Schema?.UnresolvedReference == true || "object".Equals(Schema?.Type?.ToString(), StringComparison.OrdinalIgnoreCase))
else if (Schema?.UnresolvedReference == true || Schema?.Type == JsonSchemaType.Object)
{
writer.WriteProperty(OpenApiConstants.Type, "string");
}
Expand Down Expand Up @@ -333,7 +333,7 @@ public virtual void SerializeAsV2(IOpenApiWriter writer)
// allowEmptyValue
writer.WriteProperty(OpenApiConstants.AllowEmptyValue, AllowEmptyValue, false);

if (this.In == ParameterLocation.Query && "array".Equals(Schema?.Type.ToString(), StringComparison.OrdinalIgnoreCase))
if (this.In == ParameterLocation.Query && Schema?.Type == JsonSchemaType.Array)
{
if (this.Style == ParameterStyle.Form && this.Explode == true)
{
Expand Down
5 changes: 3 additions & 2 deletions src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Writers;

Expand Down Expand Up @@ -141,11 +142,11 @@ internal IEnumerable<OpenApiFormDataParameter> ConvertToFormDataParameters()
foreach (var property in Content.First().Value.Schema.Properties)
{
var paramSchema = property.Value;
if ("string".Equals(paramSchema.Type.ToString(), StringComparison.OrdinalIgnoreCase)
if ("string".Equals(paramSchema.Type.ToIdentifier(), StringComparison.OrdinalIgnoreCase)
&& ("binary".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase)
|| "base64".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase)))
{
paramSchema.Type = "file";
paramSchema.Type = "file".ToJsonSchemaType();
paramSchema.Format = null;
}
yield return new()
Expand Down
Loading
Loading