|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Schema;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
#if !NET9_0_OR_GREATER
using System.Xml.Linq;
#endif
using Xunit;
using static Microsoft.Extensions.AI.JsonSchemaExporter.TestTypes;
#pragma warning disable SA1402 // File may only contain a single type
namespace Microsoft.Extensions.AI.JsonSchemaExporter;
public abstract class JsonSchemaExporterTests
{
protected abstract JsonSerializerOptions Options { get; }
[Theory]
[MemberData(nameof(TestTypes.GetTestData), MemberType = typeof(TestTypes))]
public void TestTypes_GeneratesExpectedJsonSchema(ITestData testData)
{
JsonSerializerOptions options = testData.Options is { } opts
? new(opts) { TypeInfoResolver = Options.TypeInfoResolver }
: Options;
JsonNode schema = options.GetJsonSchemaAsNode(testData.Type, (JsonSchemaExporterOptions?)testData.ExporterOptions);
SchemaTestHelpers.AssertEqualJsonSchema(testData.ExpectedJsonSchema, schema);
}
[Theory]
[MemberData(nameof(TestTypes.GetTestDataUsingAllValues), MemberType = typeof(TestTypes))]
public void TestTypes_SerializedValueMatchesGeneratedSchema(ITestData testData)
{
JsonSerializerOptions options = testData.Options is { } opts
? new(opts) { TypeInfoResolver = Options.TypeInfoResolver }
: Options;
JsonNode schema = options.GetJsonSchemaAsNode(testData.Type, (JsonSchemaExporterOptions?)testData.ExporterOptions);
JsonNode? instance = JsonSerializer.SerializeToNode(testData.Value, testData.Type, options);
SchemaTestHelpers.AssertDocumentMatchesSchema(schema, instance);
}
[Theory]
[InlineData(typeof(string), "string")]
[InlineData(typeof(int[]), "array")]
[InlineData(typeof(Dictionary<string, int>), "object")]
[InlineData(typeof(TestTypes.SimplePoco), "object")]
public void TreatNullObliviousAsNonNullable_True_MarksAllReferenceTypesAsNonNullable(Type referenceType, string expectedType)
{
Assert.True(!referenceType.IsValueType);
var config = new JsonSchemaExporterOptions { TreatNullObliviousAsNonNullable = true };
JsonNode schema = Options.GetJsonSchemaAsNode(referenceType, config);
JsonValue type = Assert.IsAssignableFrom<JsonValue>(schema["type"]);
Assert.Equal(expectedType, (string)type!);
}
[Theory]
[InlineData(typeof(int), "integer")]
[InlineData(typeof(double), "number")]
[InlineData(typeof(bool), "boolean")]
[InlineData(typeof(ImmutableArray<int>), "array")]
[InlineData(typeof(TestTypes.StructDictionary<string, int>), "object")]
[InlineData(typeof(TestTypes.SimpleRecordStruct), "object")]
public void TreatNullObliviousAsNonNullable_True_DoesNotImpactNonReferenceTypes(Type referenceType, string expectedType)
{
Assert.True(referenceType.IsValueType);
var config = new JsonSchemaExporterOptions { TreatNullObliviousAsNonNullable = true };
JsonNode schema = Options.GetJsonSchemaAsNode(referenceType, config);
JsonValue value = Assert.IsAssignableFrom<JsonValue>(schema["type"]);
Assert.Equal(expectedType, (string)value!);
}
#if !NET9_0 // Disable until https://github.com/dotnet/runtime/pull/108764 gets backported
[Fact]
public void CanGenerateXElementSchema()
{
JsonNode schema = Options.GetJsonSchemaAsNode(typeof(XElement));
Assert.True(schema.ToJsonString().Length < 100_000);
}
#endif
#if !NET9_0 // Disable until https://github.com/dotnet/runtime/pull/109954 gets backported
[Fact]
public void TransformSchemaNode_PropertiesWithCustomConverters()
{
// Regression test for https://github.com/dotnet/runtime/issues/109868
List<(Type? parentType, string? propertyName, Type type)> visitedNodes = new();
JsonSchemaExporterOptions exporterOptions = new()
{
TransformSchemaNode = (ctx, schema) =>
{
#if NET9_0_OR_GREATER
visitedNodes.Add((ctx.PropertyInfo?.DeclaringType, ctx.PropertyInfo?.Name, ctx.TypeInfo.Type));
#else
visitedNodes.Add((ctx.DeclaringType, ctx.PropertyInfo?.Name, ctx.TypeInfo.Type));
#endif
return schema;
}
};
List<(Type? parentType, string? propertyName, Type type)> expectedNodes =
[
(typeof(ClassWithPropertiesUsingCustomConverters), "Prop1", typeof(ClassWithPropertiesUsingCustomConverters.ClassWithCustomConverter1)),
(typeof(ClassWithPropertiesUsingCustomConverters), "Prop2", typeof(ClassWithPropertiesUsingCustomConverters.ClassWithCustomConverter2)),
(null, null, typeof(ClassWithPropertiesUsingCustomConverters)),
];
Options.GetJsonSchemaAsNode(typeof(ClassWithPropertiesUsingCustomConverters), exporterOptions);
Assert.Equal(expectedNodes, visitedNodes);
}
#endif
[Fact]
public void TreatNullObliviousAsNonNullable_True_DoesNotImpactObjectType()
{
var config = new JsonSchemaExporterOptions { TreatNullObliviousAsNonNullable = true };
JsonNode schema = Options.GetJsonSchemaAsNode(typeof(object), config);
Assert.False(schema is JsonObject jObj && jObj.ContainsKey("type"));
}
[Fact]
public void TypeWithDisallowUnmappedMembers_AdditionalPropertiesFailValidation()
{
JsonNode schema = Options.GetJsonSchemaAsNode(typeof(TestTypes.PocoDisallowingUnmappedMembers));
JsonNode? jsonWithUnmappedProperties = JsonNode.Parse("""{ "UnmappedProperty" : {} }"""{ "UnmappedProperty" : {} }""");
SchemaTestHelpers.AssertDoesNotMatchSchema(schema, jsonWithUnmappedProperties);
}
[Fact]
public void GetJsonSchema_NullInputs_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => ((JsonSerializerOptions)null!).GetJsonSchemaAsNode(typeof(int)));
Assert.Throws<ArgumentNullException>(() => Options.GetJsonSchemaAsNode(type: null!));
Assert.Throws<ArgumentNullException>(() => ((JsonTypeInfo)null!).GetJsonSchemaAsNode());
}
[Fact]
public void GetJsonSchema_NoResolver_ThrowInvalidOperationException()
{
var options = new JsonSerializerOptions();
Assert.Throws<InvalidOperationException>(() => options.GetJsonSchemaAsNode(typeof(int)));
}
[Fact]
public void MaxDepth_SetToZero_NonTrivialSchema_ThrowsInvalidOperationException()
{
JsonSerializerOptions options = new(Options) { MaxDepth = 1 };
var ex = Assert.Throws<InvalidOperationException>(() => options.GetJsonSchemaAsNode(typeof(TestTypes.SimplePoco)));
Assert.Contains("The depth of the generated JSON schema exceeds the JsonSerializerOptions.MaxDepth setting.", ex.Message);
}
[Fact]
public void ReferenceHandlePreserve_Enabled_ThrowsNotSupportedException()
{
var options = new JsonSerializerOptions(Options) { ReferenceHandler = ReferenceHandler.Preserve };
options.MakeReadOnly();
var ex = Assert.Throws<NotSupportedException>(() => options.GetJsonSchemaAsNode(typeof(TestTypes.SimplePoco)));
Assert.Contains("ReferenceHandler.Preserve", ex.Message);
}
}
public sealed class ReflectionJsonSchemaExporterTests : JsonSchemaExporterTests
{
protected override JsonSerializerOptions Options => JsonSerializerOptions.Default;
}
public sealed class SourceGenJsonSchemaExporterTests : JsonSchemaExporterTests
{
protected override JsonSerializerOptions Options => TestTypes.TestTypesContext.Default.Options;
}
|