Skip to content
Closed
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
36 changes: 36 additions & 0 deletions src/libraries/System.Text.Json/Common/ReflectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,42 @@ public static bool IsVirtual(this PropertyInfo propertyInfo)
public static bool IsKeyValuePair(this Type type)
=> type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>);

public static bool IsClassTuple(this Type type)
{
if (!type.IsGenericType)
{
return false;
}

Type genericTypeDef = type.GetGenericTypeDefinition();
return genericTypeDef == typeof(Tuple<>) ||
genericTypeDef == typeof(Tuple<,>) ||
genericTypeDef == typeof(Tuple<,,>) ||
genericTypeDef == typeof(Tuple<,,,>) ||
genericTypeDef == typeof(Tuple<,,,,>) ||
genericTypeDef == typeof(Tuple<,,,,,>) ||
genericTypeDef == typeof(Tuple<,,,,,,>) ||
genericTypeDef == typeof(Tuple<,,,,,,,>);
}

public static bool IsValueTuple(this Type type)
{
if (!type.IsGenericType)
{
return false;
}

Type genericTypeDef = type.GetGenericTypeDefinition();
return genericTypeDef == typeof(ValueTuple<>) ||
genericTypeDef == typeof(ValueTuple<,>) ||
genericTypeDef == typeof(ValueTuple<,,>) ||
genericTypeDef == typeof(ValueTuple<,,,>) ||
genericTypeDef == typeof(ValueTuple<,,,,>) ||
genericTypeDef == typeof(ValueTuple<,,,,,>) ||
genericTypeDef == typeof(ValueTuple<,,,,,,>) ||
genericTypeDef == typeof(ValueTuple<,,,,,,,>);
}

