diff --git a/dotnet/samples/GettingStartedWithProcesses/Step04/SchemaGenerator.cs b/dotnet/samples/GettingStartedWithProcesses/Step04/SchemaGenerator.cs index 89d6c99de731..5350e2c53d6e 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step04/SchemaGenerator.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step04/SchemaGenerator.cs @@ -1,10 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using System.ComponentModel; -using System.Reflection; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using JsonSchemaMapper; +using Microsoft.Extensions.AI; using Microsoft.SemanticKernel; namespace Step04; @@ -14,55 +11,18 @@ internal static class JsonSchemaGenerator /// /// Wrapper for generating a JSON schema as string from a .NET type. /// - public static string FromType() + public static string FromType() { JsonSerializerOptions options = new(JsonSerializerOptions.Default) { UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, }; - JsonSchemaMapperConfiguration config = new() + AIJsonSchemaCreateOptions config = new() { - TreatNullObliviousAsNonNullable = true, - TransformSchemaNode = (context, schema) => - { - // NOTE: This can be replaced with `IncludeAdditionalProperties = false` when upgraded to System.Json.Text 9.0.0 - if (context.TypeInfo.Type == typeof(SchemaType)) - { - schema["additionalProperties"] = false; - } - - // Determine if a type or property and extract the relevant attribute provider - ICustomAttributeProvider? attributeProvider = context.PropertyInfo is not null - ? context.PropertyInfo.AttributeProvider - : context.TypeInfo.Type; - - // Look up any description attributes - DescriptionAttribute? descriptionAttr = attributeProvider? - .GetCustomAttributes(inherit: true) - .Select(attr => attr as DescriptionAttribute) - .FirstOrDefault(attr => attr is not null); - - // Apply description attribute to the generated schema - if (descriptionAttr != null) - { - if (schema is not JsonObject jObj) - { - // Handle the case where the schema is a boolean - JsonValueKind valueKind = schema.GetValueKind(); - schema = jObj = new JsonObject(); - if (valueKind is JsonValueKind.False) - { - jObj.Add("not", true); - } - } - - jObj["description"] = descriptionAttr.Description; - } - - return schema; - } + IncludeSchemaKeyword = false, + DisallowAdditionalProperties = true, }; - return KernelJsonSchemaBuilder.Build(typeof(SchemaType), "Intent Result", config).AsJson(); + return KernelJsonSchemaBuilder.Build(typeof(TSchemaType), "Intent Result", config).AsJson(); } } diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiFunctionTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiFunctionTests.cs index b6dc394e6e92..0ae1a254e6f4 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiFunctionTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiFunctionTests.cs @@ -95,8 +95,8 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParamete { "type": "object", "required": ["param1", "param2"], "properties": { - "param1": { "type": "string", "description": "String param 1" }, - "param2": { "type": "integer", "description": "Int param 2" } } } + "param1": { "description": "String param 1", "type": "string" }, + "param2": { "description": "Int param 2" , "type": "integer"} } } """; KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] @@ -126,8 +126,8 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParame { "type": "object", "required": ["param1", "param2"], "properties": { - "param1": { "type": "string", "description": "String param 1" }, - "param2": { "type": "integer", "description": "Int param 2" } } } + "param1": { "description": "String param 1", "type": "string" }, + "param2": { "description": "Int param 2", "type": "integer"} } } """; KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] @@ -180,7 +180,7 @@ public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescript // Assert Assert.Equal( - """{"type":"object","required":[],"properties":{"param1":{"type":"string","description":"something neat"}}}""", + """{"type":"object","required":[],"properties":{"param1":{"description":"something neat","type":"string"}}}""", JsonSerializer.Serialize(result.Parameters)); } } diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs index 75552dc1f23b..99afbb5ec092 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs @@ -200,7 +200,7 @@ public void ItCanCreateValidGeminiFunctionManualForPlugin() // Assert Assert.NotNull(result); Assert.Equal( - """{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"type":"string","enum":["Value1","Value2"],"description":"Enum parameter"},"parameter3":{"type":"string","format":"date-time","description":"DateTime parameter"}}}""", + """{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"description":"String parameter","type":"string"},"parameter2":{"description":"Enum parameter","type":"string","enum":["Value1","Value2"]},"parameter3":{"description":"DateTime parameter","type":"string"}}}""", JsonSerializer.Serialize(result.Parameters) ); } diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIFunctionTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIFunctionTests.cs index 1967ee882ec8..ffafba5a446d 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIFunctionTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIFunctionTests.cs @@ -92,7 +92,7 @@ public void ItCanConvertToFunctionDefinitionWithPluginName() [Fact] public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParameterType() { - string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; + string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "description": "String param 1", "type": "string" }, "param2": { "description": "Int param 2", "type": "integer" } } } """; KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] { @@ -118,7 +118,7 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParamete [Fact] public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParameterType() { - string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; + string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "description": "String param 1", "type": "string" }, "param2": { "description": "Int param 2", "type": "integer" } } } """; KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] { @@ -174,7 +174,7 @@ public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescript Assert.NotNull(pd.properties); Assert.Single(pd.properties); Assert.Equal( - JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"string", "description":"something neat" }""")), + JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "description":"something neat", "type":"string" }""")), JsonSerializer.Serialize(pd.properties.First().Value.RootElement)); } diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIJsonSchemaTransformerTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIJsonSchemaTransformerTests.cs index 29ed2ba3f5e6..d1690f560473 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIJsonSchemaTransformerTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIJsonSchemaTransformerTests.cs @@ -3,24 +3,23 @@ using System.Collections.Generic; using System.ComponentModel; using System.Text.Json; -using JsonSchemaMapper; +using Microsoft.Extensions.AI; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; using Xunit; namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core; /// -/// Unit tests for class. +/// Unit tests for schema transformations used by OpenAI clients. /// public sealed class OpenAIJsonSchemaTransformerTests { - private static readonly JsonSchemaMapperConfiguration s_jsonSchemaMapperConfiguration = new() + private static readonly AIJsonSchemaCreateOptions s_jsonSchemaCreateOptions = new() { - IncludeSchemaVersion = false, - IncludeTypeInEnums = true, - TreatNullObliviousAsNonNullable = true, - TransformSchemaNode = OpenAIJsonSchemaTransformer.Transform, + IncludeSchemaKeyword = false, + IncludeTypeInEnumSchemas = true, + DisallowAdditionalProperties = true, + RequireAllProperties = true, }; private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() @@ -124,7 +123,7 @@ public void ItTransformsJsonSchemaCorrectly() """; // Act - var schema = KernelJsonSchemaBuilder.Build(type, configuration: s_jsonSchemaMapperConfiguration); + var schema = KernelJsonSchemaBuilder.Build(type, configuration: s_jsonSchemaCreateOptions); // Assert Assert.Equal(NormalizeJson(expectedSchema), NormalizeJson(schema.ToString())); diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs index e817d559aeaa..ce518ded70f9 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs @@ -196,7 +196,7 @@ public void ItCanCreateValidAzureOpenAIFunctionManualForPlugin() // Assert Assert.NotNull(result); Assert.Equal( - """{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"type":"string","enum":["Value1","Value2"],"description":"Enum parameter"},"parameter3":{"type":"string","format":"date-time","description":"DateTime parameter"}}}""", + """{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"description":"String parameter","type":"string"},"parameter2":{"description":"Enum parameter","type":"string","enum":["Value1","Value2"]},"parameter3":{"description":"DateTime parameter","type":"string"}}}""", result.FunctionParameters.ToString() ); } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIJsonSchemaTransformer.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIJsonSchemaTransformer.cs deleted file mode 100644 index 73a0fbfb711d..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIJsonSchemaTransformer.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using System.Text.Json.Nodes; -using JsonSchemaMapper; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// JSON Schema transformer to apply OpenAI conditions for structured outputs. -/// -/// - "additionalProperties" property must always be set to in objects. -/// More information here: . -/// -/// -/// - All fields must be "required". -/// More information here: . -/// -/// -internal static class OpenAIJsonSchemaTransformer -{ - private const string AdditionalPropertiesPropertyName = "additionalProperties"; - private const string TypePropertyName = "type"; - private const string ObjectValueName = "object"; - private const string PropertiesPropertyName = "properties"; - private const string RequiredPropertyName = "required"; - - internal static JsonNode Transform(JsonSchemaGenerationContext context, JsonNode schema) - { - // Transform schema if node is object only. - if (schema is JsonObject jsonSchemaObject) - { - var types = GetTypes(jsonSchemaObject); - - if (types is not null && types.Contains(ObjectValueName)) - { - // Set "additionalProperties" to "false". - jsonSchemaObject[AdditionalPropertiesPropertyName] = false; - - // Specify all properties as "required". - if (jsonSchemaObject.TryGetPropertyValue(PropertiesPropertyName, out var properties) && - properties is JsonObject propertiesObject) - { - var propertyNames = propertiesObject.Select(l => (JsonNode)l.Key).ToArray(); - - jsonSchemaObject[RequiredPropertyName] = new JsonArray(propertyNames); - } - } - } - - return schema; - } - - private static List? GetTypes(JsonObject jsonObject) - { - if (jsonObject.TryGetPropertyValue(TypePropertyName, out var typeProperty) && typeProperty is not null) - { - // For cases when "type" has an array value (e.g "type": "["object", "null"]"). - if (typeProperty is JsonArray nodeArray) - { - return nodeArray.ToArray().Select(element => element?.GetValue()).ToList(); - } - - // Case when "type" has a string value (e.g. "type": "object"). - return [typeProperty.GetValue()]; - } - - return null; - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Helpers/OpenAIChatResponseFormatBuilder.cs b/dotnet/src/Connectors/Connectors.OpenAI/Helpers/OpenAIChatResponseFormatBuilder.cs index 835d4c19ae32..e032335dbe5a 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Helpers/OpenAIChatResponseFormatBuilder.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Helpers/OpenAIChatResponseFormatBuilder.cs @@ -3,7 +3,6 @@ using System; using System.Text; using System.Text.Json; -using JsonSchemaMapper; using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.OpenAI; @@ -14,14 +13,14 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; internal static class OpenAIChatResponseFormatBuilder { /// - /// for JSON schema format for structured outputs. + /// for JSON schema format for structured outputs. /// - private static readonly JsonSchemaMapperConfiguration s_jsonSchemaMapperConfiguration = new() + private static readonly Microsoft.Extensions.AI.AIJsonSchemaCreateOptions s_jsonSchemaCreateOptions = new() { - IncludeSchemaVersion = false, - IncludeTypeInEnums = true, - TreatNullObliviousAsNonNullable = true, - TransformSchemaNode = OpenAIJsonSchemaTransformer.Transform + IncludeSchemaKeyword = false, + IncludeTypeInEnumSchemas = true, + DisallowAdditionalProperties = true, + RequireAllProperties = true, }; /// @@ -56,7 +55,7 @@ internal static ChatResponseFormat GetJsonSchemaResponseFormat(Type formatObject { var type = formatObjectType.IsGenericType && formatObjectType.GetGenericTypeDefinition() == typeof(Nullable<>) ? Nullable.GetUnderlyingType(formatObjectType)! : formatObjectType; - var schema = KernelJsonSchemaBuilder.Build(type, configuration: s_jsonSchemaMapperConfiguration); + var schema = KernelJsonSchemaBuilder.Build(type, configuration: s_jsonSchemaCreateOptions); var schemaBinaryData = BinaryData.FromString(schema.ToString()); var typeName = GetTypeName(type); diff --git a/dotnet/src/InternalUtilities/src/Schema/.editorconfig b/dotnet/src/InternalUtilities/src/Schema/.editorconfig deleted file mode 100644 index 76e8ee827086..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -# Suppressing code analysis diagnostics for code included as a source copy -[*.cs] -dotnet_diagnostic.CA1852.severity = none -dotnet_diagnostic.IDE0005.severity = none -dotnet_diagnostic.IDE0009.severity = none -dotnet_diagnostic.IDE0055.severity = none -dotnet_diagnostic.IDE0161.severity = none -dotnet_diagnostic.IDE1006.severity = none -dotnet_diagnostic.RCS1211.severity = none \ No newline at end of file diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaGenerationContext.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaGenerationContext.cs deleted file mode 100644 index 05955507277a..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaGenerationContext.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// Source copied from https://github.com/eiriktsarpalis/stj-schema-mapper -// It should be kept in sync with any changes made in that repo, -// and should be removed once the relevant replacements are available in STJv9. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.Json.Serialization.Metadata; - -namespace JsonSchemaMapper; - -/// -/// Defines the context in which a JSON schema within a type graph is being generated. -/// -#if EXPOSE_JSON_SCHEMA_MAPPER -public -#else -internal -#endif - readonly struct JsonSchemaGenerationContext -{ - internal JsonSchemaGenerationContext( - JsonTypeInfo typeInfo, - Type? declaringType, - JsonPropertyInfo? propertyInfo, - ParameterInfo? parameterInfo, - ICustomAttributeProvider? propertyAttributeProvider) - { - TypeInfo = typeInfo; - DeclaringType = declaringType; - PropertyInfo = propertyInfo; - ParameterInfo = parameterInfo; - PropertyAttributeProvider = propertyAttributeProvider; - } - - /// - /// The for the type being processed. - /// - public JsonTypeInfo TypeInfo { get; } - - /// - /// The declaring type of the property or parameter being processed. - /// - public Type? DeclaringType { get; } - - /// - /// The if the schema is being generated for a property. - /// - public JsonPropertyInfo? PropertyInfo { get; } - - /// - /// The if a constructor parameter - /// has been associated with the accompanying . - /// - public ParameterInfo? ParameterInfo { get; } - - /// - /// The corresponding to the property or field being processed. - /// - public ICustomAttributeProvider? PropertyAttributeProvider { get; } - - /// - /// Checks if the type, property, or parameter has the specified attribute applied. - /// - /// The type of the attribute to resolve. - /// Whether to look up the hierarchy chain for the inherited custom attribute. - /// True if the attribute is defined by the current context. - public bool IsDefined(bool inherit = false) - where TAttribute : Attribute => - GetCustomAttributes(typeof(TAttribute), inherit).Any(); - - /// - /// Checks if the type, property, or parameter has the specified attribute applied. - /// - /// The type of the attribute to resolve. - /// Whether to look up the hierarchy chain for the inherited custom attribute. - /// The first attribute resolved from the current context, or null. - public TAttribute? GetAttribute(bool inherit = false) - where TAttribute : Attribute => - (TAttribute?)GetCustomAttributes(typeof(TAttribute), inherit).FirstOrDefault(); - - /// - /// Resolves any custom attributes that might have been applied to the type, property, or parameter. - /// - /// The attribute type to resolve. - /// Whether to look up the hierarchy chain for the inherited custom attribute. - /// An enumerable of all custom attributes defined by the context. - public IEnumerable GetCustomAttributes(Type type, bool inherit = false) - { - // Resolves attributes starting from the property, then the parameter, and finally the type itself. - return GetAttrs(PropertyAttributeProvider) - .Concat(GetAttrs(ParameterInfo)) - .Concat(GetAttrs(TypeInfo.Type)) - .Cast(); - - object[] GetAttrs(ICustomAttributeProvider? provider) => - provider?.GetCustomAttributes(type, inherit) ?? Array.Empty(); - } -} diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.JsonSchema.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.JsonSchema.cs deleted file mode 100644 index 5eeb37b61fee..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.JsonSchema.cs +++ /dev/null @@ -1,563 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// Source copied from https://github.com/eiriktsarpalis/stj-schema-mapper -// It should be kept in sync with any changes made in that repo, -// and should be removed once the relevant replacements are available in STJv9. - -#if !NET9_0_OR_GREATER && !SYSTEM_TEXT_JSON_V9 -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Text.Json.Nodes; - -namespace JsonSchemaMapper; - -#if EXPOSE_JSON_SCHEMA_MAPPER -public -#else -internal -#endif - static partial class JsonSchemaMapper -{ - // Simple JSON schema representation taken from System.Text.Json - // https://github.com/dotnet/runtime/blob/50d6cad649aad2bfa4069268eddd16fd51ec5cf3/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs - private sealed class JsonSchema - { - public static JsonSchema False { get; } = new(false); - public static JsonSchema True { get; } = new(true); - - public JsonSchema() - { - } - - private JsonSchema(bool trueOrFalse) - { - _trueOrFalse = trueOrFalse; - } - - public bool IsTrue => _trueOrFalse is true; - public bool IsFalse => _trueOrFalse is false; - private readonly bool? _trueOrFalse; - - public string? Schema - { - get => _schema; - set - { - VerifyMutable(); - _schema = value; - } - } - - private string? _schema; - - public string? Title - { - get => _title; - set - { - VerifyMutable(); - _title = value; - } - } - - private string? _title; - - public string? Description - { - get => _description; - set - { - VerifyMutable(); - _description = value; - } - } - - private string? _description; - - public string? Ref - { - get => _ref; - set - { - VerifyMutable(); - _ref = value; - } - } - - private string? _ref; - - public string? Comment - { - get => _comment; - set - { - VerifyMutable(); - _comment = value; - } - } - - private string? _comment; - - public JsonSchemaType Type - { - get => _type; - set - { - VerifyMutable(); - _type = value; - } - } - - private JsonSchemaType _type = JsonSchemaType.Any; - - public string? Format - { - get => _format; - set - { - VerifyMutable(); - _format = value; - } - } - - private string? _format; - - public string? Pattern - { - get => _pattern; - set - { - VerifyMutable(); - _pattern = value; - } - } - - private string? _pattern; - - public JsonNode? Constant - { - get => _constant; - set - { - VerifyMutable(); - _constant = value; - } - } - - private JsonNode? _constant; - - public List>? Properties - { - get => _properties; - set - { - VerifyMutable(); - _properties = value; - } - } - - private List>? _properties; - - public List? Required - { - get => _required; - set - { - VerifyMutable(); - _required = value; - } - } - - private List? _required; - - public JsonSchema? Items - { - get => _items; - set - { - VerifyMutable(); - _items = value; - } - } - - private JsonSchema? _items; - - public JsonSchema? AdditionalProperties - { - get => _additionalProperties; - set - { - VerifyMutable(); - _additionalProperties = value; - } - } - - private JsonSchema? _additionalProperties; - - public JsonArray? Enum - { - get => _enum; - set - { - VerifyMutable(); - _enum = value; - } - } - - private JsonArray? _enum; - - public JsonSchema? Not - { - get => _not; - set - { - VerifyMutable(); - _not = value; - } - } - - private JsonSchema? _not; - - public List? AnyOf - { - get => _anyOf; - set - { - VerifyMutable(); - _anyOf = value; - } - } - - private List? _anyOf; - - public bool HasDefaultValue - { - get => _hasDefaultValue; - set - { - VerifyMutable(); - _hasDefaultValue = value; - } - } - - private bool _hasDefaultValue; - - public JsonNode? DefaultValue - { - get => _defaultValue; - set - { - VerifyMutable(); - _defaultValue = value; - } - } - - private JsonNode? _defaultValue; - - public int? MinLength - { - get => _minLength; - set - { - VerifyMutable(); - _minLength = value; - } - } - - private int? _minLength; - - public int? MaxLength - { - get => _maxLength; - set - { - VerifyMutable(); - _maxLength = value; - } - } - - private int? _maxLength; - - public JsonSchemaGenerationContext? GenerationContext { get; set; } - - public int KeywordCount - { - get - { - if (_trueOrFalse != null) - { - return 0; - } - - int count = 0; - Count(Schema != null); - Count(Ref != null); - Count(Comment != null); - Count(Title != null); - Count(Description != null); - Count(Type != JsonSchemaType.Any); - Count(Format != null); - Count(Pattern != null); - Count(Constant != null); - Count(Properties != null); - Count(Required != null); - Count(Items != null); - Count(AdditionalProperties != null); - Count(Enum != null); - Count(Not != null); - Count(AnyOf != null); - Count(HasDefaultValue); - Count(MinLength != null); - Count(MaxLength != null); - - return count; - - void Count(bool isKeywordSpecified) - { - count += isKeywordSpecified ? 1 : 0; - } - } - } - - public void MakeNullable() - { - if (_trueOrFalse != null) - { - return; - } - - if (Type != JsonSchemaType.Any) - { - Type |= JsonSchemaType.Null; - } - } - - public JsonNode ToJsonNode(JsonSchemaMapperConfiguration options) - { - if (_trueOrFalse is { } boolSchema) - { - return CompleteSchema((JsonNode)boolSchema); - } - - var objSchema = new JsonObject(); - - if (Schema != null) - { - objSchema.Add(JsonSchemaConstants.SchemaPropertyName, Schema); - } - - if (Title != null) - { - objSchema.Add(JsonSchemaConstants.TitlePropertyName, Title); - } - - if (Description != null) - { - objSchema.Add(JsonSchemaConstants.DescriptionPropertyName, Description); - } - - if (Ref != null) - { - objSchema.Add(JsonSchemaConstants.RefPropertyName, Ref); - } - - if (Comment != null) - { - objSchema.Add(JsonSchemaConstants.CommentPropertyName, Comment); - } - - if (MapSchemaType(Type) is JsonNode type) - { - objSchema.Add(JsonSchemaConstants.TypePropertyName, type); - } - - if (Format != null) - { - objSchema.Add(JsonSchemaConstants.FormatPropertyName, Format); - } - - if (Pattern != null) - { - objSchema.Add(JsonSchemaConstants.PatternPropertyName, Pattern); - } - - if (Constant != null) - { - objSchema.Add(JsonSchemaConstants.ConstPropertyName, Constant); - } - - if (Properties != null) - { - var properties = new JsonObject(); - foreach (KeyValuePair property in Properties) - { - properties.Add(property.Key, property.Value.ToJsonNode(options)); - } - - objSchema.Add(JsonSchemaConstants.PropertiesPropertyName, properties); - } - - if (Required != null) - { - var requiredArray = new JsonArray(); - foreach (string requiredProperty in Required) - { - requiredArray.Add((JsonNode)requiredProperty); - } - - objSchema.Add(JsonSchemaConstants.RequiredPropertyName, requiredArray); - } - - if (Items != null) - { - objSchema.Add(JsonSchemaConstants.ItemsPropertyName, Items.ToJsonNode(options)); - } - - if (AdditionalProperties != null) - { - objSchema.Add(JsonSchemaConstants.AdditionalPropertiesPropertyName, AdditionalProperties.ToJsonNode(options)); - } - - if (Enum != null) - { - objSchema.Add(JsonSchemaConstants.EnumPropertyName, Enum); - } - - if (Not != null) - { - objSchema.Add(JsonSchemaConstants.NotPropertyName, Not.ToJsonNode(options)); - } - - if (AnyOf != null) - { - JsonArray anyOfArray = new(); - foreach (JsonSchema schema in AnyOf) - { - anyOfArray.Add(schema.ToJsonNode(options)); - } - - objSchema.Add(JsonSchemaConstants.AnyOfPropertyName, anyOfArray); - } - - if (HasDefaultValue) - { - objSchema.Add(JsonSchemaConstants.DefaultPropertyName, DefaultValue); - } - - if (MinLength is int minLength) - { - objSchema.Add(JsonSchemaConstants.MinLengthPropertyName, (JsonNode)minLength); - } - - if (MaxLength is int maxLength) - { - objSchema.Add(JsonSchemaConstants.MaxLengthPropertyName, (JsonNode)maxLength); - } - - return CompleteSchema(objSchema); - - JsonNode CompleteSchema(JsonNode schema) - { - if (GenerationContext is { } context) - { - Debug.Assert(options.TransformSchemaNode != null, "context should only be populated if a callback is present."); - - // Apply any user-defined transformations to the schema. - return options.TransformSchemaNode!(context, schema); - } - - return schema; - } - } - - public static void EnsureMutable(ref JsonSchema schema) - { - switch (schema._trueOrFalse) - { - case false: - schema = new JsonSchema { Not = JsonSchema.True }; - break; - case true: - schema = new JsonSchema(); - break; - } - } - - private static readonly JsonSchemaType[] s_schemaValues = new JsonSchemaType[] - { - // NB the order of these values influences order of types in the rendered schema - JsonSchemaType.String, - JsonSchemaType.Integer, - JsonSchemaType.Number, - JsonSchemaType.Boolean, - JsonSchemaType.Array, - JsonSchemaType.Object, - JsonSchemaType.Null, - }; - - private void VerifyMutable() - { - Debug.Assert(_trueOrFalse is null, "Schema is not mutable"); - if (_trueOrFalse is not null) - { - Throw(); - static void Throw() => throw new InvalidOperationException(); - } - } - - private static JsonNode? MapSchemaType(JsonSchemaType schemaType) - { - if (schemaType is JsonSchemaType.Any) - { - return null; - } - - if (ToIdentifier(schemaType) is string identifier) - { - return identifier; - } - - var array = new JsonArray(); - foreach (JsonSchemaType type in s_schemaValues) - { - if ((schemaType & type) != 0) - { - array.Add((JsonNode)ToIdentifier(type)!); - } - } - - return array; - - static string? ToIdentifier(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, - }; - } - } - } - - [EditorBrowsable(EditorBrowsableState.Never)] - private enum JsonSchemaType - { - Any = 0, // No type declared on the schema - Null = 1, - Boolean = 2, - Integer = 4, - Number = 8, - String = 16, - Array = 32, - Object = 64, - } -} -#endif diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.cs deleted file mode 100644 index cd45d7cb1e7f..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.cs +++ /dev/null @@ -1,958 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// Source copied from https://github.com/eiriktsarpalis/stj-schema-mapper -// It should be kept in sync with any changes made in that repo, -// and should be removed once the relevant replacements are available in STJv9. - -#if !NET9_0_OR_GREATER && !SYSTEM_TEXT_JSON_V9 -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; - -namespace JsonSchemaMapper; - -#if EXPOSE_JSON_SCHEMA_MAPPER -public -#else -internal -#endif - static partial class JsonSchemaMapper -{ - // For System.Text.Json versions prior to v9, JsonSchemaMapper is implemented as a standalone component. - // The implementation uses private reflection to access metadata not available with the older APIs of STJ. - // While the implementation is forward compatible with .NET 9, it is not guaranteed that it will work with - // later versions of .NET and users are encouraged to switch to the built-in JsonSchemaExporter eventually. - - private static partial JsonNode MapRootTypeJsonSchema(JsonTypeInfo typeInfo, JsonSchemaMapperConfiguration configuration) - { - GenerationState state = new(configuration, typeInfo.Options); - JsonSchema schema = MapJsonSchemaCore(ref state, typeInfo); - return schema.ToJsonNode(configuration); - } - - private static partial JsonNode MapMethodParameterJsonSchema( - ParameterInfo parameterInfo, - JsonTypeInfo parameterTypeInfo, - JsonSchemaMapperConfiguration configuration, - NullabilityInfoContext nullabilityContext, - out bool isRequired) - { - Debug.Assert(parameterInfo.Name != null); - - GenerationState state = new(configuration, parameterTypeInfo.Options, nullabilityContext); - - string? parameterDescription = null; - isRequired = false; - - ResolveParameterInfo( - parameterInfo, - parameterTypeInfo, - state.NullabilityInfoContext, - state.Configuration, - out bool hasDefaultValue, - out JsonNode? defaultValue, - out bool isNonNullableType, - ref parameterDescription, - ref isRequired); - - state.PushSchemaNode(JsonSchemaConstants.PropertiesPropertyName); - state.PushSchemaNode(parameterInfo.Name!); - - JsonSchema paramSchema = MapJsonSchemaCore( - ref state, - parameterTypeInfo, - parameterInfo: parameterInfo, - description: parameterDescription, - isNonNullableType: isNonNullableType); - - if (hasDefaultValue) - { - JsonSchema.EnsureMutable(ref paramSchema); - paramSchema.DefaultValue = defaultValue; - paramSchema.HasDefaultValue = true; - } - - state.PopSchemaNode(); - state.PopSchemaNode(); - - return paramSchema.ToJsonNode(configuration); - } - - private static JsonSchema MapJsonSchemaCore( - ref GenerationState state, - JsonTypeInfo typeInfo, - Type? parentType = null, - JsonPropertyInfo? propertyInfo = null, - ICustomAttributeProvider? propertyAttributeProvider = null, - ParameterInfo? parameterInfo = null, - bool isNonNullableType = false, - JsonConverter? customConverter = null, - JsonNumberHandling? customNumberHandling = null, - JsonTypeInfo? parentPolymorphicTypeInfo = null, - bool parentPolymorphicTypeContainsTypesWithoutDiscriminator = false, - bool parentPolymorphicTypeIsNonNullable = false, - KeyValuePair? typeDiscriminator = null, - string? description = null, - bool cacheResult = true) - { - Debug.Assert(typeInfo.IsReadOnly); - - if (cacheResult && state.TryPushType(typeInfo, propertyInfo, out string? existingJsonPointer)) - { - // We're generating the schema of a recursive type, return a reference pointing to the outermost schema. - return CompleteSchema(ref state, new JsonSchema { Ref = existingJsonPointer }); - } - - JsonSchema schema; - JsonConverter effectiveConverter = customConverter ?? typeInfo.Converter; - JsonNumberHandling effectiveNumberHandling = customNumberHandling ?? typeInfo.NumberHandling ?? typeInfo.Options.NumberHandling; - - if (!IsBuiltInConverter(effectiveConverter)) - { - // Return a `true` schema for types with user-defined converters. - return CompleteSchema(ref state, JsonSchema.True); - } - - if (state.Configuration.ResolveDescriptionAttributes) - { - description ??= typeInfo.Type.GetCustomAttribute()?.Description; - } - - if (parentPolymorphicTypeInfo is null && typeInfo.PolymorphismOptions is { DerivedTypes.Count: > 0 } polyOptions) - { - // This is the base type of a polymorphic type hierarchy. The schema for this type - // will include an "anyOf" property with the schemas for all derived types. - - string typeDiscriminatorKey = polyOptions.TypeDiscriminatorPropertyName; - List derivedTypes = polyOptions.DerivedTypes.ToList(); - - if (!typeInfo.Type.IsAbstract && !derivedTypes.Any(derived => derived.DerivedType == typeInfo.Type)) - { - // For non-abstract base types that haven't been explicitly configured, - // add a trivial schema to the derived types since we should support it. - derivedTypes.Add(new JsonDerivedType(typeInfo.Type)); - } - - bool containsTypesWithoutDiscriminator = derivedTypes.Exists(static derivedTypes => derivedTypes.TypeDiscriminator is null); - JsonSchemaType schemaType = JsonSchemaType.Any; - List? anyOf = new(derivedTypes.Count); - - state.PushSchemaNode(JsonSchemaConstants.AnyOfPropertyName); - - foreach (JsonDerivedType derivedType in derivedTypes) - { - Debug.Assert(derivedType.TypeDiscriminator is null or int or string); - - KeyValuePair? derivedTypeDiscriminator = null; - if (derivedType.TypeDiscriminator is { } discriminatorValue) - { - JsonNode discriminatorNode = discriminatorValue switch - { - string stringId => (JsonNode)stringId, - _ => (JsonNode)(int)discriminatorValue, - }; - - JsonSchema discriminatorSchema = new() { Constant = discriminatorNode }; - derivedTypeDiscriminator = new(typeDiscriminatorKey, discriminatorSchema); - } - - JsonTypeInfo derivedTypeInfo = typeInfo.Options.GetTypeInfo(derivedType.DerivedType); - - state.PushSchemaNode(anyOf.Count.ToString(CultureInfo.InvariantCulture)); - JsonSchema derivedSchema = MapJsonSchemaCore( - ref state, - derivedTypeInfo, - parentPolymorphicTypeInfo: typeInfo, - typeDiscriminator: derivedTypeDiscriminator, - parentPolymorphicTypeContainsTypesWithoutDiscriminator: containsTypesWithoutDiscriminator, - parentPolymorphicTypeIsNonNullable: isNonNullableType, - cacheResult: false); - - state.PopSchemaNode(); - - // Determine if all derived schemas have the same type. - if (anyOf.Count == 0) - { - schemaType = derivedSchema.Type; - } - else if (schemaType != derivedSchema.Type) - { - schemaType = JsonSchemaType.Any; - } - - anyOf.Add(derivedSchema); - } - - state.PopSchemaNode(); - - if (schemaType is not JsonSchemaType.Any) - { - // If all derived types have the same schema type, we can simplify the schema - // by moving the type keyword to the base schema and removing it from the derived schemas. - foreach (JsonSchema derivedSchema in anyOf) - { - derivedSchema.Type = JsonSchemaType.Any; - - if (derivedSchema.KeywordCount == 0) - { - // if removing the type results in an empty schema, - // remove the anyOf array entirely since it's always true. - anyOf = null; - break; - } - } - } - - schema = new() - { - Type = schemaType, - AnyOf = anyOf, - - // If all derived types have a discriminator, we can require it in the base schema. - Required = containsTypesWithoutDiscriminator ? null : new() { typeDiscriminatorKey }, - }; - - return CompleteSchema(ref state, schema); - } - - if (Nullable.GetUnderlyingType(typeInfo.Type) is Type nullableElementType) - { - JsonTypeInfo elementTypeInfo = typeInfo.Options.GetTypeInfo(nullableElementType); - customConverter = ExtractCustomNullableConverter(customConverter); - schema = MapJsonSchemaCore(ref state, elementTypeInfo, customConverter: customConverter, cacheResult: false); - - if (schema.Enum != null) - { - Debug.Assert(elementTypeInfo.Type.IsEnum, "The enum keyword should only be populated by schemas for enum types."); - schema.Enum.Add(null); // Append null to the enum array. - } - - return CompleteSchema(ref state, schema); - } - - switch (typeInfo.Kind) - { - case JsonTypeInfoKind.Object: - List>? properties = null; - List? required = null; - JsonSchema? additionalProperties = null; - - JsonUnmappedMemberHandling effectiveUnmappedMemberHandling = typeInfo.UnmappedMemberHandling ?? typeInfo.Options.UnmappedMemberHandling; - if (effectiveUnmappedMemberHandling is JsonUnmappedMemberHandling.Disallow) - { - // Disallow unspecified properties. - additionalProperties = JsonSchema.False; - } - - if (typeDiscriminator is { } typeDiscriminatorPair) - { - (properties ??= new()).Add(typeDiscriminatorPair); - if (parentPolymorphicTypeContainsTypesWithoutDiscriminator) - { - // Require the discriminator here since it's not common to all derived types. - (required ??= new()).Add(typeDiscriminatorPair.Key); - } - } - - Func? parameterInfoMapper = ResolveJsonConstructorParameterMapper(typeInfo); - - state.PushSchemaNode(JsonSchemaConstants.PropertiesPropertyName); - foreach (JsonPropertyInfo property in typeInfo.Properties) - { - if (property is { Get: null, Set: null } or { IsExtensionData: true }) - { - continue; // Skip JsonIgnored properties and extension data - } - - JsonNumberHandling? propertyNumberHandling = property.NumberHandling ?? effectiveNumberHandling; - JsonTypeInfo propertyTypeInfo = typeInfo.Options.GetTypeInfo(property.PropertyType); - - // Resolve the attribute provider for the property. - ICustomAttributeProvider? attributeProvider = ResolveAttributeProvider(typeInfo.Type, property); - - // Resolve property-level description attributes. - string? propertyDescription = state.Configuration.ResolveDescriptionAttributes - ? attributeProvider?.GetCustomAttributes(inherit: true).OfType().FirstOrDefault()?.Description - : null; - - // Declare the property as nullable if either getter or setter are nullable. - bool isNonNullableProperty = false; - if (attributeProvider is MemberInfo memberInfo) - { - NullabilityInfo nullabilityInfo = state.NullabilityInfoContext.GetMemberNullability(memberInfo); - isNonNullableProperty = - (property.Get is null || nullabilityInfo.ReadState is NullabilityState.NotNull) && - (property.Set is null || nullabilityInfo.WriteState is NullabilityState.NotNull); - } - - bool isRequired = property.IsRequired; - bool hasDefaultValue = false; - JsonNode? defaultValue = null; - - ParameterInfo? associatedParameter = parameterInfoMapper?.Invoke(property); - if (associatedParameter != null) - { - ResolveParameterInfo( - associatedParameter, - propertyTypeInfo, - state.NullabilityInfoContext, - state.Configuration, - out hasDefaultValue, - out defaultValue, - out bool isNonNullableParameter, - ref propertyDescription, - ref isRequired); - - isNonNullableProperty &= isNonNullableParameter; - } - - state.PushSchemaNode(property.Name); - JsonSchema propertySchema = MapJsonSchemaCore( - ref state, - propertyTypeInfo, - parentType: typeInfo.Type, - propertyInfo: property, - parameterInfo: associatedParameter, - propertyAttributeProvider: attributeProvider, - isNonNullableType: isNonNullableProperty, - description: propertyDescription, - customConverter: property.CustomConverter, - customNumberHandling: propertyNumberHandling); - - state.PopSchemaNode(); - - if (hasDefaultValue) - { - JsonSchema.EnsureMutable(ref propertySchema); - propertySchema.DefaultValue = defaultValue; - propertySchema.HasDefaultValue = true; - } - - (properties ??= new()).Add(new(property.Name, propertySchema)); - - if (isRequired) - { - (required ??= new()).Add(property.Name); - } - } - - state.PopSchemaNode(); - return CompleteSchema(ref state, new() - { - Type = JsonSchemaType.Object, - Properties = properties, - Required = required, - AdditionalProperties = additionalProperties, - }); - - case JsonTypeInfoKind.Enumerable: - Type elementType = GetElementType(typeInfo); - JsonTypeInfo elementTypeInfo = typeInfo.Options.GetTypeInfo(elementType); - - if (typeDiscriminator is null) - { - state.PushSchemaNode(JsonSchemaConstants.ItemsPropertyName); - JsonSchema items = MapJsonSchemaCore(ref state, elementTypeInfo, customNumberHandling: effectiveNumberHandling); - state.PopSchemaNode(); - - return CompleteSchema(ref state, new() - { - Type = JsonSchemaType.Array, - Items = items.IsTrue ? null : items, - }); - } - else - { - // Polymorphic enumerable types are represented using a wrapping object: - // { "$type" : "discriminator", "$values" : [element1, element2, ...] } - // Which corresponds to the schema - // { "properties" : { "$type" : { "const" : "discriminator" }, "$values" : { "type" : "array", "items" : { ... } } } } - const string ValuesKeyword = "$values"; - - state.PushSchemaNode(JsonSchemaConstants.PropertiesPropertyName); - state.PushSchemaNode(ValuesKeyword); - state.PushSchemaNode(JsonSchemaConstants.ItemsPropertyName); - - JsonSchema items = MapJsonSchemaCore(ref state, elementTypeInfo, customNumberHandling: effectiveNumberHandling); - - state.PopSchemaNode(); - state.PopSchemaNode(); - state.PopSchemaNode(); - - return CompleteSchema(ref state, new() - { - Type = JsonSchemaType.Object, - Properties = new() - { - typeDiscriminator.Value, - new(ValuesKeyword, - new JsonSchema() - { - Type = JsonSchemaType.Array, - Items = items.IsTrue ? null : items, - }), - }, - Required = parentPolymorphicTypeContainsTypesWithoutDiscriminator ? new() { typeDiscriminator.Value.Key } : null, - }); - } - - case JsonTypeInfoKind.Dictionary: - Type valueType = GetElementType(typeInfo); - JsonTypeInfo valueTypeInfo = typeInfo.Options.GetTypeInfo(valueType); - - List>? dictProps = null; - List? dictRequired = null; - - if (typeDiscriminator is { } dictDiscriminator) - { - dictProps = new() { dictDiscriminator }; - if (parentPolymorphicTypeContainsTypesWithoutDiscriminator) - { - // Require the discriminator here since it's not common to all derived types. - dictRequired = new() { dictDiscriminator.Key }; - } - } - - state.PushSchemaNode(JsonSchemaConstants.AdditionalPropertiesPropertyName); - JsonSchema valueSchema = MapJsonSchemaCore(ref state, valueTypeInfo, customNumberHandling: effectiveNumberHandling); - state.PopSchemaNode(); - - return CompleteSchema(ref state, new() - { - Type = JsonSchemaType.Object, - Properties = dictProps, - Required = dictRequired, - AdditionalProperties = valueSchema.IsTrue ? null : valueSchema, - }); - - default: - Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.None); - - if (s_simpleTypeSchemaFactories.TryGetValue(typeInfo.Type, out Func? simpleTypeSchemaFactory)) - { - schema = simpleTypeSchemaFactory(effectiveNumberHandling); - } - else if (typeInfo.Type.IsEnum) - { - schema = GetEnumConverterSchema(typeInfo, effectiveConverter, state.Configuration); - } - else - { - schema = JsonSchema.True; - } - - return CompleteSchema(ref state, schema); - } - - JsonSchema CompleteSchema(ref GenerationState state, JsonSchema schema) - { - if (schema.Ref is null) - { - if (state.Configuration.IncludeSchemaVersion && state.CurrentDepth == 0) - { - JsonSchema.EnsureMutable(ref schema); - schema.Schema = SchemaVersion; - } - - if (description is not null) - { - JsonSchema.EnsureMutable(ref schema); - schema.Description = description; - } - - // A schema is marked as nullable if either - // 1. We have a schema for a property where either the getter or setter are marked as nullable. - // 2. We have a schema for a reference type, unless we're explicitly treating null-oblivious types as non-nullable. - bool isNullableSchema = (propertyInfo != null || parameterInfo != null) - ? !isNonNullableType - : CanBeNull(typeInfo.Type) && !parentPolymorphicTypeIsNonNullable && !state.Configuration.TreatNullObliviousAsNonNullable; - - if (isNullableSchema) - { - schema.MakeNullable(); - } - - if (cacheResult) - { - state.PopGeneratedType(); - } - } - - if (state.Configuration.TransformSchemaNode != null) - { - // Prime the schema for invocation by the JsonNode transformer. - schema.GenerationContext = new(typeInfo, parentType, propertyInfo, parameterInfo, propertyAttributeProvider); - } - - return schema; - } - } - - private readonly ref struct GenerationState - { - private readonly List _currentPath; - private readonly List<(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo, int depth)> _generationStack; - private readonly int _maxDepth; - - public GenerationState(JsonSchemaMapperConfiguration configuration, JsonSerializerOptions options, NullabilityInfoContext? nullabilityInfoContext = null) - { - Configuration = configuration; - NullabilityInfoContext = nullabilityInfoContext ?? new(); - _maxDepth = options.MaxDepth is 0 ? 64 : options.MaxDepth; - _generationStack = new(); - _currentPath = new(); - } - - public JsonSchemaMapperConfiguration Configuration { get; } - public NullabilityInfoContext NullabilityInfoContext { get; } - public int CurrentDepth => _currentPath.Count; - - public void PushSchemaNode(string nodeId) - { - if (CurrentDepth == _maxDepth) - { - ThrowHelpers.ThrowInvalidOperationException_MaxDepthReached(); - } - - _currentPath.Add(nodeId); - } - - public void PopSchemaNode() - { - _currentPath.RemoveAt(_currentPath.Count - 1); - } - - /// - /// Pushes the current type/property to the generation stack or returns a JSON pointer if the type is recursive. - /// - public bool TryPushType(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo, [NotNullWhen(true)] out string? existingJsonPointer) - { - foreach ((JsonTypeInfo otherTypeInfo, JsonPropertyInfo? otherPropertyInfo, int depth) in _generationStack) - { - if (typeInfo == otherTypeInfo && propertyInfo == otherPropertyInfo) - { - existingJsonPointer = FormatJsonPointer(_currentPath, depth); - return true; - } - } - - _generationStack.Add((typeInfo, propertyInfo, CurrentDepth)); - existingJsonPointer = null; - return false; - } - - public void PopGeneratedType() - { - Debug.Assert(_generationStack.Count > 0); - _generationStack.RemoveAt(_generationStack.Count - 1); - } - - private static string FormatJsonPointer(List currentPathList, int depth) - { - Debug.Assert(0 <= depth && depth < currentPathList.Count); - - if (depth == 0) - { - return "#"; - } - - StringBuilder sb = new(); - sb.Append('#'); - - for (int i = 0; i < depth; i++) - { - string segment = currentPathList[i]; - if (segment.AsSpan().IndexOfAny('~', '/') != -1) - { - segment = segment.Replace("~", "~0").Replace("/", "~1"); - } - - sb.Append('/'); - sb.Append(segment); - } - - return sb.ToString(); - } - } - - private static readonly Dictionary> s_simpleTypeSchemaFactories = new() - { - [typeof(object)] = _ => JsonSchema.True, - [typeof(bool)] = _ => new JsonSchema { Type = JsonSchemaType.Boolean }, - [typeof(byte)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), - [typeof(ushort)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), - [typeof(uint)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), - [typeof(ulong)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), - [typeof(sbyte)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), - [typeof(short)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), - [typeof(int)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), - [typeof(long)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), - [typeof(float)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Number, numberHandling, isIeeeFloatingPoint: true), - [typeof(double)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Number, numberHandling, isIeeeFloatingPoint: true), - [typeof(decimal)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Number, numberHandling), -#if NET6_0_OR_GREATER - [typeof(Half)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Number, numberHandling, isIeeeFloatingPoint: true), -#endif -#if NET7_0_OR_GREATER - [typeof(UInt128)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), - [typeof(Int128)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), -#endif - [typeof(char)] = _ => new JsonSchema { Type = JsonSchemaType.String, MinLength = 1, MaxLength = 1 }, - [typeof(string)] = _ => new JsonSchema { Type = JsonSchemaType.String }, - [typeof(byte[])] = _ => new JsonSchema { Type = JsonSchemaType.String }, - [typeof(Memory)] = _ => new JsonSchema { Type = JsonSchemaType.String }, - [typeof(ReadOnlyMemory)] = _ => new JsonSchema { Type = JsonSchemaType.String }, - [typeof(DateTime)] = _ => new JsonSchema { Type = JsonSchemaType.String, Format = "date-time" }, - [typeof(DateTimeOffset)] = _ => new JsonSchema { Type = JsonSchemaType.String, Format = "date-time" }, - [typeof(TimeSpan)] = _ => new JsonSchema - { - Comment = "Represents a System.TimeSpan value.", - Type = JsonSchemaType.String, - Pattern = @"^-?(\d+\.)?\d{2}:\d{2}:\d{2}(\.\d{1,7})?$", - }, - -#if NET6_0_OR_GREATER - [typeof(DateOnly)] = _ => new JsonSchema { Type = JsonSchemaType.String, Format = "date" }, - [typeof(TimeOnly)] = _ => new JsonSchema { Type = JsonSchemaType.String, Format = "time" }, -#endif - [typeof(Guid)] = _ => new JsonSchema { Type = JsonSchemaType.String, Format = "uuid" }, - [typeof(Uri)] = _ => new JsonSchema { Type = JsonSchemaType.String, Format = "uri" }, - [typeof(Version)] = _ => new JsonSchema - { - Comment = "Represents a version string.", - Type = JsonSchemaType.String, - Pattern = @"^\d+(\.\d+){1,3}$", - }, - - [typeof(JsonDocument)] = _ => new JsonSchema { Type = JsonSchemaType.Any }, - [typeof(JsonElement)] = _ => new JsonSchema { Type = JsonSchemaType.Any }, - [typeof(JsonNode)] = _ => new JsonSchema { Type = JsonSchemaType.Any }, - [typeof(JsonValue)] = _ => new JsonSchema { Type = JsonSchemaType.Any }, - [typeof(JsonObject)] = _ => new JsonSchema { Type = JsonSchemaType.Object }, - [typeof(JsonArray)] = _ => new JsonSchema { Type = JsonSchemaType.Array }, - }; - - // Adapted from https://github.com/dotnet/runtime/blob/d606c601510c1a1a28cb6ef3550f12db049c0776/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonPrimitiveConverter.cs#L36-L69 - private static JsonSchema GetSchemaForNumericType(JsonSchemaType schemaType, JsonNumberHandling numberHandling, bool isIeeeFloatingPoint = false) - { - Debug.Assert(schemaType is JsonSchemaType.Integer or JsonSchemaType.Number); - Debug.Assert(!isIeeeFloatingPoint || schemaType is JsonSchemaType.Number); - - string? pattern = null; - - if ((numberHandling & (JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)) != 0) - { - pattern = schemaType is JsonSchemaType.Integer - ? @"^-?(?:0|[1-9]\d*)$" - : isIeeeFloatingPoint - ? @"^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$" - : @"^-?(?:0|[1-9]\d*)(?:\.\d+)?$"; - - schemaType |= JsonSchemaType.String; - } - - if (isIeeeFloatingPoint && (numberHandling & JsonNumberHandling.AllowNamedFloatingPointLiterals) != 0) - { - return new JsonSchema - { - AnyOf = new() - { - new JsonSchema { Type = schemaType, Pattern = pattern }, - new JsonSchema { Enum = new() { (JsonNode)"NaN", (JsonNode)"Infinity", (JsonNode)"-Infinity" } }, - }, - }; - } - - return new JsonSchema { Type = schemaType, Pattern = pattern }; - } - - // Uses reflection to determine the element type of an enumerable or dictionary type - // Workaround for https://github.com/dotnet/runtime/issues/77306#issuecomment-2007887560 - private static Type GetElementType(JsonTypeInfo typeInfo) - { - Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary); - s_elementTypeProperty ??= typeof(JsonTypeInfo).GetProperty("ElementType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - return (Type)s_elementTypeProperty?.GetValue(typeInfo)!; - } - - private static PropertyInfo? s_elementTypeProperty; - - // The source generator currently doesn't populate attribute providers for properties - // cf. https://github.com/dotnet/runtime/issues/100095 - // Work around the issue by running a query for the relevant MemberInfo using the internal MemberName property - // https://github.com/dotnet/runtime/blob/de774ff9ee1a2c06663ab35be34b755cd8d29731/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs#L206 -#if NETCOREAPP - [EditorBrowsable(EditorBrowsableState.Never)] - [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", - Justification = "We're reading the internal JsonPropertyInfo.MemberName which cannot have been trimmed away.")] - [UnconditionalSuppressMessage("Trimming", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations.", - Justification = "We're reading the member which is already accessed by the source generator.")] -#endif - private static ICustomAttributeProvider? ResolveAttributeProvider(Type? declaringType, JsonPropertyInfo? propertyInfo) - { - if (declaringType is null || propertyInfo is null) - { - return null; - } - - if (propertyInfo.AttributeProvider is { } provider) - { - return provider; - } - - s_memberNameProperty ??= typeof(JsonPropertyInfo).GetProperty("MemberName", BindingFlags.Instance | BindingFlags.NonPublic)!; - var memberName = (string?)s_memberNameProperty.GetValue(propertyInfo); - if (memberName is not null) - { - return declaringType.GetMember(memberName, MemberTypes.Property | MemberTypes.Field, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(); - } - - return null; - } - - private static PropertyInfo? s_memberNameProperty; - - // Uses reflection to determine any custom converters specified for the element of a nullable type. -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2026", - Justification = "We're resolving private fields of the built-in Nullable converter which cannot have been trimmed away.")] -#endif - private static JsonConverter? ExtractCustomNullableConverter(JsonConverter? converter) - { - Debug.Assert(converter is null || IsBuiltInConverter(converter)); - - // There is unfortunately no way in which we can obtain the element converter from a nullable converter without resorting to private reflection - // https://github.com/dotnet/runtime/blob/5fda47434cecc590095e9aef3c4e560b7b7ebb47/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs#L15-L17 - Type? converterType = converter?.GetType(); - if (converterType?.Name == "NullableConverter`1") - { - FieldInfo elementConverterField = converterType.GetPrivateFieldWithPotentiallyTrimmedMetadata("_elementConverter"); - return (JsonConverter)elementConverterField!.GetValue(converter)!; - } - - return null; - } - - // Uses reflection to determine schema for enum types - // Adapted from https://github.com/dotnet/runtime/blob/d606c601510c1a1a28cb6ef3550f12db049c0776/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs#L498-L521 -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2026", - Justification = "We're resolving private fields of the built-in enum converter which cannot have been trimmed away.")] -#endif - private static JsonSchema GetEnumConverterSchema(JsonTypeInfo typeInfo, JsonConverter converter, JsonSchemaMapperConfiguration configuration) - { - Debug.Assert(typeInfo.Type.IsEnum && IsBuiltInConverter(converter)); - - if (converter is JsonConverterFactory factory) - { - converter = factory.CreateConverter(typeInfo.Type, typeInfo.Options)!; - } - - Type converterType = converter.GetType(); - FieldInfo converterOptionsField = converterType.GetPrivateFieldWithPotentiallyTrimmedMetadata("_converterOptions"); - FieldInfo namingPolicyField = converterType.GetPrivateFieldWithPotentiallyTrimmedMetadata("_namingPolicy"); - - const int EnumConverterOptionsAllowStrings = 1; - var converterOptions = (int)converterOptionsField!.GetValue(converter)!; - if ((converterOptions & EnumConverterOptionsAllowStrings) != 0) - { - // This explicitly ignores the integer component in converters configured as AllowNumbers | AllowStrings - // which is the default for JsonStringEnumConverter. This sacrifices some precision in the schema for simplicity. - - if (typeInfo.Type.GetCustomAttribute() is not null) - { - // Do not report enum values in case of flags. - return new() { Type = JsonSchemaType.String }; - } - - var namingPolicy = (JsonNamingPolicy?)namingPolicyField!.GetValue(converter)!; - JsonArray enumValues = new(); - foreach (string name in Enum.GetNames(typeInfo.Type)) - { - // This does not account for custom names specified via the new - // JsonStringEnumMemberNameAttribute introduced in .NET 9. - string effectiveName = namingPolicy?.ConvertName(name) ?? name; - enumValues.Add((JsonNode)effectiveName); - } - - JsonSchema schema = new() { Enum = enumValues }; - if (configuration.IncludeTypeInEnums) - { - schema.Type = JsonSchemaType.String; - } - - return schema; - } - - return new() { Type = JsonSchemaType.Integer }; - } - -#if NETCOREAPP - [RequiresUnreferencedCode("Resolves unreferenced member metadata.")] -#endif - private static FieldInfo GetPrivateFieldWithPotentiallyTrimmedMetadata(this Type type, string fieldName) - { - FieldInfo? field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); - if (field is null) - { - throw new InvalidOperationException( - $"Could not resolve metadata for field '{fieldName}' in type '{type}'. " + - "If running Native AOT ensure that the 'IlcTrimMetadata' property has been disabled."); - } - - return field; - } - - // Resolves the parameters of the deserialization constructor for a type, if they exist. -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", - Justification = "The deserialization constructor should have already been referenced by the source generator and therefore will not have been trimmed.")] -#endif - private static Func? ResolveJsonConstructorParameterMapper(JsonTypeInfo typeInfo) - { - Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object); - - if (typeInfo.Properties.Count > 0 && - typeInfo.CreateObject is null && // Ensure that a default constructor isn't being used - typeInfo.Type.TryGetDeserializationConstructor(useDefaultCtorInAnnotatedStructs: true, out ConstructorInfo? ctor)) - { - ParameterInfo[]? parameters = ctor?.GetParameters(); - if (parameters?.Length > 0) - { - Dictionary dict = new(parameters.Length); - foreach (ParameterInfo parameter in parameters) - { - if (parameter.Name is not null) - { - // We don't care about null parameter names or conflicts since they - // would have already been rejected by JsonTypeInfo configuration. - dict[new(parameter.Name, parameter.ParameterType)] = parameter; - } - } - - return prop => dict.TryGetValue(new(prop.Name, prop.PropertyType), out ParameterInfo? parameter) ? parameter : null; - } - } - - return null; - } - - // Parameter to property matching semantics as declared in - // https://github.com/dotnet/runtime/blob/12d96ccfaed98e23c345188ee08f8cfe211c03e7/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs#L1007-L1030 - private readonly struct ParameterLookupKey : IEquatable - { - public ParameterLookupKey(string name, Type type) - { - Name = name; - Type = type; - } - - public string Name { get; } - public Type Type { get; } - - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Name); - public bool Equals(ParameterLookupKey other) => Type == other.Type && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase); - public override bool Equals(object? obj) => obj is ParameterLookupKey key && Equals(key); - } - - // Resolves the deserialization constructor for a type using logic copied from - // https://github.com/dotnet/runtime/blob/e12e2fa6cbdd1f4b0c8ad1b1e2d960a480c21703/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs#L227-L286 - private static bool TryGetDeserializationConstructor( -#if NETCOREAPP - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] -#endif - this Type type, - bool useDefaultCtorInAnnotatedStructs, - out ConstructorInfo? deserializationCtor) - { - ConstructorInfo? ctorWithAttribute = null; - ConstructorInfo? publicParameterlessCtor = null; - ConstructorInfo? lonePublicCtor = null; - - ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); - - if (constructors.Length == 1) - { - lonePublicCtor = constructors[0]; - } - - foreach (ConstructorInfo constructor in constructors) - { - if (HasJsonConstructorAttribute(constructor)) - { - if (ctorWithAttribute != null) - { - deserializationCtor = null; - return false; - } - - ctorWithAttribute = constructor; - } - else if (constructor.GetParameters().Length == 0) - { - publicParameterlessCtor = constructor; - } - } - - // Search for non-public ctors with [JsonConstructor]. - foreach (ConstructorInfo constructor in type.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)) - { - if (HasJsonConstructorAttribute(constructor)) - { - if (ctorWithAttribute != null) - { - deserializationCtor = null; - return false; - } - - ctorWithAttribute = constructor; - } - } - - // Structs will use default constructor if attribute isn't used. - if (useDefaultCtorInAnnotatedStructs && type.IsValueType && ctorWithAttribute == null) - { - deserializationCtor = null; - return true; - } - - deserializationCtor = ctorWithAttribute ?? publicParameterlessCtor ?? lonePublicCtor; - return true; - - static bool HasJsonConstructorAttribute(ConstructorInfo constructorInfo) => - constructorInfo.GetCustomAttribute() != null; - } - - private static bool IsBuiltInConverter(JsonConverter converter) => - converter.GetType().Assembly == typeof(JsonConverter).Assembly; - - // Resolves the nullable reference type annotations for a property or field, - // additionally addressing a few known bugs of the NullabilityInfo pre .NET 9. - private static NullabilityInfo GetMemberNullability(this NullabilityInfoContext context, MemberInfo memberInfo) - { - Debug.Assert(memberInfo is PropertyInfo or FieldInfo); - return memberInfo is PropertyInfo prop - ? context.Create(prop) - : context.Create((FieldInfo)memberInfo); - } - - private static bool CanBeNull(Type type) => !type.IsValueType || Nullable.GetUnderlyingType(type) is not null; - - private static partial class ThrowHelpers - { - [DoesNotReturn] - public static void ThrowInvalidOperationException_MaxDepthReached() => - throw new InvalidOperationException("The depth of the generated JSON schema exceeds the JsonSerializerOptions.MaxDepth setting."); - } -} -#endif diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv9.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv9.cs deleted file mode 100644 index 82c7fd005cfc..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv9.cs +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// Source copied from https://github.com/eiriktsarpalis/stj-schema-mapper -// It should be kept in sync with any changes made in that repo, -// and should be removed once the relevant replacements are available in STJv9. - -#if NET9_0_OR_GREATER || SYSTEM_TEXT_JSON_V9 -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Schema; -using System.Text.Json.Serialization.Metadata; -using System.Threading.Tasks; - -namespace JsonSchemaMapper; - -#if EXPOSE_JSON_SCHEMA_MAPPER -public -#else -internal -#endif - static partial class JsonSchemaMapper -{ - // For System.Text.Json v9 or greater, JsonSchemaMapper is implemented as a shim over the - // built-in JsonSchemaExporter component. Added functionality is implemented by performing - // fix-ups over the generated schema. - - private static partial JsonNode MapRootTypeJsonSchema(JsonTypeInfo typeInfo, JsonSchemaMapperConfiguration configuration) - { - JsonSchemaExporterOptions exporterOptions = new() - { - TreatNullObliviousAsNonNullable = configuration.TreatNullObliviousAsNonNullable, - TransformSchemaNode = (JsonSchemaExporterContext ctx, JsonNode schema) => ApplySchemaTransformations(schema, ctx, configuration), - }; - - return JsonSchemaExporter.GetJsonSchemaAsNode(typeInfo, exporterOptions); - } - - private static partial JsonNode MapMethodParameterJsonSchema( - ParameterInfo parameterInfo, - JsonTypeInfo parameterTypeInfo, - JsonSchemaMapperConfiguration configuration, - NullabilityInfoContext nullabilityContext, - out bool isRequired) - { - Debug.Assert(parameterInfo.Name != null); - - JsonSchemaExporterOptions exporterOptions = new() - { - TreatNullObliviousAsNonNullable = configuration.TreatNullObliviousAsNonNullable, - TransformSchemaNode = (JsonSchemaExporterContext ctx, JsonNode schema) => ApplySchemaTransformations(schema, ctx, configuration, parameterInfo.Name), - }; - - string? parameterDescription = null; - isRequired = false; - - ResolveParameterInfo( - parameterInfo, - parameterTypeInfo, - nullabilityContext, - configuration, - out bool hasDefaultValue, - out JsonNode? defaultValue, - out bool isNonNullableType, - ref parameterDescription, - ref isRequired); - - JsonNode parameterSchema = JsonSchemaExporter.GetJsonSchemaAsNode(parameterTypeInfo, exporterOptions); - - if (parameterDescription is not null) - { - ConvertSchemaToObject(ref parameterSchema).Insert(0, JsonSchemaConstants.DescriptionPropertyName, (JsonNode)parameterDescription); - } - - if (hasDefaultValue) - { - ConvertSchemaToObject(ref parameterSchema).Add(JsonSchemaConstants.DefaultPropertyName, defaultValue); - } - - if (isNonNullableType && - parameterSchema is JsonObject parameterSchemaObj && - parameterSchemaObj.TryGetPropertyValue(JsonSchemaConstants.TypePropertyName, out JsonNode? typeSchema) && - typeSchema is JsonArray typeArray) - { - for (int i = 0; i < typeArray.Count; i++) - { - if (typeArray[i]!.GetValue() is "null") - { - typeArray.RemoveAt(i); - break; - } - } - - if (typeArray.Count == 1) - { - parameterSchemaObj[JsonSchemaConstants.TypePropertyName] = (JsonNode)(string)typeArray[0]!; - } - } - - return parameterSchema; - } - - private static JsonNode ApplySchemaTransformations( - JsonNode schema, - JsonSchemaExporterContext ctx, - JsonSchemaMapperConfiguration configuration, - string? parameterName = null) - { - JsonSchemaGenerationContext mapperCtx = new( - ctx.TypeInfo, - ctx.TypeInfo.Type, - ctx.PropertyInfo, - (ParameterInfo?)ctx.PropertyInfo?.AssociatedParameter?.AttributeProvider, - ctx.PropertyInfo?.AttributeProvider); - - if (configuration.IncludeTypeInEnums) - { - if (ctx.TypeInfo.Type.IsEnum && - schema is JsonObject enumSchema && - enumSchema.ContainsKey(JsonSchemaConstants.EnumPropertyName)) - { - enumSchema.Insert(0, JsonSchemaConstants.TypePropertyName, (JsonNode)"string"); - } - else if ( - Nullable.GetUnderlyingType(ctx.TypeInfo.Type) is Type { IsEnum: true } && - schema is JsonObject nullableEnumSchema && - nullableEnumSchema.ContainsKey(JsonSchemaConstants.EnumPropertyName)) - { - nullableEnumSchema.Insert(0, JsonSchemaConstants.TypePropertyName, new JsonArray() { (JsonNode)"string", (JsonNode)"null" }); - } - } - - if (configuration.ResolveDescriptionAttributes && mapperCtx.GetAttribute() is DescriptionAttribute attr) - { - ConvertSchemaToObject(ref schema).Insert(0, JsonSchemaConstants.DescriptionPropertyName, (JsonNode)attr.Description); - } - - if (parameterName is null && configuration.IncludeSchemaVersion && ctx.Path.IsEmpty) - { - ConvertSchemaToObject(ref schema).Insert(0, JsonSchemaConstants.SchemaPropertyName, (JsonNode)SchemaVersion); - } - - if (configuration.TransformSchemaNode is { } callback) - { - schema = callback(mapperCtx, schema); - } - - if (parameterName != null && schema is JsonObject refObj && - refObj.TryGetPropertyValue(JsonSchemaConstants.RefPropertyName, out JsonNode? paramName)) - { - // Fix up any $ref URIs to match the path from the root document. - string refUri = paramName!.GetValue(); - Debug.Assert(refUri is "#" || refUri.StartsWith("#/", StringComparison.Ordinal)); - refUri = refUri == "#" - ? $"#/{JsonSchemaConstants.PropertiesPropertyName}/{parameterName}" - : $"#/{JsonSchemaConstants.PropertiesPropertyName}/{parameterName}/{refUri[2..]}"; - - refObj[JsonSchemaConstants.RefPropertyName] = (JsonNode)refUri; - } - - return schema; - } - - private static JsonObject ConvertSchemaToObject(ref JsonNode schema) - { - JsonObject jObj; - - switch (schema.GetValueKind()) - { - case JsonValueKind.Object: - return (JsonObject)schema; - - case JsonValueKind.False: - schema = jObj = new() { [JsonSchemaConstants.NotPropertyName] = true }; - return jObj; - - default: - Debug.Assert(schema.GetValueKind() is JsonValueKind.True, "invalid schema type."); - schema = jObj = new JsonObject(); - return jObj; - } - } -} -#endif diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.cs deleted file mode 100644 index c19a56330e4c..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.cs +++ /dev/null @@ -1,415 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// Source copied from https://github.com/eiriktsarpalis/stj-schema-mapper -// It should be kept in sync with any changes made in that repo, -// and should be removed once the relevant replacements are available in STJv9. - -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; - -namespace JsonSchemaMapper; - -/// -/// Maps .NET types to JSON schema objects using contract metadata from instances. -/// -#if EXPOSE_JSON_SCHEMA_MAPPER -public -#else -[ExcludeFromCodeCoverage] -internal -#endif - static partial class JsonSchemaMapper -{ - /// - /// The JSON schema draft version used by the generated schemas. - /// - public const string SchemaVersion = "https://json-schema.org/draft/2020-12/schema"; - - /// - /// Generates a JSON schema corresponding to the contract metadata of the specified type. - /// - /// The options instance from which to resolve the contract metadata. - /// The root type for which to generate the JSON schema. - /// The configuration object controlling the schema generation. - /// A new instance defining the JSON schema for . - /// One of the specified parameters is . - /// The parameter contains unsupported configuration. - public static JsonNode GetJsonSchema(this JsonSerializerOptions options, Type type, JsonSchemaMapperConfiguration? configuration = null) - { - if (options is null) - { - ThrowHelpers.ThrowArgumentNullException(nameof(options)); - } - - if (type is null) - { - ThrowHelpers.ThrowArgumentNullException(nameof(type)); - } - - ValidateOptions(options); - configuration ??= JsonSchemaMapperConfiguration.Default; - JsonTypeInfo typeInfo = options.GetTypeInfo(type); - return MapRootTypeJsonSchema(typeInfo, configuration); - } - - /// - /// Generates a JSON object schema with properties corresponding to the specified method parameters. - /// - /// The options instance from which to resolve the contract metadata. - /// The method from whose parameters to generate the JSON schema. - /// The configuration object controlling the schema generation. - /// A new instance defining the JSON schema for . - /// One of the specified parameters is . - /// The parameter contains unsupported configuration. - public static JsonNode GetJsonSchema(this JsonSerializerOptions options, MethodBase method, JsonSchemaMapperConfiguration? configuration = null) - { - if (options is null) - { - ThrowHelpers.ThrowArgumentNullException(nameof(options)); - } - - if (method is null) - { - ThrowHelpers.ThrowArgumentNullException(nameof(method)); - } - - ValidateOptions(options); - configuration ??= JsonSchemaMapperConfiguration.Default; - - JsonObject schema = new(); - - if (configuration.IncludeSchemaVersion) - { - schema.Add(JsonSchemaConstants.SchemaPropertyName, SchemaVersion); - } - - schema.Add(JsonSchemaConstants.TitlePropertyName, method.Name); - - if (configuration.ResolveDescriptionAttributes && - method.GetCustomAttribute() is DescriptionAttribute attr) - { - schema.Add(JsonSchemaConstants.DescriptionPropertyName, attr.Description); - } - - schema.Add(JsonSchemaConstants.TypePropertyName, "object"); - - NullabilityInfoContext nullabilityInfoContext = new(); - JsonObject? paramSchemas = null; - JsonArray? requiredParams = null; - - foreach (ParameterInfo parameterInfo in method.GetParameters()) - { - if (parameterInfo.Name is null) - { - ThrowHelpers.ThrowInvalidOperationException_TrimmedMethodParameters(method); - } - - JsonTypeInfo parameterTypeInfo = options.GetTypeInfo(parameterInfo.ParameterType); - JsonNode parameterSchema = MapMethodParameterJsonSchema( - parameterInfo, - parameterTypeInfo, - configuration, - nullabilityInfoContext, - out bool isRequired); - - (paramSchemas ??= new()).Add(parameterInfo.Name, parameterSchema); - if (isRequired) - { - (requiredParams ??= new()).Add((JsonNode)parameterInfo.Name); - } - } - - if (paramSchemas != null) - { - schema.Add(JsonSchemaConstants.PropertiesPropertyName, paramSchemas); - } - - if (requiredParams != null) - { - schema.Add(JsonSchemaConstants.RequiredPropertyName, requiredParams); - } - - return schema; - } - - /// - /// Generates a JSON schema corresponding to the specified contract metadata. - /// - /// The contract metadata for which to generate the schema. - /// The configuration object controlling the schema generation. - /// A new instance defining the JSON schema for . - /// One of the specified parameters is . - /// The parameter contains unsupported configuration. - public static JsonNode GetJsonSchema(this JsonTypeInfo typeInfo, JsonSchemaMapperConfiguration? configuration = null) - { - if (typeInfo is null) - { - ThrowHelpers.ThrowArgumentNullException(nameof(typeInfo)); - } - - ValidateOptions(typeInfo.Options); - typeInfo.MakeReadOnly(); - configuration ??= JsonSchemaMapperConfiguration.Default; - return MapRootTypeJsonSchema(typeInfo, configuration); - } - - /// - /// Renders the specified instance as a JSON string. - /// - /// The node to serialize. - /// Whether to indent the resultant JSON text. - /// The JSON node rendered as a JSON string. - public static string ToJsonString(this JsonNode? node, bool writeIndented = false) - { - return node is null ? "null" : node.ToJsonString(writeIndented ? s_writeIndentedOptions : null); - } - - private static readonly JsonSerializerOptions s_writeIndentedOptions = new() { WriteIndented = true }; - - private static partial JsonNode MapRootTypeJsonSchema(JsonTypeInfo typeInfo, JsonSchemaMapperConfiguration configuration); - - private static partial JsonNode MapMethodParameterJsonSchema( - ParameterInfo parameterInfo, - JsonTypeInfo parameterTypeInfo, - JsonSchemaMapperConfiguration configuration, - NullabilityInfoContext nullabilityContext, - out bool isRequired); - - private static void ValidateOptions(JsonSerializerOptions options) - { - if (options.ReferenceHandler == ReferenceHandler.Preserve) - { - ThrowHelpers.ThrowNotSupportedException_ReferenceHandlerPreserveNotSupported(); - } - - options.MakeReadOnly(); - } - - private static void ResolveParameterInfo( - ParameterInfo parameter, - JsonTypeInfo parameterTypeInfo, - NullabilityInfoContext nullabilityInfoContext, - JsonSchemaMapperConfiguration configuration, - out bool hasDefaultValue, - out JsonNode? defaultValue, - out bool isNonNullable, - ref string? description, - ref bool isRequired) - { - Debug.Assert(parameterTypeInfo.Type == parameter.ParameterType); - - if (configuration.ResolveDescriptionAttributes) - { - // Resolve parameter-level description attributes. - description ??= parameter.GetCustomAttribute()?.Description; - } - - // Incorporate the nullability information from the parameter. - isNonNullable = nullabilityInfoContext.GetParameterNullability(parameter) is NullabilityState.NotNull; - - if (parameter.HasDefaultValue) - { - // Append the default value to the description. - object? defaultVal = parameter.GetNormalizedDefaultValue(); - defaultValue = JsonSerializer.SerializeToNode(defaultVal, parameterTypeInfo); - hasDefaultValue = true; - } - else - { - // Parameter is not optional, mark as required. - isRequired = true; - defaultValue = null; - hasDefaultValue = false; - } - } - - private static NullabilityState GetParameterNullability(this NullabilityInfoContext context, ParameterInfo parameterInfo) - { -#if !NET9_0_OR_GREATER - // Workaround for https://github.com/dotnet/runtime/issues/92487 - if (GetGenericParameterDefinition(parameterInfo) is { ParameterType: { IsGenericParameter: true } typeParam }) - { - // Step 1. Look for nullable annotations on the type parameter. - if (GetNullableFlags(typeParam) is byte[] flags) - { - return TranslateByte(flags[0]); - } - - // Step 2. Look for nullable annotations on the generic method declaration. - if (typeParam.DeclaringMethod != null && GetNullableContextFlag(typeParam.DeclaringMethod) is byte flag) - { - return TranslateByte(flag); - } - - // Step 3. Look for nullable annotations on the generic method declaration. - if (GetNullableContextFlag(typeParam.DeclaringType!) is byte flag2) - { - return TranslateByte(flag2); - } - - // Default to nullable. - return NullabilityState.Nullable; - -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", - Justification = "We're resolving private fields of the built-in enum converter which cannot have been trimmed away.")] -#endif - static byte[]? GetNullableFlags(MemberInfo member) - { - Attribute? attr = member.GetCustomAttributes().FirstOrDefault(attr => - { - Type attrType = attr.GetType(); - return attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableAttribute"; - }); - - return (byte[])attr?.GetType().GetField("NullableFlags")?.GetValue(attr)!; - } - -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", - Justification = "We're resolving private fields of the built-in enum converter which cannot have been trimmed away.")] -#endif - static byte? GetNullableContextFlag(MemberInfo member) - { - Attribute? attr = member.GetCustomAttributes().FirstOrDefault(attr => - { - Type attrType = attr.GetType(); - return attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableContextAttribute"; - }); - - return (byte?)attr?.GetType().GetField("Flag")?.GetValue(attr)!; - } - - static NullabilityState TranslateByte(byte b) - { - return b switch - { - 1 => NullabilityState.NotNull, - 2 => NullabilityState.Nullable, - _ => NullabilityState.Unknown - }; - } - } - - static ParameterInfo GetGenericParameterDefinition(ParameterInfo parameter) - { - if (parameter.Member is { DeclaringType.IsConstructedGenericType: true } - or MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false }) - { - var genericMethod = (MethodBase)GetGenericMemberDefinition(parameter.Member); - return genericMethod.GetParameters()[parameter.Position]; - } - - return parameter; - } - -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", - Justification = "Looking up the generic member definition of the provided member.")] -#endif - static MemberInfo GetGenericMemberDefinition(MemberInfo member) - { - if (member is Type type) - { - return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type; - } - - if (member.DeclaringType!.IsConstructedGenericType) - { - const BindingFlags AllMemberFlags = - BindingFlags.Static | BindingFlags.Instance | - BindingFlags.Public | BindingFlags.NonPublic; - - return member.DeclaringType.GetGenericTypeDefinition() - .GetMember(member.Name, AllMemberFlags) - .First(m => m.MetadataToken == member.MetadataToken); - } - - if (member is MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false } method) - { - return method.GetGenericMethodDefinition(); - } - - return member; - } -#endif - return context.Create(parameterInfo).WriteState; - } - - // Taken from https://github.com/dotnet/runtime/blob/903bc019427ca07080530751151ea636168ad334/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs#L288-L317 - private static object? GetNormalizedDefaultValue(this ParameterInfo parameterInfo) - { - Type parameterType = parameterInfo.ParameterType; - object? defaultValue = parameterInfo.DefaultValue; - - if (defaultValue is null) - { - return null; - } - - // DBNull.Value is sometimes used as the default value (returned by reflection) of nullable params in place of null. - if (defaultValue == DBNull.Value && parameterType != typeof(DBNull)) - { - return null; - } - - // Default values of enums or nullable enums are represented using the underlying type and need to be cast explicitly - // cf. https://github.com/dotnet/runtime/issues/68647 - if (parameterType.IsEnum) - { - return Enum.ToObject(parameterType, defaultValue); - } - - if (Nullable.GetUnderlyingType(parameterType) is Type underlyingType && underlyingType.IsEnum) - { - return Enum.ToObject(underlyingType, defaultValue); - } - - return defaultValue; - } - - private static class JsonSchemaConstants - { - public const string SchemaPropertyName = "$schema"; - public const string RefPropertyName = "$ref"; - public const string CommentPropertyName = "$comment"; - public const string TitlePropertyName = "title"; - public const string DescriptionPropertyName = "description"; - public const string TypePropertyName = "type"; - public const string FormatPropertyName = "format"; - public const string PatternPropertyName = "pattern"; - public const string PropertiesPropertyName = "properties"; - public const string RequiredPropertyName = "required"; - public const string ItemsPropertyName = "items"; - public const string AdditionalPropertiesPropertyName = "additionalProperties"; - public const string EnumPropertyName = "enum"; - public const string NotPropertyName = "not"; - public const string AnyOfPropertyName = "anyOf"; - public const string ConstPropertyName = "const"; - public const string DefaultPropertyName = "default"; - public const string MinLengthPropertyName = "minLength"; - public const string MaxLengthPropertyName = "maxLength"; - } - - private static partial class ThrowHelpers - { - [DoesNotReturn] - public static void ThrowArgumentNullException(string name) => throw new ArgumentNullException(name); - - [DoesNotReturn] - public static void ThrowInvalidOperationException_TrimmedMethodParameters(MethodBase method) => - throw new InvalidOperationException($"The parameters for method '{method}' have been trimmed away."); - - [DoesNotReturn] - public static void ThrowNotSupportedException_ReferenceHandlerPreserveNotSupported() => - throw new NotSupportedException("Schema generation not supported with ReferenceHandler.Preserve enabled."); - } -} diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapperConfiguration.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapperConfiguration.cs deleted file mode 100644 index 3e6edfec8b50..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapperConfiguration.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// Source copied from https://github.com/eiriktsarpalis/stj-schema-mapper -// It should be kept in sync with any changes made in that repo, -// and should be removed once the relevant replacements are available in STJv9. - -using System; -using System.ComponentModel; -using System.Text.Json.Nodes; - -namespace JsonSchemaMapper; - -/// -/// Controls the behavior of the class. -/// -#if EXPOSE_JSON_SCHEMA_MAPPER -public -#else -internal -#endif - class JsonSchemaMapperConfiguration -{ - /// - /// Gets the default configuration object used by . - /// - public static JsonSchemaMapperConfiguration Default { get; } = new(); - - /// - /// Determines whether the '$schema' property should be included in the root schema document. - /// - /// - /// Defaults to true. - /// - public bool IncludeSchemaVersion { get; init; } = true; - - /// - /// Determines whether the should be resolved for types and properties. - /// - /// - /// Defaults to true. - /// - public bool ResolveDescriptionAttributes { get; init; } = true; - - /// - /// Specifies whether the type keyword should be included in enum type schemas. - /// - /// - /// Defaults to false. - /// - public bool IncludeTypeInEnums { get; init; } - - /// - /// Determines whether non-nullable schemas should be generated for null oblivious reference types. - /// - /// - /// Defaults to . Due to restrictions in the run-time representation of nullable reference types - /// most occurrences are null oblivious and are treated as nullable by the serializer. A notable exception to that rule - /// are nullability annotations of field, property and constructor parameters which are represented in the contract metadata. - /// - public bool TreatNullObliviousAsNonNullable { get; init; } - - /// - /// Defines a callback that is invoked for every schema that is generated within the type graph. - /// - public Func? TransformSchemaNode { get; init; } -} diff --git a/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs b/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs index 87da898f588e..ee10254ff8b1 100644 --- a/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs +++ b/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs @@ -1,13 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary. using System; -using System.Diagnostics; +#pragma warning restore IDE0005 // Using directive is unnecessary. using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -using JsonSchemaMapper; +using Microsoft.Extensions.AI; + +#pragma warning disable IDE0010 // Add missing cases namespace Microsoft.SemanticKernel; @@ -22,16 +24,20 @@ namespace Microsoft.SemanticKernel; internal static class KernelJsonSchemaBuilder { private static JsonSerializerOptions? s_options; - private static readonly JsonSchemaMapperConfiguration s_config = new() + private static readonly AIJsonSchemaCreateOptions s_schemaOptions = new() { - IncludeSchemaVersion = false, - IncludeTypeInEnums = true, - TreatNullObliviousAsNonNullable = true, + IncludeSchemaKeyword = false, + IncludeTypeInEnumSchemas = true, + RequireAllProperties = false, + DisallowAdditionalProperties = false, }; + private static readonly JsonElement s_trueSchemaAsObject = JsonDocument.Parse("{}").RootElement; + private static readonly JsonElement s_falseSchemaAsObject = JsonDocument.Parse("""{"not":true}""").RootElement; + [RequiresUnreferencedCode("Uses reflection to generate JSON schema, making it incompatible with AOT scenarios.")] [RequiresDynamicCode("Uses reflection to generate JSON schema, making it incompatible with AOT scenarios.")] - public static KernelJsonSchema Build(Type type, string? description = null, JsonSchemaMapperConfiguration? configuration = null) + public static KernelJsonSchema Build(Type type, string? description = null, AIJsonSchemaCreateOptions? configuration = null) { return Build(type, GetDefaultOptions(), description, configuration); } @@ -40,27 +46,21 @@ public static KernelJsonSchema Build( Type type, JsonSerializerOptions options, string? description = null, - JsonSchemaMapperConfiguration? configuration = null) + AIJsonSchemaCreateOptions? configuration = null) { - var mapperConfiguration = configuration ?? s_config; - - JsonNode jsonSchema = options.GetJsonSchema(type, mapperConfiguration); - Debug.Assert(jsonSchema.GetValueKind() is JsonValueKind.Object or JsonValueKind.False or JsonValueKind.True); - - if (jsonSchema is not JsonObject jsonObj) - { - // Transform boolean schemas into object equivalents. - jsonObj = jsonSchema.GetValue() - ? new JsonObject() - : new JsonObject { ["not"] = true }; - } - - if (!string.IsNullOrWhiteSpace(description)) + configuration ??= s_schemaOptions; + JsonElement schemaDocument = AIJsonUtilities.CreateJsonSchema(type, description, serializerOptions: options, inferenceOptions: configuration); + switch (schemaDocument.ValueKind) { - jsonObj["description"] = description; + case JsonValueKind.False: + schemaDocument = s_falseSchemaAsObject; + break; + case JsonValueKind.True: + schemaDocument = s_trueSchemaAsObject; + break; } - return KernelJsonSchema.Parse(jsonObj.ToJsonString(options)); + return KernelJsonSchema.Parse(schemaDocument.GetRawText()); } [RequiresUnreferencedCode("Uses JsonStringEnumConverter and DefaultJsonTypeInfoResolver classes, making it incompatible with AOT scenarios.")] diff --git a/dotnet/src/InternalUtilities/src/Schema/Polyfills/NullabilityInfo.cs b/dotnet/src/InternalUtilities/src/Schema/Polyfills/NullabilityInfo.cs deleted file mode 100644 index 395aa7a3d158..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/Polyfills/NullabilityInfo.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -#if !NET6_0_OR_GREATER -using System.Diagnostics.CodeAnalysis; - -namespace System.Reflection -{ - /// - /// A class that represents nullability info. - /// - [ExcludeFromCodeCoverage] - internal sealed class NullabilityInfo - { - internal NullabilityInfo(Type type, NullabilityState readState, NullabilityState writeState, - NullabilityInfo? elementType, NullabilityInfo[] typeArguments) - { - Type = type; - ReadState = readState; - WriteState = writeState; - ElementType = elementType; - GenericTypeArguments = typeArguments; - } - - /// - /// The of the member or generic parameter - /// to which this NullabilityInfo belongs. - /// - public Type Type { get; } - - /// - /// The nullability read state of the member. - /// - public NullabilityState ReadState { get; internal set; } - - /// - /// The nullability write state of the member. - /// - public NullabilityState WriteState { get; internal set; } - - /// - /// If the member type is an array, gives the of the elements of the array, null otherwise. - /// - public NullabilityInfo? ElementType { get; } - - /// - /// If the member type is a generic type, gives the array of for each type parameter. - /// - public NullabilityInfo[] GenericTypeArguments { get; } - } - - /// - /// An enum that represents nullability state. - /// - internal enum NullabilityState - { - /// - /// Nullability context not enabled (oblivious) - /// - Unknown, - - /// - /// Non nullable value or reference type - /// - NotNull, - - /// - /// Nullable value or reference type - /// - Nullable, - } -} -#endif diff --git a/dotnet/src/InternalUtilities/src/Schema/Polyfills/NullabilityInfoContext.cs b/dotnet/src/InternalUtilities/src/Schema/Polyfills/NullabilityInfoContext.cs deleted file mode 100644 index 14f24e7fd722..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/Polyfills/NullabilityInfoContext.cs +++ /dev/null @@ -1,672 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -#if !NET6_0_OR_GREATER -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; - -namespace System.Reflection -{ - /// - /// Provides APIs for populating nullability information/context from reflection members: - /// , , and . - /// - [ExcludeFromCodeCoverage] - internal sealed class NullabilityInfoContext - { - private const string CompilerServicesNameSpace = "System.Runtime.CompilerServices"; - private readonly Dictionary _publicOnlyModules = []; - private readonly Dictionary _context = []; - - internal static bool IsSupported { get; } = - AppContext.TryGetSwitch("System.Reflection.NullabilityInfoContext.IsSupported", out bool isSupported) ? isSupported : true; - - [Flags] - private enum NotAnnotatedStatus - { - None = 0x0, // no restriction, all members annotated - Private = 0x1, // private members not annotated - Internal = 0x2, // internal members not annotated - } - - private NullabilityState? GetNullableContext(MemberInfo? memberInfo) - { - while (memberInfo is not null) - { - if (_context.TryGetValue(memberInfo, out NullabilityState state)) - { - return state; - } - - foreach (CustomAttributeData attribute in memberInfo.GetCustomAttributesData()) - { - if (attribute.AttributeType.Name == "NullableContextAttribute" && - attribute.AttributeType.Namespace == CompilerServicesNameSpace && - attribute.ConstructorArguments.Count == 1) - { - state = TranslateByte(attribute.ConstructorArguments[0].Value); - _context.Add(memberInfo, state); - return state; - } - } - - memberInfo = memberInfo.DeclaringType; - } - - return null; - } - - /// - /// Populates for the given . - /// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's - /// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state. - /// - /// The parameter which nullability info gets populated. - /// If the parameterInfo parameter is null. - /// . - public NullabilityInfo Create(ParameterInfo parameterInfo) - { - EnsureIsSupported(); - - IList attributes = parameterInfo.GetCustomAttributesData(); - NullableAttributeStateParser parser = parameterInfo.Member is MethodBase method && IsPrivateOrInternalMethodAndAnnotationDisabled(method) - ? NullableAttributeStateParser.Unknown - : CreateParser(attributes); - NullabilityInfo nullability = GetNullabilityInfo(parameterInfo.Member, parameterInfo.ParameterType, parser); - - if (nullability.ReadState != NullabilityState.Unknown) - { - CheckParameterMetadataType(parameterInfo, nullability); - } - - CheckNullabilityAttributes(nullability, attributes); - return nullability; - } - - private void CheckParameterMetadataType(ParameterInfo parameter, NullabilityInfo nullability) - { - ParameterInfo? metaParameter; - MemberInfo metaMember; - - switch (parameter.Member) - { - case ConstructorInfo ctor: - var metaCtor = (ConstructorInfo)GetMemberMetadataDefinition(ctor); - metaMember = metaCtor; - metaParameter = GetMetaParameter(metaCtor, parameter); - break; - - case MethodInfo method: - MethodInfo metaMethod = GetMethodMetadataDefinition(method); - metaMember = metaMethod; - metaParameter = string.IsNullOrEmpty(parameter.Name) ? metaMethod.ReturnParameter : GetMetaParameter(metaMethod, parameter); - break; - - default: - return; - } - - if (metaParameter is not null) - { - CheckGenericParameters(nullability, metaMember, metaParameter.ParameterType, parameter.Member.ReflectedType); - } - } - - private static ParameterInfo? GetMetaParameter(MethodBase metaMethod, ParameterInfo parameter) - { - var parameters = metaMethod.GetParameters(); - for (int i = 0; i < parameters.Length; i++) - { - if (parameter.Position == i && - parameter.Name == parameters[i].Name) - { - return parameters[i]; - } - } - - return null; - } - - private static MethodInfo GetMethodMetadataDefinition(MethodInfo method) - { - if (method.IsGenericMethod && !method.IsGenericMethodDefinition) - { - method = method.GetGenericMethodDefinition(); - } - - return (MethodInfo)GetMemberMetadataDefinition(method); - } - - private static void CheckNullabilityAttributes(NullabilityInfo nullability, IList attributes) - { - var codeAnalysisReadState = NullabilityState.Unknown; - var codeAnalysisWriteState = NullabilityState.Unknown; - - foreach (CustomAttributeData attribute in attributes) - { - if (attribute.AttributeType.Namespace == "System.Diagnostics.CodeAnalysis") - { - if (attribute.AttributeType.Name == "NotNullAttribute") - { - codeAnalysisReadState = NullabilityState.NotNull; - } - else if ((attribute.AttributeType.Name == "MaybeNullAttribute" || - attribute.AttributeType.Name == "MaybeNullWhenAttribute") && - codeAnalysisReadState == NullabilityState.Unknown && - !IsValueTypeOrValueTypeByRef(nullability.Type)) - { - codeAnalysisReadState = NullabilityState.Nullable; - } - else if (attribute.AttributeType.Name == "DisallowNullAttribute") - { - codeAnalysisWriteState = NullabilityState.NotNull; - } - else if (attribute.AttributeType.Name == "AllowNullAttribute" && - codeAnalysisWriteState == NullabilityState.Unknown && - !IsValueTypeOrValueTypeByRef(nullability.Type)) - { - codeAnalysisWriteState = NullabilityState.Nullable; - } - } - } - - if (codeAnalysisReadState != NullabilityState.Unknown) - { - nullability.ReadState = codeAnalysisReadState; - } - - if (codeAnalysisWriteState != NullabilityState.Unknown) - { - nullability.WriteState = codeAnalysisWriteState; - } - } - - /// - /// Populates for the given . - /// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's - /// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state. - /// - /// The parameter which nullability info gets populated. - /// If the propertyInfo parameter is null. - /// . - public NullabilityInfo Create(PropertyInfo propertyInfo) - { - EnsureIsSupported(); - - MethodInfo? getter = propertyInfo.GetGetMethod(true); - MethodInfo? setter = propertyInfo.GetSetMethod(true); - bool annotationsDisabled = (getter is null || IsPrivateOrInternalMethodAndAnnotationDisabled(getter)) - && (setter is null || IsPrivateOrInternalMethodAndAnnotationDisabled(setter)); - NullableAttributeStateParser parser = annotationsDisabled ? NullableAttributeStateParser.Unknown : CreateParser(propertyInfo.GetCustomAttributesData()); - NullabilityInfo nullability = GetNullabilityInfo(propertyInfo, propertyInfo.PropertyType, parser); - - if (getter is not null) - { - CheckNullabilityAttributes(nullability, getter.ReturnParameter.GetCustomAttributesData()); - } - else - { - nullability.ReadState = NullabilityState.Unknown; - } - - if (setter is not null) - { - CheckNullabilityAttributes(nullability, setter.GetParameters().Last().GetCustomAttributesData()); - } - else - { - nullability.WriteState = NullabilityState.Unknown; - } - - return nullability; - } - - private bool IsPrivateOrInternalMethodAndAnnotationDisabled(MethodBase method) - { - if ((method.IsPrivate || method.IsFamilyAndAssembly || method.IsAssembly) && - IsPublicOnly(method.IsPrivate, method.IsFamilyAndAssembly, method.IsAssembly, method.Module)) - { - return true; - } - - return false; - } - - /// - /// Populates for the given . - /// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's - /// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state. - /// - /// The parameter which nullability info gets populated. - /// If the eventInfo parameter is null. - /// . - public NullabilityInfo Create(EventInfo eventInfo) - { - EnsureIsSupported(); - - return GetNullabilityInfo(eventInfo, eventInfo.EventHandlerType!, CreateParser(eventInfo.GetCustomAttributesData())); - } - - /// - /// Populates for the given - /// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's - /// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state. - /// - /// The parameter which nullability info gets populated. - /// If the fieldInfo parameter is null. - /// . - public NullabilityInfo Create(FieldInfo fieldInfo) - { - EnsureIsSupported(); - - IList attributes = fieldInfo.GetCustomAttributesData(); - NullableAttributeStateParser parser = IsPrivateOrInternalFieldAndAnnotationDisabled(fieldInfo) ? NullableAttributeStateParser.Unknown : CreateParser(attributes); - NullabilityInfo nullability = GetNullabilityInfo(fieldInfo, fieldInfo.FieldType, parser); - CheckNullabilityAttributes(nullability, attributes); - return nullability; - } - - private static void EnsureIsSupported() - { - if (!IsSupported) - { - throw new InvalidOperationException("NullabilityInfoContext is not supported"); - } - } - - private bool IsPrivateOrInternalFieldAndAnnotationDisabled(FieldInfo fieldInfo) - { - if ((fieldInfo.IsPrivate || fieldInfo.IsFamilyAndAssembly || fieldInfo.IsAssembly) && - IsPublicOnly(fieldInfo.IsPrivate, fieldInfo.IsFamilyAndAssembly, fieldInfo.IsAssembly, fieldInfo.Module)) - { - return true; - } - - return false; - } - - private bool IsPublicOnly(bool isPrivate, bool isFamilyAndAssembly, bool isAssembly, Module module) - { - if (!_publicOnlyModules.TryGetValue(module, out NotAnnotatedStatus value)) - { - value = PopulateAnnotationInfo(module.GetCustomAttributesData()); - _publicOnlyModules.Add(module, value); - } - - if (value == NotAnnotatedStatus.None) - { - return false; - } - - if (((isPrivate || isFamilyAndAssembly) && value.HasFlag(NotAnnotatedStatus.Private)) || - (isAssembly && value.HasFlag(NotAnnotatedStatus.Internal))) - { - return true; - } - - return false; - } - - private static NotAnnotatedStatus PopulateAnnotationInfo(IList customAttributes) - { - foreach (CustomAttributeData attribute in customAttributes) - { - if (attribute.AttributeType.Name == "NullablePublicOnlyAttribute" && - attribute.AttributeType.Namespace == CompilerServicesNameSpace && - attribute.ConstructorArguments.Count == 1) - { - if (attribute.ConstructorArguments[0].Value is bool boolValue && boolValue) - { - return NotAnnotatedStatus.Internal | NotAnnotatedStatus.Private; - } - else - { - return NotAnnotatedStatus.Private; - } - } - } - - return NotAnnotatedStatus.None; - } - - private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, NullableAttributeStateParser parser) - { - int index = 0; - NullabilityInfo nullability = GetNullabilityInfo(memberInfo, type, parser, ref index); - - if (nullability.ReadState != NullabilityState.Unknown) - { - TryLoadGenericMetaTypeNullability(memberInfo, nullability); - } - - return nullability; - } - - private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, NullableAttributeStateParser parser, ref int index) - { - NullabilityState state = NullabilityState.Unknown; - NullabilityInfo? elementState = null; - NullabilityInfo[] genericArgumentsState = []; - Type underlyingType = type; - - if (underlyingType.IsByRef || underlyingType.IsPointer) - { - underlyingType = underlyingType.GetElementType()!; - } - - if (underlyingType.IsValueType) - { - if (Nullable.GetUnderlyingType(underlyingType) is { } nullableUnderlyingType) - { - underlyingType = nullableUnderlyingType; - state = NullabilityState.Nullable; - } - else - { - state = NullabilityState.NotNull; - } - - if (underlyingType.IsGenericType) - { - ++index; - } - } - else - { - if (!parser.ParseNullableState(index++, ref state) - && GetNullableContext(memberInfo) is { } contextState) - { - state = contextState; - } - - if (underlyingType.IsArray) - { - elementState = GetNullabilityInfo(memberInfo, underlyingType.GetElementType()!, parser, ref index); - } - } - - if (underlyingType.IsGenericType) - { - Type[] genericArguments = underlyingType.GetGenericArguments(); - genericArgumentsState = new NullabilityInfo[genericArguments.Length]; - - for (int i = 0; i < genericArguments.Length; i++) - { - genericArgumentsState[i] = GetNullabilityInfo(memberInfo, genericArguments[i], parser, ref index); - } - } - - return new NullabilityInfo(type, state, state, elementState, genericArgumentsState); - } - - private static NullableAttributeStateParser CreateParser(IList customAttributes) - { - foreach (CustomAttributeData attribute in customAttributes) - { - if (attribute.AttributeType.Name == "NullableAttribute" && - attribute.AttributeType.Namespace == CompilerServicesNameSpace && - attribute.ConstructorArguments.Count == 1) - { - return new NullableAttributeStateParser(attribute.ConstructorArguments[0].Value); - } - } - - return new NullableAttributeStateParser(null); - } - - private void TryLoadGenericMetaTypeNullability(MemberInfo memberInfo, NullabilityInfo nullability) - { - MemberInfo? metaMember = GetMemberMetadataDefinition(memberInfo); - Type? metaType = null; - if (metaMember is FieldInfo field) - { - metaType = field.FieldType; - } - else if (metaMember is PropertyInfo property) - { - metaType = GetPropertyMetaType(property); - } - - if (metaType is not null) - { - CheckGenericParameters(nullability, metaMember!, metaType, memberInfo.ReflectedType); - } - } - - private static MemberInfo GetMemberMetadataDefinition(MemberInfo member) - { - Type? type = member.DeclaringType; - if ((type is not null) && type.IsGenericType && !type.IsGenericTypeDefinition) - { - return NullabilityInfoHelpers.GetMemberWithSameMetadataDefinitionAs(type.GetGenericTypeDefinition(), member); - } - - return member; - } - - private static Type GetPropertyMetaType(PropertyInfo property) - { - if (property.GetGetMethod(true) is MethodInfo method) - { - return method.ReturnType; - } - - return property.GetSetMethod(true)!.GetParameters()[0].ParameterType; - } - - private void CheckGenericParameters(NullabilityInfo nullability, MemberInfo metaMember, Type metaType, Type? reflectedType) - { - if (metaType.IsGenericParameter) - { - if (nullability.ReadState == NullabilityState.NotNull) - { - TryUpdateGenericParameterNullability(nullability, metaType, reflectedType); - } - } - else if (metaType.ContainsGenericParameters) - { - if (nullability.GenericTypeArguments.Length > 0) - { - Type[] genericArguments = metaType.GetGenericArguments(); - - for (int i = 0; i < genericArguments.Length; i++) - { - CheckGenericParameters(nullability.GenericTypeArguments[i], metaMember, genericArguments[i], reflectedType); - } - } - else if (nullability.ElementType is { } elementNullability && metaType.IsArray) - { - CheckGenericParameters(elementNullability, metaMember, metaType.GetElementType()!, reflectedType); - } - - // We could also follow this branch for metaType.IsPointer, but since pointers must be unmanaged this - // will be a no-op regardless - else if (metaType.IsByRef) - { - CheckGenericParameters(nullability, metaMember, metaType.GetElementType()!, reflectedType); - } - } - } - - private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, Type genericParameter, Type? reflectedType) - { - Debug.Assert(genericParameter.IsGenericParameter); - - if (reflectedType is not null - && !genericParameter.IsGenericMethodParameter() - && TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, reflectedType, reflectedType)) - { - return true; - } - - if (IsValueTypeOrValueTypeByRef(nullability.Type)) - { - return true; - } - - var state = NullabilityState.Unknown; - if (CreateParser(genericParameter.GetCustomAttributesData()).ParseNullableState(0, ref state)) - { - nullability.ReadState = state; - nullability.WriteState = state; - return true; - } - - if (GetNullableContext(genericParameter) is { } contextState) - { - nullability.ReadState = contextState; - nullability.WriteState = contextState; - return true; - } - - return false; - } - - private bool TryUpdateGenericTypeParameterNullabilityFromReflectedType(NullabilityInfo nullability, Type genericParameter, Type context, Type reflectedType) - { - Debug.Assert(genericParameter.IsGenericParameter && !genericParameter.IsGenericMethodParameter()); - - Type contextTypeDefinition = context.IsGenericType && !context.IsGenericTypeDefinition ? context.GetGenericTypeDefinition() : context; - if (genericParameter.DeclaringType == contextTypeDefinition) - { - return false; - } - - Type? baseType = contextTypeDefinition.BaseType; - if (baseType is null) - { - return false; - } - - if (!baseType.IsGenericType - || (baseType.IsGenericTypeDefinition ? baseType : baseType.GetGenericTypeDefinition()) != genericParameter.DeclaringType) - { - return TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, baseType, reflectedType); - } - - Type[] genericArguments = baseType.GetGenericArguments(); - Type genericArgument = genericArguments[genericParameter.GenericParameterPosition]; - if (genericArgument.IsGenericParameter) - { - return TryUpdateGenericParameterNullability(nullability, genericArgument, reflectedType); - } - - NullableAttributeStateParser parser = CreateParser(contextTypeDefinition.GetCustomAttributesData()); - int nullabilityStateIndex = 1; // start at 1 since index 0 is the type itself - for (int i = 0; i < genericParameter.GenericParameterPosition; i++) - { - nullabilityStateIndex += CountNullabilityStates(genericArguments[i]); - } - - return TryPopulateNullabilityInfo(nullability, parser, ref nullabilityStateIndex); - - static int CountNullabilityStates(Type type) - { - Type underlyingType = Nullable.GetUnderlyingType(type) ?? type; - if (underlyingType.IsGenericType) - { - int count = 1; - foreach (Type genericArgument in underlyingType.GetGenericArguments()) - { - count += CountNullabilityStates(genericArgument); - } - - return count; - } - - if (underlyingType.HasElementType) - { - return (underlyingType.IsArray ? 1 : 0) + CountNullabilityStates(underlyingType.GetElementType()!); - } - - return type.IsValueType ? 0 : 1; - } - } - -#pragma warning disable SA1204 // Static elements should appear before instance elements - private static bool TryPopulateNullabilityInfo(NullabilityInfo nullability, NullableAttributeStateParser parser, ref int index) -#pragma warning restore SA1204 // Static elements should appear before instance elements - { - bool isValueType = IsValueTypeOrValueTypeByRef(nullability.Type); - if (!isValueType) - { - var state = NullabilityState.Unknown; - if (!parser.ParseNullableState(index, ref state)) - { - return false; - } - - nullability.ReadState = state; - nullability.WriteState = state; - } - - if (!isValueType || (Nullable.GetUnderlyingType(nullability.Type) ?? nullability.Type).IsGenericType) - { - index++; - } - - if (nullability.GenericTypeArguments.Length > 0) - { - foreach (NullabilityInfo genericTypeArgumentNullability in nullability.GenericTypeArguments) - { - TryPopulateNullabilityInfo(genericTypeArgumentNullability, parser, ref index); - } - } - else if (nullability.ElementType is { } elementTypeNullability) - { - TryPopulateNullabilityInfo(elementTypeNullability, parser, ref index); - } - - return true; - } - - private static NullabilityState TranslateByte(object? value) - { - return value is byte b ? TranslateByte(b) : NullabilityState.Unknown; - } - - private static NullabilityState TranslateByte(byte b) => - b switch - { - 1 => NullabilityState.NotNull, - 2 => NullabilityState.Nullable, - _ => NullabilityState.Unknown - }; - - private static bool IsValueTypeOrValueTypeByRef(Type type) => - type.IsValueType || ((type.IsByRef || type.IsPointer) && type.GetElementType()!.IsValueType); - - private readonly struct NullableAttributeStateParser - { - private static readonly object UnknownByte = (byte)0; - - private readonly object? _nullableAttributeArgument; - - public NullableAttributeStateParser(object? nullableAttributeArgument) - { - this._nullableAttributeArgument = nullableAttributeArgument; - } - - public static NullableAttributeStateParser Unknown => new(UnknownByte); - - public bool ParseNullableState(int index, ref NullabilityState state) - { - switch (this._nullableAttributeArgument) - { - case byte b: - state = TranslateByte(b); - return true; - case ReadOnlyCollection args - when index < args.Count && args[index].Value is byte elementB: - state = TranslateByte(elementB); - return true; - default: - return false; - } - } - } - } -} -#endif diff --git a/dotnet/src/InternalUtilities/src/Schema/Polyfills/NullabilityInfoHelpers.cs b/dotnet/src/InternalUtilities/src/Schema/Polyfills/NullabilityInfoHelpers.cs deleted file mode 100644 index 31c891fb4595..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/Polyfills/NullabilityInfoHelpers.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -#if !NET6_0_OR_GREATER -using System.Diagnostics.CodeAnalysis; - -namespace System.Reflection -{ - /// - /// Polyfills for System.Private.CoreLib internals. - /// - [ExcludeFromCodeCoverage] - internal static class NullabilityInfoHelpers - { - public static MemberInfo GetMemberWithSameMetadataDefinitionAs(Type type, MemberInfo member) - { - const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; - foreach (var info in type.GetMembers(all)) - { - if (info.HasSameMetadataDefinitionAs(member)) - { - return info; - } - } - - throw new MissingMemberException(type.FullName, member.Name); - } - - // https://github.com/dotnet/runtime/blob/main/src/coreclr/System.Private.CoreLib/src/System/Reflection/MemberInfo.Internal.cs - public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) - { - return target.MetadataToken == other.MetadataToken && - target.Module.Equals(other.Module); - } - - // https://github.com/dotnet/runtime/issues/23493 - public static bool IsGenericMethodParameter(this Type target) - { - return target.IsGenericParameter && - target.DeclaringMethod is not null; - } - } -} -#endif diff --git a/dotnet/src/InternalUtilities/src/Schema/README.md b/dotnet/src/InternalUtilities/src/Schema/README.md deleted file mode 100644 index 0ddddcbd1ac1..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/README.md +++ /dev/null @@ -1,7 +0,0 @@ -The *.cs files in this folder, other than KernelJsonSchemaBuilder.cs, are a direct copy of the code at -https://github.com/eiriktsarpalis/stj-schema-mapper/tree/94b6d9b979f1a80a1c305605dfc6de3b7a6fe78b/src/JsonSchemaMapper. -They should be kept in sync with any changes made in that repo, and should be removed once the relevant replacements are available in System.Text.Json. - -EXPOSE_JSON_SCHEMA_MAPPER should _not_ be defined so as to keep all of the functionality internal. - -A .editorconfig is used to suppress code analysis violations this repo tries to enforce that the repo containing the copied code doesn't. \ No newline at end of file diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionMetadataFactoryTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionMetadataFactoryTests.cs index 6d423aebb781..d5a129157c4a 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionMetadataFactoryTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionMetadataFactoryTests.cs @@ -36,7 +36,7 @@ public void ItCanCreateFromType(JsonSerializerOptions? jsos) Assert.NotEmpty(metadata1.Parameters); Assert.NotNull(metadata1.Parameters[0].Schema); - Assert.Equal("""{"type":"string","description":"Description for parameter 1"}""", metadata1.Parameters[0].Schema!.ToString()); + Assert.Equal("""{"description":"Description for parameter 1","type":"string"}""", metadata1.Parameters[0].Schema!.ToString()); Assert.NotNull(metadata1.ReturnParameter); Assert.NotNull(metadata1.ReturnParameter.Schema); @@ -50,7 +50,7 @@ public void ItCanCreateFromType(JsonSerializerOptions? jsos) Assert.NotEmpty(metadata2.Parameters); Assert.NotNull(metadata2.Parameters[0].Schema); - Assert.Equal("""{"type":"object","properties":{"Value":{"type":["string","null"]}},"description":"Description for parameter 1"}""", metadata2.Parameters[0].Schema!.ToString()); + Assert.Equal("""{"description":"Description for parameter 1","type":"object","properties":{"Value":{"type":["string","null"]}}}""", metadata2.Parameters[0].Schema!.ToString()); Assert.NotNull(metadata2.ReturnParameter); Assert.NotNull(metadata2.ReturnParameter.Schema); diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelParameterMetadataTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelParameterMetadataTests.cs index c109b28d2aa3..231ebda1c70f 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelParameterMetadataTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelParameterMetadataTests.cs @@ -71,7 +71,7 @@ public void ItIncludesDescriptionInSchema(JsonSerializerOptions? jsos) new KernelParameterMetadata("p", jsos) { Description = "something neat", ParameterType = typeof(int) } : new KernelParameterMetadata("p") { Description = "something neat", ParameterType = typeof(int) }; - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"integer", "description":"something neat" }""")), JsonSerializer.Serialize(m.Schema)); + Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse("""{"description":"something neat", "type":"integer"}""")), JsonSerializer.Serialize(m.Schema)); } [Theory] @@ -82,7 +82,7 @@ public void ItIncludesDefaultValueInSchema(JsonSerializerOptions? jsos) new KernelParameterMetadata("p", jsos) { DefaultValue = "42", ParameterType = typeof(int) } : new KernelParameterMetadata("p") { DefaultValue = "42", ParameterType = typeof(int) }; - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"integer", "description":"(default value: 42)" }""")), JsonSerializer.Serialize(m.Schema)); + Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse("""{"description":"(default value: 42)", "type":"integer"}""")), JsonSerializer.Serialize(m.Schema)); } [Theory] @@ -93,7 +93,7 @@ public void ItIncludesDescriptionAndDefaultValueInSchema(JsonSerializerOptions? new KernelParameterMetadata("p", jsos) { Description = "something neat", DefaultValue = "42", ParameterType = typeof(int) } : new KernelParameterMetadata("p") { Description = "something neat", DefaultValue = "42", ParameterType = typeof(int) }; - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"integer", "description":"something neat (default value: 42)" }""")), JsonSerializer.Serialize(m.Schema)); + Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse("""{"description":"something neat (default value: 42)", "type":"integer"}""")), JsonSerializer.Serialize(m.Schema)); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelPluginFactoryTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelPluginFactoryTests.cs index 9afa058e9248..cf428f26ae1c 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelPluginFactoryTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelPluginFactoryTests.cs @@ -72,7 +72,7 @@ private static async Task AssertPluginAndFunctionsAsync(Kernel kernel, KernelPlu Assert.NotEmpty(function1.Metadata.Parameters); Assert.NotNull(function1.Metadata.Parameters[0].Schema); - Assert.Equal("""{"type":"string","description":"Description for parameter 1"}""", function1.Metadata.Parameters[0].Schema!.ToString()); + Assert.Equal("""{"description":"Description for parameter 1","type":"string"}""", function1.Metadata.Parameters[0].Schema!.ToString()); Assert.NotNull(function1.Metadata.ReturnParameter); Assert.NotNull(function1.Metadata.ReturnParameter.Schema); @@ -86,7 +86,7 @@ private static async Task AssertPluginAndFunctionsAsync(Kernel kernel, KernelPlu Assert.NotEmpty(function2.Metadata.Parameters); Assert.NotNull(function2.Metadata.Parameters[0].Schema); - Assert.Equal("""{"type":"object","properties":{"Value":{"type":["string","null"]}},"description":"Description for parameter 1"}""", function2.Metadata.Parameters[0].Schema!.ToString()); + Assert.Equal("""{"description":"Description for parameter 1","type":"object","properties":{"Value":{"type":["string","null"]}}}""", function2.Metadata.Parameters[0].Schema!.ToString()); Assert.NotNull(function2.Metadata.ReturnParameter); Assert.NotNull(function2.Metadata.ReturnParameter.Schema); diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelPluginTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelPluginTests.cs index 5721cb6f2c19..877bf8a90857 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelPluginTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelPluginTests.cs @@ -220,9 +220,9 @@ public async Task ItCanProduceAIFunctionsThatInvokeKernelFunctions(bool withKern Assert.NotNull(funcs[1].Metadata.Parameters[0].Schema); Assert.NotNull(funcs[1].Metadata.Parameters[1].Schema); - Assert.Equal(plugin["Function1"].Metadata.Parameters[0].Schema?.ToString(), funcs[0].Metadata.Parameters[0].Schema?.ToString()); - Assert.Equal(plugin["Function2"].Metadata.Parameters[0].Schema?.ToString(), funcs[1].Metadata.Parameters[0].Schema?.ToString()); - Assert.Equal(plugin["Function2"].Metadata.Parameters[1].Schema?.ToString(), funcs[1].Metadata.Parameters[1].Schema?.ToString()); + Assert.Equal(plugin["Function1"].Metadata.Parameters[0].Schema?.ToString(), JsonSerializer.Serialize(funcs[0].Metadata.Parameters[0].Schema)); + Assert.Equal(plugin["Function2"].Metadata.Parameters[0].Schema?.ToString(), JsonSerializer.Serialize(funcs[1].Metadata.Parameters[0].Schema)); + Assert.Equal(plugin["Function2"].Metadata.Parameters[1].Schema?.ToString(), JsonSerializer.Serialize(funcs[1].Metadata.Parameters[1].Schema)); using CancellationTokenSource cts = new(); diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelReturnParameterMetadataTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelReturnParameterMetadataTests.cs index 846840681371..c2f2a1010200 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelReturnParameterMetadataTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelReturnParameterMetadataTests.cs @@ -37,7 +37,7 @@ public void ItIncludesDescriptionInSchema(JsonSerializerOptions? jsos) new KernelReturnParameterMetadata(jsos) { Description = "d", ParameterType = typeof(int) } : new KernelReturnParameterMetadata() { Description = "d", ParameterType = typeof(int) }; - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"integer", "description":"d" }""")), JsonSerializer.Serialize(m.Schema)); + Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse("""{"description":"d", "type":"integer"}""")), JsonSerializer.Serialize(m.Schema)); } [Fact]