From 46242f055268d5fb31f5196987e54154e8064182 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 31 Oct 2024 15:30:05 +0000 Subject: [PATCH 1/4] Plug JsonSchemaExporter test data to the AIJsonUtilities tests --- eng/packages/TestOnly.props | 1 - .../DiagnosticAttributes/README.md | 2 +- .../Utilities/AIJsonUtilities.Schema.cs | 18 ++- ...ft.Extensions.AI.Abstractions.Tests.csproj | 13 +- .../{ => Utilities}/AIJsonUtilitiesTests.cs | 33 ++++- .../JsonSchemaExporterTests.cs | 6 +- .../{Helpers.cs => SchemaTestHelpers.cs} | 17 +-- test/Shared/JsonSchemaExporter/TestData.cs | 26 +++- test/Shared/JsonSchemaExporter/TestTypes.cs | 121 +++++++++--------- test/Shared/Shared.Tests.csproj | 2 +- 10 files changed, 143 insertions(+), 96 deletions(-) rename test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/{ => Utilities}/AIJsonUtilitiesTests.cs (79%) rename test/Shared/JsonSchemaExporter/{Helpers.cs => SchemaTestHelpers.cs} (75%) diff --git a/eng/packages/TestOnly.props b/eng/packages/TestOnly.props index 78772d87d09..f6753c9c14d 100644 --- a/eng/packages/TestOnly.props +++ b/eng/packages/TestOnly.props @@ -21,7 +21,6 @@ - diff --git a/src/LegacySupport/DiagnosticAttributes/README.md b/src/LegacySupport/DiagnosticAttributes/README.md index b34b86160e6..067675cbcb7 100644 --- a/src/LegacySupport/DiagnosticAttributes/README.md +++ b/src/LegacySupport/DiagnosticAttributes/README.md @@ -2,6 +2,6 @@ To use this source in your project, add the following to your `.csproj` file: ```xml - true + true ``` diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs index cd33a2557af..e5800408203 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs @@ -14,6 +14,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Schema; +using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; #pragma warning disable S1121 // Assignments should not be made from within sub-expressions @@ -186,7 +187,6 @@ private static JsonElement GetJsonSchemaCached(JsonSerializerOptions options, Fu private static JsonElement GetJsonSchemaCore(JsonSerializerOptions options, FunctionParameterKey key) { _ = Throw.IfNull(options); - options.MakeReadOnly(); if (key.Type is null) { @@ -282,7 +282,7 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema) // Some consumers of the JSON schema, including Ollama as of v0.3.13, don't understand // schemas with "type": [...], and only understand "type" being a single value. // STJ represents .NET integer types as ["string", "integer"], which will then lead to an error. - if (TypeIsArrayContainingInteger(objSchema)) + if (TypeIsIntegerWithStringNumberHandling(ctx, objSchema)) { // We don't want to emit any array for "type". In this case we know it contains "integer" // so reduce the type to that alone, assuming it's the most specific type. @@ -351,17 +351,21 @@ static JsonObject ConvertSchemaToObject(ref JsonNode schema) } } - private static bool TypeIsArrayContainingInteger(JsonObject schema) + private static bool TypeIsIntegerWithStringNumberHandling(JsonSchemaExporterContext ctx, JsonObject schema) { - if (schema["type"] is JsonArray typeArray) + if (ctx.TypeInfo.NumberHandling is not JsonNumberHandling.Strict && schema["type"] is JsonArray typeArray) { - foreach (var entry in typeArray) + int count = 0; + foreach (JsonNode? entry in typeArray) { - if (entry?.GetValueKind() == JsonValueKind.String && entry.GetValue() == "integer") + if (entry?.GetValueKind() is JsonValueKind.String && + entry.GetValue() is "integer" or "string") { - return true; + count++; } } + + return count == typeArray.Count; } return false; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj index 0d4d5fbfa96..911ce1b2bf8 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj @@ -5,16 +5,27 @@ - $(NoWarn);CA1063;CA1861;CA2201;VSTHRD003 + $(NoWarn);CA1063;CA1861;CA2201;VSTHRD003;S104 true + true + true + true true + true + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AIJsonUtilitiesTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs similarity index 79% rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AIJsonUtilitiesTests.cs rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs index d7ff5c6783e..395bbd3a73a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -3,7 +3,9 @@ using System.ComponentModel; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using Microsoft.Extensions.AI.JsonSchemaExporter; using Xunit; namespace Microsoft.Extensions.AI; @@ -130,7 +132,7 @@ public static void ResolveParameterJsonSchema_ReturnsExpectedValue() } [Fact] - public static void ResolveParameterJsonSchema_TreatsIntegralTypesAsInteger_EvenWithAllowReadingFromString() + public static void CreateParameterJsonSchema_TreatsIntegralTypesAsInteger_EvenWithAllowReadingFromString() { JsonElement expected = JsonDocument.Parse(""" { @@ -160,9 +162,36 @@ public enum MyEnumValue } [Fact] - public static void ResolveJsonSchema_CanBeBoolean() + public static void CreateJsonSchema_CanBeBoolean() { JsonElement schema = AIJsonUtilities.CreateJsonSchema(typeof(object)); Assert.Equal(JsonValueKind.True, schema.ValueKind); } + + [Theory] + [MemberData(nameof(TestTypes.GetTestDataUsingAllValues), MemberType = typeof(TestTypes))] + public static void CreateJsonSchema_ValidateWithTestData(ITestData testData) + { + // Stress tests the schema generation method using types from the JsonSchemaExporter test battery. + + JsonSerializerOptions options = testData.Options is { } opts + ? new(opts) { TypeInfoResolver = TestTypes.TestTypesContext.Default } + : TestTypes.TestTypesContext.Default.Options; + + JsonElement schema = AIJsonUtilities.CreateJsonSchema(testData.Type, serializerOptions: options); + JsonNode? schemaAsNode = JsonSerializer.SerializeToNode(schema, options); + + Assert.NotNull(schemaAsNode); + Assert.Equal(testData.ExpectedJsonSchema.GetValueKind(), schemaAsNode.GetValueKind()); + + if (testData.Value is null || testData.WritesNumbersAsStrings) + { + // Our generated schema does not accept null root values + // or numbers formatted as strings, so we skip this test. + return; + } + + JsonNode? serializedValue = JsonSerializer.SerializeToNode(testData.Value, testData.Type, options); + SchemaTestHelpers.AssertDocumentMatchesSchema(schemaAsNode, serializedValue); + } } diff --git a/test/Shared/JsonSchemaExporter/JsonSchemaExporterTests.cs b/test/Shared/JsonSchemaExporter/JsonSchemaExporterTests.cs index d526025d5ba..93207a7167f 100644 --- a/test/Shared/JsonSchemaExporter/JsonSchemaExporterTests.cs +++ b/test/Shared/JsonSchemaExporter/JsonSchemaExporterTests.cs @@ -32,7 +32,7 @@ public void TestTypes_GeneratesExpectedJsonSchema(ITestData testData) : Options; JsonNode schema = options.GetJsonSchemaAsNode(testData.Type, (JsonSchemaExporterOptions?)testData.ExporterOptions); - Helpers.AssertValidJsonSchema(testData.Type, testData.ExpectedJsonSchema, schema); + SchemaTestHelpers.AssertEqualJsonSchema(testData.ExpectedJsonSchema, schema); } [Theory] @@ -45,7 +45,7 @@ public void TestTypes_SerializedValueMatchesGeneratedSchema(ITestData testData) JsonNode schema = options.GetJsonSchemaAsNode(testData.Type, (JsonSchemaExporterOptions?)testData.ExporterOptions); JsonNode? instance = JsonSerializer.SerializeToNode(testData.Value, testData.Type, options); - Helpers.AssertDocumentMatchesSchema(schema, instance); + SchemaTestHelpers.AssertDocumentMatchesSchema(schema, instance); } [Theory] @@ -100,7 +100,7 @@ public void TypeWithDisallowUnmappedMembers_AdditionalPropertiesFailValidation() { JsonNode schema = Options.GetJsonSchemaAsNode(typeof(TestTypes.PocoDisallowingUnmappedMembers)); JsonNode? jsonWithUnmappedProperties = JsonNode.Parse("""{ "UnmappedProperty" : {} }"""); - Helpers.AssertDoesNotMatchSchema(schema, jsonWithUnmappedProperties); + SchemaTestHelpers.AssertDoesNotMatchSchema(schema, jsonWithUnmappedProperties); } [Fact] diff --git a/test/Shared/JsonSchemaExporter/Helpers.cs b/test/Shared/JsonSchemaExporter/SchemaTestHelpers.cs similarity index 75% rename from test/Shared/JsonSchemaExporter/Helpers.cs rename to test/Shared/JsonSchemaExporter/SchemaTestHelpers.cs index a925c1721f0..02e659a27aa 100644 --- a/test/Shared/JsonSchemaExporter/Helpers.cs +++ b/test/Shared/JsonSchemaExporter/SchemaTestHelpers.cs @@ -8,29 +8,20 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Json.Schema; -using Json.Schema.Generation; using Xunit.Sdk; namespace Microsoft.Extensions.AI.JsonSchemaExporter; -internal static partial class Helpers +internal static partial class SchemaTestHelpers { - public static void AssertValidJsonSchema(Type type, string? expectedJsonSchema, JsonNode actualJsonSchema) + public static void AssertEqualJsonSchema(JsonNode expectedJsonSchema, JsonNode actualJsonSchema) { - // If an expected schema is provided, use that. Otherwise, generate a schema from the type. - JsonNode? expectedJsonSchemaNode = expectedJsonSchema != null - ? JsonNode.Parse(expectedJsonSchema, documentOptions: new() { CommentHandling = JsonCommentHandling.Skip }) - : JsonSerializer.SerializeToNode(new JsonSchemaBuilder().FromType(type), Context.Default.JsonSchema); - - // Trim the $schema property from actual schema since it's not included by the generator. - (actualJsonSchema as JsonObject)?.Remove("$schema"); - - if (!JsonNode.DeepEquals(expectedJsonSchemaNode, actualJsonSchema)) + if (!JsonNode.DeepEquals(expectedJsonSchema, actualJsonSchema)) { throw new XunitException($""" Generated schema does not match the expected specification. Expected: - {FormatJson(expectedJsonSchemaNode)} + {FormatJson(expectedJsonSchema)} Actual: {FormatJson(actualJsonSchema)} """); diff --git a/test/Shared/JsonSchemaExporter/TestData.cs b/test/Shared/JsonSchemaExporter/TestData.cs index 6b2c9d841a3..0254a62b144 100644 --- a/test/Shared/JsonSchemaExporter/TestData.cs +++ b/test/Shared/JsonSchemaExporter/TestData.cs @@ -5,26 +5,40 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Schema; namespace Microsoft.Extensions.AI.JsonSchemaExporter; internal sealed record TestData( T? Value, + [StringSyntax(StringSyntaxAttribute.Json)] string ExpectedJsonSchema, IEnumerable? AdditionalValues = null, - [StringSyntax("Json")] string? ExpectedJsonSchema = null, JsonSchemaExporterOptions? ExporterOptions = null, - JsonSerializerOptions? Options = null) + JsonSerializerOptions? Options = null, + bool WritesNumbersAsStrings = false) : ITestData { + private static readonly JsonDocumentOptions _schemaParseOptions = new() { CommentHandling = JsonCommentHandling.Skip }; + public Type Type => typeof(T); object? ITestData.Value => Value; object? ITestData.ExporterOptions => ExporterOptions; + JsonNode ITestData.ExpectedJsonSchema { get; } = + JsonNode.Parse(ExpectedJsonSchema, documentOptions: _schemaParseOptions) + ?? throw new ArgumentNullException("schema must not be null"); IEnumerable ITestData.GetTestDataForAllValues() { yield return this; + if (default(T) is null && + ExporterOptions is { TreatNullObliviousAsNonNullable: false } && + Value is not null) + { + yield return this with { Value = default }; + } + if (AdditionalValues != null) { foreach (T? value in AdditionalValues) @@ -41,15 +55,13 @@ public interface ITestData object? Value { get; } - /// - /// Gets the expected JSON schema for the value. - /// Fall back to JsonSchemaGenerator as the source of truth if null. - /// - string? ExpectedJsonSchema { get; } + JsonNode ExpectedJsonSchema { get; } object? ExporterOptions { get; } JsonSerializerOptions? Options { get; } + bool WritesNumbersAsStrings { get; } + IEnumerable GetTestDataForAllValues(); } diff --git a/test/Shared/JsonSchemaExporter/TestTypes.cs b/test/Shared/JsonSchemaExporter/TestTypes.cs index 4615143aa78..f8c54fdb178 100644 --- a/test/Shared/JsonSchemaExporter/TestTypes.cs +++ b/test/Shared/JsonSchemaExporter/TestTypes.cs @@ -45,40 +45,41 @@ public static IEnumerable GetTestDataCore() // Primitives and built-in types yield return new TestData( Value: new(), - AdditionalValues: [null, 42, false, 3.14, 3.14M, new int[] { 1, 2, 3 }, new SimpleRecord(1, "str", false, 3.14)], + AdditionalValues: [42, false, 3.14, 3.14M, new int[] { 1, 2, 3 }, new SimpleRecord(1, "str", false, 3.14)], ExpectedJsonSchema: "true"); - yield return new TestData(true); - yield return new TestData(42); - yield return new TestData(42); - yield return new TestData(42); - yield return new TestData(42); - yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); - yield return new TestData(42); - yield return new TestData(42); - yield return new TestData(42); - yield return new TestData(1.2f); - yield return new TestData(3.14159d); - yield return new TestData(3.14159M); + yield return new TestData(true, """{"type":"boolean"}"""); + yield return new TestData(42, """{"type":"integer"}"""); + yield return new TestData(42, """{"type":"integer"}"""); + yield return new TestData(42, """{"type":"integer"}"""); + yield return new TestData(42, """{"type":"integer"}"""); + yield return new TestData(42, """{"type":"integer"}"""); + yield return new TestData(42, """{"type":"integer"}"""); + yield return new TestData(42, """{"type":"integer"}"""); + yield return new TestData(42, """{"type":"integer"}"""); + yield return new TestData(1.2f, """{"type":"number"}"""); + yield return new TestData(3.14159d, """{"type":"number"}"""); + yield return new TestData(3.14159M, """{"type":"number"}"""); #if NET7_0_OR_GREATER - yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); - yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); + yield return new TestData(42, """{"type":"integer"}"""); + yield return new TestData(42, """{"type":"integer"}"""); #endif #if NET6_0_OR_GREATER - yield return new TestData((Half)3.141, ExpectedJsonSchema: """{"type":"number"}"""); + yield return new TestData((Half)3.141, """{"type":"number"}"""); #endif - yield return new TestData("I am a string", ExpectedJsonSchema: """{"type":["string","null"]}"""); - yield return new TestData('c', ExpectedJsonSchema: """{"type":"string","minLength":1,"maxLength":1}"""); + yield return new TestData("I am a string", """{"type":["string","null"]}"""); + yield return new TestData('c', """{"type":"string","minLength":1,"maxLength":1}"""); yield return new TestData( Value: [1, 2, 3], AdditionalValues: [[]], ExpectedJsonSchema: """{"type":["string","null"]}"""); - yield return new TestData>(new byte[] { 1, 2, 3 }, ExpectedJsonSchema: """{"type":"string"}"""); - yield return new TestData>(new byte[] { 1, 2, 3 }, ExpectedJsonSchema: """{"type":"string"}"""); + yield return new TestData>(new byte[] { 1, 2, 3 }, """{"type":"string"}"""); + yield return new TestData>(new byte[] { 1, 2, 3 }, """{"type":"string"}"""); yield return new TestData( Value: new(2021, 1, 1), - AdditionalValues: [DateTime.MinValue, DateTime.MaxValue]); + AdditionalValues: [DateTime.MinValue, DateTime.MaxValue], + ExpectedJsonSchema: """{"type":"string","format": "date-time"}"""); yield return new TestData( Value: new(new DateTime(2021, 1, 1), TimeSpan.Zero), @@ -91,35 +92,34 @@ public static IEnumerable GetTestDataCore() ExpectedJsonSchema: """{"$comment": "Represents a System.TimeSpan value.", "type":"string", "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$"}"""); #if NET6_0_OR_GREATER - yield return new TestData(new(2021, 1, 1), ExpectedJsonSchema: """{"type":"string","format": "date"}"""); - yield return new TestData(new(hour: 22, minute: 30, second: 33, millisecond: 100), ExpectedJsonSchema: """{"type":"string","format": "time"}"""); + yield return new TestData(new(2021, 1, 1), """{"type":"string","format": "date"}"""); + yield return new TestData(new(hour: 22, minute: 30, second: 33, millisecond: 100), """{"type":"string","format": "time"}"""); #endif - yield return new TestData(Guid.Empty); - yield return new TestData(new("http://example.com"), ExpectedJsonSchema: """{"type":["string","null"], "format":"uri"}"""); - yield return new TestData(new(1, 2, 3, 4), ExpectedJsonSchema: """{"$comment":"Represents a version string.", "type":["string","null"],"pattern":"^\\d+(\\.\\d+){1,3}$"}"""); - yield return new TestData(JsonDocument.Parse("""[{ "x" : 42 }]"""), ExpectedJsonSchema: "true"); - yield return new TestData(JsonDocument.Parse("""[{ "x" : 42 }]""").RootElement, ExpectedJsonSchema: "true"); - yield return new TestData(JsonNode.Parse("""[{ "x" : 42 }]"""), ExpectedJsonSchema: "true"); - yield return new TestData((JsonValue)42, ExpectedJsonSchema: "true"); - yield return new TestData(new() { ["x"] = 42 }, ExpectedJsonSchema: """{"type":["object","null"]}"""); - yield return new TestData([1, 2, 3], ExpectedJsonSchema: """{"type":["array","null"]}"""); + yield return new TestData(Guid.Empty, """{"type":"string","format":"uuid"}"""); + yield return new TestData(new("http://example.com"), """{"type":["string","null"], "format":"uri"}"""); + yield return new TestData(new(1, 2, 3, 4), """{"$comment":"Represents a version string.", "type":["string","null"],"pattern":"^\\d+(\\.\\d+){1,3}$"}"""); + yield return new TestData(JsonDocument.Parse("""[{ "x" : 42 }]"""), "true"); + yield return new TestData(JsonDocument.Parse("""[{ "x" : 42 }]""").RootElement, "true"); + yield return new TestData(JsonNode.Parse("""[{ "x" : 42 }]"""), "true"); + yield return new TestData((JsonValue)42, "true"); + yield return new TestData(new() { ["x"] = 42 }, """{"type":["object","null"]}"""); + yield return new TestData([1, 2, 3], """{"type":["array","null"]}"""); // Enum types - yield return new TestData(IntEnum.A, ExpectedJsonSchema: """{"type":"integer"}"""); - yield return new TestData(StringEnum.A, ExpectedJsonSchema: """{"enum": ["A","B","C"]}"""); - yield return new TestData(FlagsStringEnum.A, ExpectedJsonSchema: """{"type":"string"}"""); + yield return new TestData(IntEnum.A, """{"type":"integer"}"""); + yield return new TestData(StringEnum.A, """{"enum": ["A","B","C"]}"""); + yield return new TestData(FlagsStringEnum.A, """{"type":"string"}"""); // Nullable types - yield return new TestData(true, AdditionalValues: [null], ExpectedJsonSchema: """{"type":["boolean","null"]}"""); - yield return new TestData(42, AdditionalValues: [null], ExpectedJsonSchema: """{"type":["integer","null"]}"""); - yield return new TestData(3.14, AdditionalValues: [null], ExpectedJsonSchema: """{"type":["number","null"]}"""); - yield return new TestData(Guid.Empty, AdditionalValues: [null], ExpectedJsonSchema: """{"type":["string","null"],"format":"uuid"}"""); - yield return new TestData(JsonDocument.Parse("{}").RootElement, AdditionalValues: [null], ExpectedJsonSchema: "true"); - yield return new TestData(IntEnum.A, AdditionalValues: [null], ExpectedJsonSchema: """{"type":["integer","null"]}"""); - yield return new TestData(StringEnum.A, AdditionalValues: [null], ExpectedJsonSchema: """{"enum":["A","B","C",null]}"""); + yield return new TestData(true, """{"type":["boolean","null"]}"""); + yield return new TestData(42, """{"type":["integer","null"]}"""); + yield return new TestData(3.14, """{"type":["number","null"]}"""); + yield return new TestData(Guid.Empty, """{"type":["string","null"],"format":"uuid"}"""); + yield return new TestData(JsonDocument.Parse("{}").RootElement, "true"); + yield return new TestData(IntEnum.A, """{"type":["integer","null"]}"""); + yield return new TestData(StringEnum.A, """{"enum":["A","B","C",null]}"""); yield return new TestData( new(1, "two", true, 3.14), - AdditionalValues: [null], ExpectedJsonSchema: """ { "type":["object","null"], @@ -135,7 +135,7 @@ public static IEnumerable GetTestDataCore() // User-defined POCOs yield return new TestData( Value: new() { String = "string", StringNullable = "string", Int = 42, Double = 3.14, Boolean = true }, - AdditionalValues: [new() { String = "str", StringNullable = null }, null], + AdditionalValues: [new() { String = "str", StringNullable = null }], ExpectedJsonSchema: """ { "type": ["object","null"], @@ -269,6 +269,7 @@ public static IEnumerable GetTestDataCore() new() { X = 1, Y = double.PositiveInfinity, Z = 3 }, new() { X = 1, Y = double.NegativeInfinity, Z = 3 }, ], + WritesNumbersAsStrings: true, ExpectedJsonSchema: """ { "type": ["object","null"], @@ -288,7 +289,7 @@ public static IEnumerable GetTestDataCore() yield return new TestData( Value: new() { Value = 1, Next = new() { Value = 2, Next = new() { Value = 3 } } }, - AdditionalValues: [null, new() { Value = 1, Next = null }], + AdditionalValues: [new() { Value = 1, Next = null }], ExpectedJsonSchema: """ { "type": ["object","null"], @@ -397,8 +398,8 @@ of the type which points to the first occurrence. */ } """); - yield return new TestData(new() { Value = 42 }, ExpectedJsonSchema: "true"); - yield return new TestData(new() { Value = 42 }, ExpectedJsonSchema: """{"type":["object","null"],"properties":{"Value":true}}"""); + yield return new TestData(new() { Value = 42 }, "true"); + yield return new TestData(new() { Value = 42 }, """{"type":["object","null"],"properties":{"Value":true}}"""); yield return new TestData( Value: new() { @@ -495,7 +496,7 @@ of the type which points to the first occurrence. */ yield return new TestData( Value: new() { Name = "name", ExtensionData = new() { ["x"] = 42 } }, - ExpectedJsonSchema: """{"type":["object","null"],"properties":{"Name":{"type":["string","null"]}}}"""); + """{"type":["object","null"],"properties":{"Name":{"type":["string","null"]}}}"""); yield return new TestData( Value: new() { Name = "name", Age = 42 }, @@ -514,7 +515,7 @@ of the type which points to the first occurrence. */ // Global JsonUnmappedMemberHandling.Disallow setting yield return new TestData( Value: new() { String = "string", StringNullable = "string", Int = 42, Double = 3.14, Boolean = true }, - AdditionalValues: [new() { String = "str", StringNullable = null }, null], + AdditionalValues: [new() { String = "str", StringNullable = null }], ExpectedJsonSchema: """ { "type": ["object","null"], @@ -793,16 +794,16 @@ of the type which points to the first occurrence. */ }); // Collection types - yield return new TestData([1, 2, 3], ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"integer"}}"""); - yield return new TestData>([false, true, false], ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"boolean"}}"""); - yield return new TestData>(["one", "two", "three"], ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":["string","null"]}}"""); - yield return new TestData>(new([1.1, 2.2, 3.3]), ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"number"}}"""); - yield return new TestData>(new(['x', '2', '+']), ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":1}}"""); - yield return new TestData>(ImmutableArray.Create(1, 2, 3), ExpectedJsonSchema: """{"type":"array","items":{"type":"integer"}}"""); - yield return new TestData>(ImmutableList.Create("one", "two", "three"), ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":["string","null"]}}"""); - yield return new TestData>(ImmutableQueue.Create(false, false, true), ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"boolean"}}"""); - yield return new TestData([1, "two", 3.14], ExpectedJsonSchema: """{"type":["array","null"]}"""); - yield return new TestData([1, "two", 3.14], ExpectedJsonSchema: """{"type":["array","null"]}"""); + yield return new TestData([1, 2, 3], """{"type":["array","null"],"items":{"type":"integer"}}"""); + yield return new TestData>([false, true, false], """{"type":["array","null"],"items":{"type":"boolean"}}"""); + yield return new TestData>(["one", "two", "three"], """{"type":["array","null"],"items":{"type":["string","null"]}}"""); + yield return new TestData>(new([1.1, 2.2, 3.3]), """{"type":["array","null"],"items":{"type":"number"}}"""); + yield return new TestData>(new(['x', '2', '+']), """{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":1}}"""); + yield return new TestData>(ImmutableArray.Create(1, 2, 3), """{"type":"array","items":{"type":"integer"}}"""); + yield return new TestData>(ImmutableList.Create("one", "two", "three"), """{"type":["array","null"],"items":{"type":["string","null"]}}"""); + yield return new TestData>(ImmutableQueue.Create(false, false, true), """{"type":["array","null"],"items":{"type":"boolean"}}"""); + yield return new TestData([1, "two", 3.14], """{"type":["array","null"]}"""); + yield return new TestData([1, "two", 3.14], """{"type":["array","null"]}"""); // Dictionary types yield return new TestData>( @@ -1278,7 +1279,7 @@ public partial class TestTypesContext : JsonSerializerContext; // 2. Parameter-level attributes and // 3. Type-level attributes. return -#if NET9_0_OR_GREATER +#if NET9_0_OR_GREATER || !TESTS_JSON_SCHEMA_EXPORTER_POLYFILL GetAttrs(ctx.PropertyInfo?.AttributeProvider) ?? GetAttrs(ctx.PropertyInfo?.AssociatedParameter?.AttributeProvider) ?? #else diff --git a/test/Shared/Shared.Tests.csproj b/test/Shared/Shared.Tests.csproj index dc2a46d60d9..456e50f67a9 100644 --- a/test/Shared/Shared.Tests.csproj +++ b/test/Shared/Shared.Tests.csproj @@ -2,6 +2,7 @@ Microsoft.Shared.Test Unit tests for Microsoft.Shared + $(DefineConstants);TESTS_JSON_SCHEMA_EXPORTER_POLYFILL @@ -22,6 +23,5 @@ - From 4483d3cf8e72b712163b073ae301ed39776d8c66 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 31 Oct 2024 15:33:12 +0000 Subject: [PATCH 2/4] Update src/LegacySupport/DiagnosticAttributes/README.md --- src/LegacySupport/DiagnosticAttributes/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LegacySupport/DiagnosticAttributes/README.md b/src/LegacySupport/DiagnosticAttributes/README.md index 067675cbcb7..b34b86160e6 100644 --- a/src/LegacySupport/DiagnosticAttributes/README.md +++ b/src/LegacySupport/DiagnosticAttributes/README.md @@ -2,6 +2,6 @@ To use this source in your project, add the following to your `.csproj` file: ```xml - true + true ``` From 83eff2c4bc2203c471b0fa37838180d9f987025e Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 31 Oct 2024 15:33:45 +0000 Subject: [PATCH 3/4] Update src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs --- .../Utilities/AIJsonUtilities.Schema.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs index e5800408203..b555148df8b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs @@ -187,6 +187,7 @@ private static JsonElement GetJsonSchemaCached(JsonSerializerOptions options, Fu private static JsonElement GetJsonSchemaCore(JsonSerializerOptions options, FunctionParameterKey key) { _ = Throw.IfNull(options); + options.MakeReadOnly(); if (key.Type is null) { From ef91ebc7513168b8977b0be0e2e97e1cb40777ab Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 31 Oct 2024 16:23:02 +0000 Subject: [PATCH 4/4] Address feedback. --- .../Utilities/AIJsonUtilitiesTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs index 395bbd3a73a..52f9cad246d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -186,8 +186,8 @@ public static void CreateJsonSchema_ValidateWithTestData(ITestData testData) if (testData.Value is null || testData.WritesNumbersAsStrings) { - // Our generated schema does not accept null root values - // or numbers formatted as strings, so we skip this test. + // By design, our generated schema does not accept null root values + // or numbers formatted as strings, so we skip schema validation. return; }