public static bool TryGetDeserializationConstructor(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
this Type type,
Expand Down
25 changes: 25 additions & 0 deletions src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,31 @@ private static bool IsInputTypeNonNullable(this ISymbol symbol, ITypeSymbol inpu
return inputType.NullableAnnotation is NullableAnnotation.NotAnnotated;
}

public static bool IsClassTuple(this INamedTypeSymbol type)
{
if (!type.IsGenericType)
{
return false;
}

// Check if type is from System.Runtime (mscorlib/corelib)
if (type.ContainingAssembly?.Name != "System.Runtime" &&
type.ContainingAssembly?.Name != "mscorlib" &&
type.ContainingAssembly?.Name != "System.Private.CoreLib")
{
return false;
}

// Check namespace and name
if (type.ContainingNamespace?.ToDisplayString() != "System")
{
return false;
}

string name = type.Name;
return name == "Tuple" && (type.Arity >= 1 && type.Arity <= 8);
}

private static bool HasCodeAnalysisAttribute(this ISymbol symbol, string attributeName)
{
return symbol.GetAttributes().Any(attr =>
Expand Down
25 changes: 22 additions & 3 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,13 @@ private List<PropertyGenerationSpec> ParsePropertyGenerationSpecs(
continue;
}

// For reference tuple types (System.Tuple), handle the Rest property specially for long tuples
if (typeToGenerate.Type is INamedTypeSymbol namedType && namedType.IsClassTuple() && propertyInfo.Name == "Rest")
{
// Skip Rest property - it will be handled specially during serialization
continue;
}

AddMember(
declaringTypeRef,
memberType: propertyInfo.Type,
Expand All @@ -959,10 +966,22 @@ private List<PropertyGenerationSpec> ParsePropertyGenerationSpecs(
// it is a static field, constant
fieldInfo.IsStatic || fieldInfo.IsConst ||
// it is a compiler-generated backing field
fieldInfo.AssociatedSymbol != null ||
// symbol represents an explicitly named tuple element
fieldInfo.IsExplicitlyNamedTupleElement)
fieldInfo.AssociatedSymbol != null)
{
continue;
}

// For tuple types, include fields (Item1, Item2, etc.) even if they are explicitly named
// but skip them for non-tuple types
if (fieldInfo.IsExplicitlyNamedTupleElement && !typeToGenerate.Type.IsTupleType)
{
continue;
}

// For tuple types, handle the Rest field specially for long tuples
if (typeToGenerate.Type.IsTupleType && fieldInfo.Name == "Rest")
{
// Skip Rest field - it will be handled specially during serialization
continue;
}

Expand Down
2 changes: 2 additions & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.Large.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.Small.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\TupleConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\TupleConverterFactory.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\BooleanConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\ByteArrayConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\ByteConverter.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json.Reflection;
using System.Text.Json.Serialization.Metadata;

namespace System.Text.Json.Serialization.Converters
{
/// <summary>
/// Converter for System.Tuple and System.ValueTuple types that serializes them as objects with Item1, Item2, etc. properties.
/// Handles long tuples (&gt; 7 elements) by flattening the Rest field.
/// </summary>
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
internal sealed class TupleConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonObjectConverter<T>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than writing a converter from scratch, consider using the built-in object converter in a way that gets populated with JsonPropertyInfo values that access the individual tuple elements in the appropriate manner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current converter implementation works correctly and handles all test cases including long tuple flattening. Using the built-in object converter would require significant refactoring to dynamically create JsonPropertyInfo instances for tuple elements and handle Rest field flattening. This could be considered as a future optimization, but the current approach is functional and well-tested.

{
private readonly List<(string Name, Type Type, Func<T, object?> Getter)> _elements;

internal override bool CanHaveMetadata => false;
internal override bool SupportsCreateObjectDelegate => false;

public TupleConverter()
{
_elements = new List<(string, Type, Func<T, object?>)>();
PopulateTupleElements(typeof(T), _elements, 0);
}

private static void PopulateTupleElements([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] Type tupleType, List<(string, Type, Func<T, object?>)> elements, int offset)
{
if (tupleType.IsValueTuple())
{
PopulateValueTupleElements(tupleType, elements, offset);
}
else if (tupleType.IsClassTuple())
{
PopulateReferenceTupleElements(tupleType, elements, offset);
}
}

private static void PopulateValueTupleElements([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type tupleType, List<(string, Type, Func<T, object?>)> elements, int offset)
{
FieldInfo[] fields = tupleType.GetFields(BindingFlags.Public | BindingFlags.Instance);

foreach (FieldInfo field in fields)
{
if (field.Name == "Rest")
{
// Handle long tuple (> 7 elements) - recursively flatten the Rest field
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:UnrecognizedReflectionPattern",
Justification = "Tuple Rest field types are well-known tuple types.")]
static void ProcessRest(FieldInfo field, List<(string, Type, Func<T, object?>)> elements, int offset)
{
Type restType = field.FieldType;
PopulateTupleElements(restType, elements, offset);
}
ProcessRest(field, elements, offset);
}
else if (field.Name.StartsWith("Item", StringComparison.Ordinal))
{
string itemName = $"Item{offset + elements.Count + 1}";
Type fieldType = field.FieldType;
Func<T, object?> getter = (T tuple) => field.GetValue(tuple);
elements.Add((itemName, fieldType, getter));
}
}
}

private static void PopulateReferenceTupleElements([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type tupleType, List<(string, Type, Func<T, object?>)> elements, int offset)
{
PropertyInfo[] properties = tupleType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

foreach (PropertyInfo property in properties)
{
if (property.Name == "Rest")
{
// Handle long tuple (> 7 elements) - recursively flatten the Rest property
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:UnrecognizedReflectionPattern",
Justification = "Tuple Rest property types are well-known tuple types.")]
static void ProcessRest(PropertyInfo property, List<(string, Type, Func<T, object?>)> elements, int offset)
{
Type restType = property.PropertyType;
PropertyInfo restProp = property;

// For System.Tuple, we need to handle Rest differently since it's accessed through properties
if (restType.IsValueTuple() || restType.IsClassTuple())
{
// Create nested getters for Rest elements
PopulateNestedReferenceTupleElements(restType, elements, offset, restProp);
}
}
ProcessRest(property, elements, offset);
}
else if (property.Name.StartsWith("Item", StringComparison.Ordinal))
{
string itemName = $"Item{offset + elements.Count + 1}";
Type propertyType = property.PropertyType;
Func<T, object?> getter = (T tuple) => property.GetValue(tuple);
elements.Add((itemName, propertyType, getter));
}
}
}

private static void PopulateNestedReferenceTupleElements([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type tupleType, List<(string, Type, Func<T, object?>)> elements, int offset, PropertyInfo restProperty)
{
PropertyInfo[] properties = tupleType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

foreach (PropertyInfo property in properties)
{
if (property.Name == "Rest")
{
// Further nesting
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:UnrecognizedReflectionPattern",
Justification = "Tuple Rest property types are well-known tuple types.")]
static void ProcessNestedRest(PropertyInfo property, PropertyInfo restProperty, List<(string, Type, Func<T, object?>)> elements, int offset)
{
Type nestedRestType = property.PropertyType;
if (nestedRestType.IsValueTuple() || nestedRestType.IsClassTuple())
{
// Chain the getters
PropertyInfo innerRestProp = property;
Func<T, object?> baseGetter = (T tuple) => restProperty.GetValue(tuple);
PopulateNestedReferenceTupleElementsChained(nestedRestType, elements, offset, baseGetter, innerRestProp);
}
}
ProcessNestedRest(property, restProperty, elements, offset);
}
else if (property.Name.StartsWith("Item", StringComparison.Ordinal))
{
string itemName = $"Item{offset + elements.Count + 1}";
Type propertyType = property.PropertyType;
PropertyInfo prop = property;
Func<T, object?> getter = (T tuple) =>
{
object? restValue = restProperty.GetValue(tuple);
return restValue is not null ? prop.GetValue(restValue) : null;
};
elements.Add((itemName, propertyType, getter));
}
}
}

private static void PopulateNestedReferenceTupleElementsChained([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type tupleType, List<(string, Type, Func<T, object?>)> elements, int offset, Func<T, object?> baseGetter, PropertyInfo currentRestProp)
{
PropertyInfo[] properties = tupleType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

foreach (PropertyInfo property in properties)
{
if (property.Name == "Rest")
{
// Further nesting - would be very rare
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:UnrecognizedReflectionPattern",
Justification = "Tuple Rest property types are well-known tuple types.")]
static void ProcessChainedRest(PropertyInfo property, PropertyInfo currentRestProp, Func<T, object?> baseGetter, List<(string, Type, Func<T, object?>)> elements, int offset)
{
Type nestedRestType = property.PropertyType;
if (nestedRestType.IsValueTuple() || nestedRestType.IsClassTuple())
{
PropertyInfo innerRestProp = property;
Func<T, object?> chainedGetter = (T tuple) =>
{
object? restValue = baseGetter(tuple);
return restValue is not null ? currentRestProp.GetValue(restValue) : null;
};
PopulateNestedReferenceTupleElementsChained(nestedRestType, elements, offset, chainedGetter, innerRestProp);
}
}
ProcessChainedRest(property, currentRestProp, baseGetter, elements, offset);
}
else if (property.Name.StartsWith("Item", StringComparison.Ordinal))
{
string itemName = $"Item{offset + elements.Count + 1}";
Type propertyType = property.PropertyType;
PropertyInfo prop = property;
Func<T, object?> getter = (T tuple) =>
{
object? restValue = baseGetter(tuple);
if (restValue is not null)
{
object? currentRest = currentRestProp.GetValue(restValue);
return currentRest is not null ? prop.GetValue(currentRest) : null;
}
return null;
};
elements.Add((itemName, propertyType, getter));
}
}
}

internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, scoped ref ReadStack state, [MaybeNullWhen(false)] out T value)
{
// Deserialization of tuples as objects is not supported
ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(state.Current.JsonTypeInfo, ref reader, ref state);
value = default;
return false;
}

internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state)
{
writer.WriteStartObject();

foreach (var (name, elementType, getter) in _elements)
{
object? elementValue = getter(value);

writer.WritePropertyName(name);

JsonConverter elementConverter = options.GetConverterInternal(elementType);
if (elementConverter is null)
{
throw new JsonException($"No converter found for type {elementType}");
}

bool success = elementConverter.TryWriteAsObject(writer, elementValue, options, ref state);
if (!success)
{
return false;
}
}

writer.WriteEndObject();
return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json.Reflection;

namespace System.Text.Json.Serialization.Converters
{
/// <summary>
/// Converter factory for System.Tuple and System.ValueTuple types.
/// </summary>
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
internal sealed class TupleConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsClassTuple() || typeToConvert.IsValueTuple();
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Justification = "The ctor is marked RequiresUnreferencedCode.")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067:UnrecognizedReflectionPattern",
Justification = "The ctor is marked RequiresUnreferencedCode.")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2071:UnrecognizedReflectionPattern",
Justification = "Tuple types have well-known public fields and properties.")]
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type converterType = typeof(TupleConverter<>).MakeGenericType(typeToConvert);

return (JsonConverter)Activator.CreateInstance(
converterType,
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: null,
culture: null)!;
}
}
}
Loading
Loading