Skip to content

Commit

Permalink
Add annotations and regression testing for members accessed by legacy…
Browse files Browse the repository at this point in the history
… schema generation. (#109424)

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

* Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs

* Incorporate feedback from dotnet/extensions#5591
  • Loading branch information
eiriktsarpalis authored Nov 1, 2024
1 parent 357b09d commit 0929902
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 16 deletions.
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())
{
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

0 comments on commit 0929902

Please sign in to comment.