Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add annotations and regression testing for members accessed by legacy schema generation. #109424

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace System.Text.Json.Serialization.Converters
{
internal sealed class EnumConverter<T> : JsonPrimitiveConverter<T>
internal sealed class EnumConverter<T> : JsonPrimitiveConverter<T> // Do not rename FQN (legacy schema generation)
where T : struct, Enum
{
private static readonly TypeCode s_enumTypeCode = Type.GetTypeCode(typeof(T));
Expand All @@ -22,9 +22,8 @@ internal sealed class EnumConverter<T> : JsonPrimitiveConverter<T>
private static readonly bool s_isSignedEnum = ((int)s_enumTypeCode % 2) == 1;
private static readonly bool s_isFlagsEnum = typeof(T).IsDefined(typeof(FlagsAttribute), inherit: false);

private readonly EnumConverterOptions _converterOptions;

private readonly JsonNamingPolicy? _namingPolicy;
private readonly EnumConverterOptions _converterOptions; // Do not rename (legacy schema generation)
private readonly JsonNamingPolicy? _namingPolicy; // Do not rename (legacy schema generation)

/// <summary>
/// Stores metadata for the individual fields declared on the enum.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace System.Text.Json.Serialization.Converters
{
[Flags]
internal enum EnumConverterOptions
internal enum EnumConverterOptions // Do not modify (legacy schema generation)
{
/// <summary>
/// Allow string values.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace System.Text.Json.Serialization.Converters
{
internal sealed class NullableConverter<T> : JsonConverter<T?> where T : struct
internal sealed class NullableConverter<T> : JsonConverter<T?> where T : struct // Do not rename FQN (legacy schema generation)
{
internal override Type? ElementType => typeof(T);
internal override JsonConverter? NullableElementConverter => _elementConverter;
Expand All @@ -15,7 +15,7 @@ internal sealed class NullableConverter<T> : JsonConverter<T?> where T : struct

// It is possible to cache the underlying converter since this is an internal converter and
// an instance is created only once for each JsonSerializerOptions instance.
private readonly JsonConverter<T> _elementConverter;
private readonly JsonConverter<T> _elementConverter; // Do not rename (legacy schema generation)

public NullableConverter(JsonConverter<T> elementConverter)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,12 +526,21 @@ private static NullabilityState DetermineParameterNullability(ParameterInfo para

static byte[]? GetNullableFlags(MemberInfo member)
{
foreach (Attribute attr in member.GetCustomAttributes())
foreach (CustomAttributeData attr in member.GetCustomAttributesData())
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
{
Type attrType = attr.GetType();
if (attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableAttribute")
Type attrType = attr.AttributeType;
if (attrType.Name == "NullableAttribute" && attrType.Namespace == "System.Runtime.CompilerServices")
{
return (byte[])attr.GetType().GetField("NullableFlags")?.GetValue(attr)!;
foreach (CustomAttributeTypedArgument ctorArg in attr.ConstructorArguments)
{
switch (ctorArg.Value)
{
case byte flag:
return [flag];
case byte[] flags:
return flags;
}
}
}
}

Expand All @@ -540,12 +549,18 @@ private static NullabilityState DetermineParameterNullability(ParameterInfo para

static byte? GetNullableContextFlag(MemberInfo member)
{
foreach (Attribute attr in member.GetCustomAttributes())
foreach (CustomAttributeData attr in member.GetCustomAttributesData())
{
Type attrType = attr.GetType();
if (attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableContextAttribute")
Type attrType = attr.AttributeType;
if (attrType.Name == "NullableContextAttribute" && attrType.Namespace == "System.Runtime.CompilerServices")
{
return (byte?)attr?.GetType().GetField("Flag")?.GetValue(attr)!;
foreach (CustomAttributeTypedArgument ctorArg in attr.ConstructorArguments)
{
if (ctorArg.Value is byte flag)
{
return flag;
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ public JsonObjectCreationHandling? ObjectCreationHandling
private JsonObjectCreationHandling? _objectCreationHandling;
internal JsonObjectCreationHandling EffectiveObjectCreationHandling { get; private set; }

internal string? MemberName { get; set; }
internal string? MemberName { get; set; } // Do not rename (legacy schema generation)
internal MemberTypes MemberType { get; set; }
internal bool IsVirtual { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,52 @@ public void JsonSchemaExporterOptions_Default_IsSame()
Assert.Same(JsonSchemaExporterOptions.Default, JsonSchemaExporterOptions.Default);
}

#if !BUILDING_SOURCE_GENERATOR_TESTS
[Fact]
public void LegacySchemaExporter_CanAccessReflectedMembers()
{
// A number of libraries such as Microsoft.Extensions.AI and Semantic Kernel
// rely on a polyfilled version of JsonSchemaExporter for System.Text.Json v8
// that uses private reflection to access necessary metadata. This test validates
// that the necessary members are still present in newer implementations of STJ.

JsonStringEnumConverter converter = new(namingPolicy: JsonNamingPolicy.CamelCase, allowIntegerValues: false);
JsonSerializerOptions options = new(JsonSerializerOptions.Default) { Converters = { converter } };
JsonConverter nullableConverter = options.GetConverter(typeof(BindingFlags?));

Type nullableConverterType = nullableConverter.GetType();
Assert.True(nullableConverterType.IsGenericType);
Assert.StartsWith("System.Text.Json.Serialization.Converters.NullableConverter`1", nullableConverterType.FullName);

FieldInfo elementConverterField = nullableConverterType.GetField("_elementConverter", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(elementConverterField);
var enumConverter = (JsonConverter)elementConverterField.GetValue(nullableConverter);
Assert.NotNull(enumConverter);

Type enumConverterType = enumConverter.GetType();
Assert.True(enumConverterType.IsGenericType);
Assert.StartsWith("System.Text.Json.Serialization.Converters.EnumConverter`1", enumConverterType.FullName);

FieldInfo namingPolicyField = enumConverterType.GetField("_namingPolicy", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(namingPolicyField);
Assert.Same(JsonNamingPolicy.CamelCase, namingPolicyField.GetValue(enumConverter));

FieldInfo converterOptionsField = enumConverterType.GetField("_converterOptions", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(converterOptionsField);
Assert.Equal(1, (int)converterOptionsField.GetValue(enumConverter));

JsonPropertyInfo propertyInfo = PocoWithPropertyContext.Default.PocoWithProperty.Properties.Single();
PropertyInfo memberNameProperty = typeof(JsonPropertyInfo).GetProperty("MemberName", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(memberNameProperty);
Assert.Equal("Value", memberNameProperty.GetValue(propertyInfo));
}

record PocoWithProperty(int Value);

[JsonSerializable(typeof(PocoWithProperty))]
partial class PocoWithPropertyContext : JsonSerializerContext;
#endif

protected void AssertValidJsonSchema(Type type, string expectedJsonSchema, JsonNode actualJsonSchema)
{
JsonNode? expectedJsonSchemaNode = JsonNode.Parse(expectedJsonSchema, documentOptions: new() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true });
Expand Down
Loading