Skip to content

Commit

Permalink
Merge pull request #1897 from microsoft/mk/define-json-schema-type-as…
Browse files Browse the repository at this point in the history
…-enum

Define JSON schema's Type property as a flaggable enum to allow storing multiple values
  • Loading branch information
MaggieKimani1 authored Oct 31, 2024
2 parents 34b81ce + 5eb0010 commit bf4a9ea
Show file tree
Hide file tree
Showing 55 changed files with 940 additions and 551 deletions.
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",

Check warning on line 26 in src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'boolean' 4 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
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

0 comments on commit bf4a9ea

Please sign in to comment.