diff --git a/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs b/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs
index e8935677ba99bf..65495f28a803fc 100644
--- a/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs
+++ b/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs
@@ -363,6 +363,55 @@ public bool IsImmutableDictionaryType(ITypeSymbol type, out string? factoryTypeF
return false;
}
+ ///
+ /// Determines whether the specified type symbol is a System.Tuple generic type.
+ ///
+ public bool IsReferenceTupleType(INamedTypeSymbol type)
+ {
+ if (!type.IsGenericType)
+ {
+ return false;
+ }
+
+ INamedTypeSymbol def = type.ConstructedFrom;
+ return def.Arity switch
+ {
+ 1 => SymbolEqualityComparer.Default.Equals(def, TupleOfT1Type),
+ 2 => SymbolEqualityComparer.Default.Equals(def, TupleOfT2Type),
+ 3 => SymbolEqualityComparer.Default.Equals(def, TupleOfT3Type),
+ 4 => SymbolEqualityComparer.Default.Equals(def, TupleOfT4Type),
+ 5 => SymbolEqualityComparer.Default.Equals(def, TupleOfT5Type),
+ 6 => SymbolEqualityComparer.Default.Equals(def, TupleOfT6Type),
+ 7 => SymbolEqualityComparer.Default.Equals(def, TupleOfT7Type),
+ 8 => SymbolEqualityComparer.Default.Equals(def, TupleOfT8Type),
+ _ => false,
+ };
+ }
+
+ public INamedTypeSymbol? TupleOfT1Type => GetOrResolveType("System.Tuple`1", ref _TupleOfT1Type);
+ private Option _TupleOfT1Type;
+
+ public INamedTypeSymbol? TupleOfT2Type => GetOrResolveType("System.Tuple`2", ref _TupleOfT2Type);
+ private Option _TupleOfT2Type;
+
+ public INamedTypeSymbol? TupleOfT3Type => GetOrResolveType("System.Tuple`3", ref _TupleOfT3Type);
+ private Option _TupleOfT3Type;
+
+ public INamedTypeSymbol? TupleOfT4Type => GetOrResolveType("System.Tuple`4", ref _TupleOfT4Type);
+ private Option _TupleOfT4Type;
+
+ public INamedTypeSymbol? TupleOfT5Type => GetOrResolveType("System.Tuple`5", ref _TupleOfT5Type);
+ private Option _TupleOfT5Type;
+
+ public INamedTypeSymbol? TupleOfT6Type => GetOrResolveType("System.Tuple`6", ref _TupleOfT6Type);
+ private Option _TupleOfT6Type;
+
+ public INamedTypeSymbol? TupleOfT7Type => GetOrResolveType("System.Tuple`7", ref _TupleOfT7Type);
+ private Option _TupleOfT7Type;
+
+ public INamedTypeSymbol? TupleOfT8Type => GetOrResolveType("System.Tuple`8", ref _TupleOfT8Type);
+ private Option _TupleOfT8Type;
+
private INamedTypeSymbol? GetOrResolveType(Type type, ref Option field)
=> GetOrResolveType(type.FullName!, ref field);
diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
index 86cd355dffff03..5fed43844cda3e 100644
--- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
+++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
@@ -859,7 +859,7 @@ private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGeneratio
if (defaultCheckType != SerializedValueCheckType.None)
{
// Use temporary variable to evaluate property value only once
- string localVariableName = $"__value_{propertyGenSpec.NameSpecifiedInSourceCode.TrimStart('@')}";
+ string localVariableName = $"__value_{propertyGenSpec.NameSpecifiedInSourceCode.TrimStart('@').Replace('.', '_')}";
writer.WriteLine($"{propertyGenSpec.PropertyType.FullyQualifiedName} {localVariableName} = {objectExpr}.{propertyGenSpec.NameSpecifiedInSourceCode};");
propValueExpr = localVariableName;
}
@@ -944,6 +944,11 @@ static void ThrowPropertyNullException(string propertyName)
private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec typeGenerationSpec)
{
+ if (typeGenerationSpec.HasNestedTupleElements)
+ {
+ return GetTupleCtorInvocationFunc(typeGenerationSpec);
+ }
+
ImmutableEquatableArray parameters = typeGenerationSpec.CtorParamGenSpecs;
ImmutableEquatableArray propertyInitializers = typeGenerationSpec.PropertyInitializerSpecs;
@@ -1019,6 +1024,107 @@ static string GetParamExpression(ParameterGenerationSpec param, string argsVarNa
}
}
+ ///
+ /// Generates nested constructor invocation for tuple types.
+ /// For a 10-element tuple, generates:
+ /// static args => new ValueTuple<...>((T)args[0], ..., new ValueTuple<...>((T)args[7], ...))
+ ///
+ private static string GetTupleCtorInvocationFunc(TypeGenerationSpec typeGenerationSpec)
+ {
+ ImmutableEquatableArray parameters = typeGenerationSpec.CtorParamGenSpecs;
+ const string ArgsVarName = "args";
+
+ StringBuilder sb = new($"static {ArgsVarName} => ");
+ AppendNestedTupleConstructor(sb, typeGenerationSpec, parameters, 0, ArgsVarName);
+ return sb.ToString();
+ }
+
+ ///
+ /// Recursively appends nested tuple constructor expressions.
+ /// Groups every 7 elements and wraps the remainder in a nested ValueTuple/Tuple constructor.
+ ///
+ private static void AppendNestedTupleConstructor(
+ StringBuilder sb,
+ TypeGenerationSpec typeSpec,
+ ImmutableEquatableArray parameters,
+ int startIndex,
+ string argsVarName)
+ {
+ int remaining = parameters.Count - startIndex;
+ int directArgs = remaining > 7 ? 7 : remaining;
+
+ // Build the constructor type name from the parameter types at this nesting level.
+ // We must use the CLR type name (e.g. global::System.ValueTuple)
+ // rather than the C# tuple syntax (int, int, ...) because `new` doesn't work with tuple syntax.
+ string tuplePrefix = typeSpec.IsValueTuple ? "global::System.ValueTuple" : "global::System.Tuple";
+
+ sb.Append($"new {tuplePrefix}<");
+ for (int i = 0; i < directArgs; i++)
+ {
+ sb.Append(parameters[startIndex + i].ParameterType.FullyQualifiedName);
+ sb.Append(", ");
+ }
+
+ if (remaining > 7)
+ {
+ // The last type argument is the nested tuple type — build it recursively.
+ AppendNestedTupleTypeName(sb, typeSpec, parameters, startIndex + 7);
+ }
+ else
+ {
+ sb.Length -= 2; // remove last ", "
+ }
+
+ sb.Append(">(");
+
+ for (int i = 0; i < directArgs; i++)
+ {
+ ParameterGenerationSpec param = parameters[startIndex + i];
+ sb.Append($"({param.ParameterType.FullyQualifiedName}){argsVarName}[{param.ArgsIndex}]");
+
+ if (i < directArgs - 1 || remaining > 7)
+ {
+ sb.Append(", ");
+ }
+ }
+
+ if (remaining > 7)
+ {
+ AppendNestedTupleConstructor(sb, typeSpec, parameters, startIndex + 7, argsVarName);
+ }
+
+ sb.Append(')');
+ }
+
+ private static void AppendNestedTupleTypeName(
+ StringBuilder sb,
+ TypeGenerationSpec typeSpec,
+ ImmutableEquatableArray parameters,
+ int startIndex)
+ {
+ int remaining = parameters.Count - startIndex;
+ int directArgs = remaining > 7 ? 7 : remaining;
+ string tuplePrefix = typeSpec.IsValueTuple ? "global::System.ValueTuple" : "global::System.Tuple";
+
+ sb.Append($"{tuplePrefix}<");
+ for (int i = 0; i < directArgs; i++)
+ {
+ sb.Append(parameters[startIndex + i].ParameterType.FullyQualifiedName);
+ sb.Append(", ");
+ }
+
+ if (remaining > 7)
+ {
+ AppendNestedTupleTypeName(sb, typeSpec, parameters, startIndex + 7);
+ }
+ else
+ {
+ sb.Length -= 2;
+ }
+
+ sb.Append('>');
+ }
+
private static string? GetPrimitiveWriterMethod(TypeGenerationSpec type)
{
return type.PrimitiveTypeKind switch
diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
index 3967ea7d51ab65..350f26819cdb8c 100644
--- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
+++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
@@ -692,9 +692,36 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener
implementsIJsonOnSerializing = _knownSymbols.IJsonOnSerializingType.IsAssignableFrom(type);
implementsIJsonOnSerialized = _knownSymbols.IJsonOnSerializedType.IsAssignableFrom(type);
- ctorParamSpecs = ParseConstructorParameters(typeToGenerate, constructor, out constructionStrategy, out constructorSetsRequiredMembers);
- propertySpecs = ParsePropertyGenerationSpecs(contextType, typeToGenerate, typeIgnoreCondition, options, typeNamingPolicy, out hasExtensionDataProperty, out fastPathPropertyIndices);
- propertyInitializerSpecs = ParsePropertyInitializers(ctorParamSpecs, propertySpecs, constructorSetsRequiredMembers, ref constructionStrategy);
+ bool isTupleType = type.IsTupleType || IsReferenceTupleType(type);
+
+ if (isTupleType)
+ {
+ propertySpecs = ParseTuplePropertyGenerationSpecs(contextType, typeToGenerate, options, out fastPathPropertyIndices);
+ hasExtensionDataProperty = false;
+
+ bool hasRestNesting = type is INamedTypeSymbol { Arity: 8 } namedTuple &&
+ (namedTuple.TypeArguments[7].IsTupleType || IsReferenceTupleType(namedTuple.TypeArguments[7]));
+ if (hasRestNesting)
+ {
+ // >7 element tuples need flattened constructor params and nested ctor invocation
+ ctorParamSpecs = ParseTupleConstructorParameters(typeToGenerate, type);
+ constructionStrategy = ObjectConstructionStrategy.ParameterizedConstructor;
+ constructorSetsRequiredMembers = false;
+ propertyInitializerSpecs = null;
+ }
+ else
+ {
+ // ≤7 element tuples use the standard constructor discovery
+ ctorParamSpecs = ParseConstructorParameters(typeToGenerate, constructor, out constructionStrategy, out constructorSetsRequiredMembers);
+ propertyInitializerSpecs = ParsePropertyInitializers(ctorParamSpecs, propertySpecs, constructorSetsRequiredMembers, ref constructionStrategy);
+ }
+ }
+ else
+ {
+ ctorParamSpecs = ParseConstructorParameters(typeToGenerate, constructor, out constructionStrategy, out constructorSetsRequiredMembers);
+ propertySpecs = ParsePropertyGenerationSpecs(contextType, typeToGenerate, typeIgnoreCondition, options, typeNamingPolicy, out hasExtensionDataProperty, out fastPathPropertyIndices);
+ propertyInitializerSpecs = ParsePropertyInitializers(ctorParamSpecs, propertySpecs, constructorSetsRequiredMembers, ref constructionStrategy);
+ }
}
var typeRef = new TypeRef(type);
@@ -736,6 +763,10 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener
NullableUnderlyingType = nullableUnderlyingType,
RuntimeTypeRef = runtimeTypeRef,
IsValueTuple = type.IsTupleType,
+ IsReferenceTuple = IsReferenceTupleType(type),
+ HasNestedTupleElements = (type.IsTupleType || IsReferenceTupleType(type)) &&
+ type is INamedTypeSymbol { Arity: 8 } nt2 &&
+ (nt2.TypeArguments[7].IsTupleType || IsReferenceTupleType(nt2.TypeArguments[7])),
HasExtensionDataPropertyType = hasExtensionDataProperty,
ConverterType = customConverterType,
ImplementsIJsonOnSerialized = implementsIJsonOnSerialized,
@@ -2168,6 +2199,207 @@ void AddTypeIfNotNull(ITypeSymbol? type)
}
}
+ ///
+ /// Parses flattened property specs for tuple types (ValueTuple and System.Tuple).
+ /// Instead of emitting Item1-Item7 + Rest, emits Item1-ItemN with flattened accessors.
+ ///
+ private List ParseTuplePropertyGenerationSpecs(
+ INamedTypeSymbol _,
+ in TypeToGenerate typeToGenerate,
+ SourceGenerationOptionsSpec? __,
+ out List? fastPathPropertyIndices)
+ {
+ ITypeSymbol type = typeToGenerate.Type;
+ var properties = new List();
+ var declaringTypeRef = new TypeRef(type);
+ fastPathPropertyIndices = new List();
+
+ List<(ITypeSymbol ElementType, string AccessorPath)> flattenedElements = new();
+
+ if (type.IsTupleType && type is INamedTypeSymbol valueTupleType)
+ {
+ // Roslyn provides flattened elements via TupleElements for ValueTuples
+ ImmutableArray tupleElements = valueTupleType.TupleElements;
+ for (int i = 0; i < tupleElements.Length; i++)
+ {
+ string accessorPath = GetTupleAccessorPathForIndex(i);
+ flattenedElements.Add((tupleElements[i].Type, accessorPath));
+ }
+ }
+ else if (IsReferenceTupleType(type))
+ {
+ // For System.Tuple, manually walk the Rest chain
+ FlattenReferenceTupleElements(type, "", flattenedElements);
+ }
+
+ for (int i = 0; i < flattenedElements.Count; i++)
+ {
+ (ITypeSymbol elementType, string accessorPath) = flattenedElements[i];
+ string itemName = $"Item{i + 1}";
+ string propertyNameFieldName = $"PropName_{itemName}";
+
+ TypeRef propertyTypeRef = EnqueueType(elementType, typeToGenerate.Mode);
+
+ var propertySpec = new PropertyGenerationSpec
+ {
+ NameSpecifiedInSourceCode = accessorPath,
+ MemberName = itemName,
+ IsProperty = !type.IsTupleType, // System.Tuple uses properties; ValueTuple uses fields
+ IsPublic = true,
+ IsVirtual = false,
+ JsonPropertyName = null,
+ EffectiveJsonPropertyName = itemName,
+ PropertyNameFieldName = propertyNameFieldName,
+ IsReadOnly = true,
+ IsRequired = false,
+ HasJsonRequiredAttribute = false,
+ IsInitOnlySetter = false,
+ CanUseGetter = true,
+ CanUseSetter = type.IsTupleType, // ValueTuples have mutable fields; reference Tuples are read-only
+ DefaultIgnoreCondition = null,
+ NumberHandling = null,
+ ObjectCreationHandling = null,
+ Order = 0,
+ HasJsonInclude = false,
+ IsTupleElement = true,
+ IsExtensionData = false,
+ PropertyType = propertyTypeRef,
+ DeclaringType = declaringTypeRef,
+ ConverterType = null,
+ IsGetterNonNullableAnnotation = false,
+ IsSetterNonNullableAnnotation = false,
+ };
+
+ properties.Add(propertySpec);
+ fastPathPropertyIndices.Add(i);
+ }
+
+ return properties;
+ }
+
+ ///
+ /// Gets the accessor path for a tuple element at the given 0-based index.
+ ///
+ private static string GetTupleAccessorPathForIndex(int index)
+ {
+ int depth = index / 7;
+ int position = (index % 7) + 1;
+ string prefix = string.Concat(Enumerable.Repeat("Rest.", depth));
+ return $"{prefix}Item{position}";
+ }
+
+ ///
+ /// Flattens System.Tuple elements by walking the Rest type argument chain.
+ ///
+ private void FlattenReferenceTupleElements(ITypeSymbol type, string prefix, List<(ITypeSymbol, string)> elements)
+ {
+ if (type is not INamedTypeSymbol { IsGenericType: true } namedType)
+ {
+ return;
+ }
+
+ ImmutableArray typeArgs = namedType.TypeArguments;
+ int maxDirect = namedType.Arity == 8 ? 7 : namedType.Arity;
+
+ for (int i = 0; i < maxDirect; i++)
+ {
+ elements.Add((typeArgs[i], $"{prefix}Item{i + 1}"));
+ }
+
+ // If 8-arity, the last type argument is the Rest tuple
+ if (namedType.Arity == 8)
+ {
+ if (IsReferenceTupleType(typeArgs[7]))
+ {
+ FlattenReferenceTupleElements(typeArgs[7], $"{prefix}Rest.", elements);
+ }
+ else
+ {
+ // TRest is not a Tuple type; add it as a direct element
+ elements.Add((typeArgs[7], $"{prefix}Rest"));
+ }
+ }
+ }
+
+ ///
+ /// Creates constructor parameters for tuple types with flattened element types.
+ ///
+ private ParameterGenerationSpec[] ParseTupleConstructorParameters(
+ in TypeToGenerate typeToGenerate,
+ ITypeSymbol type)
+ {
+ var flattenedTypes = new List();
+ if (type.IsTupleType && type is INamedTypeSymbol valueTupleType)
+ {
+ foreach (IFieldSymbol element in valueTupleType.TupleElements)
+ {
+ flattenedTypes.Add(element.Type);
+ }
+ }
+ else if (IsReferenceTupleType(type))
+ {
+ FlattenReferenceTupleTypes(type, flattenedTypes);
+ }
+
+ var parameters = new ParameterGenerationSpec[flattenedTypes.Count];
+ for (int i = 0; i < flattenedTypes.Count; i++)
+ {
+ TypeRef paramTypeRef = EnqueueType(flattenedTypes[i], typeToGenerate.Mode);
+ parameters[i] = new ParameterGenerationSpec
+ {
+ ParameterType = paramTypeRef,
+ Name = $"Item{i + 1}",
+ HasDefaultValue = false,
+ DefaultValue = null,
+ ParameterIndex = i,
+ ArgsIndex = i,
+ IsNullable = !flattenedTypes[i].IsValueType || flattenedTypes[i].IsNullableType(),
+ RefKind = RefKind.None,
+ };
+ }
+
+ return parameters;
+ }
+
+ private void FlattenReferenceTupleTypes(ITypeSymbol type, List types)
+ {
+ if (type is not INamedTypeSymbol { IsGenericType: true } namedType)
+ {
+ return;
+ }
+
+ ImmutableArray typeArgs = namedType.TypeArguments;
+ int maxDirect = namedType.Arity == 8 ? 7 : namedType.Arity;
+
+ for (int i = 0; i < maxDirect; i++)
+ {
+ types.Add(typeArgs[i]);
+ }
+
+ if (namedType.Arity == 8)
+ {
+ if (IsReferenceTupleType(typeArgs[7]))
+ {
+ FlattenReferenceTupleTypes(typeArgs[7], types);
+ }
+ else
+ {
+ // TRest is not a Tuple type; add it as a direct element
+ types.Add(typeArgs[7]);
+ }
+ }
+ }
+
+ private bool IsReferenceTupleType(ITypeSymbol type)
+ {
+ if (type is not INamedTypeSymbol { IsGenericType: true } namedType)
+ {
+ return false;
+ }
+
+ return _knownSymbols.IsReferenceTupleType(namedType);
+ }
+
private readonly struct TypeToGenerate
{
public required ITypeSymbol Type { get; init; }
diff --git a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs
index a003a9d74d48c2..6b5ef76453535b 100644
--- a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs
+++ b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs
@@ -152,6 +152,12 @@ public sealed record PropertyGenerationSpec
///
public required TypeRef? ConverterType { get; init; }
+ ///
+ /// Whether this property represents a flattened tuple element.
+ /// When true, the property is always included in serialization regardless of IncludeFields settings.
+ ///
+ public bool IsTupleElement { get; init; }
+
///
/// Determines if the specified property should be included in the fast-path method body.
///
@@ -169,6 +175,12 @@ public bool ShouldIncludePropertyForFastPath(ContextGenerationSpec contextSpec)
return false;
}
+ // Tuple elements are always included regardless of IncludeFields
+ if (IsTupleElement)
+ {
+ return true;
+ }
+
// Discard fields when JsonInclude or IncludeFields aren't enabled.
if (!IsProperty && !HasJsonInclude && contextSpec.GeneratedOptionsSpec?.IncludeFields != true)
{
diff --git a/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs
index 9b71bf16438b89..370ccb16d592f7 100644
--- a/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs
+++ b/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs
@@ -53,6 +53,15 @@ public sealed record TypeGenerationSpec
public required bool IsValueTuple { get; init; }
+ public required bool IsReferenceTuple { get; init; }
+
+ public bool IsTupleType => IsValueTuple || IsReferenceTuple;
+
+ ///
+ /// True when this tuple type has elements nested via Rest that were flattened into the property list.
+ ///
+ public required bool HasNestedTupleElements { get; init; }
+
public required JsonNumberHandling? NumberHandling { get; init; }
public required JsonUnmappedMemberHandling? UnmappedMemberHandling { get; init; }
public required JsonObjectCreationHandling? PreferredPropertyObjectCreationHandling { get; init; }
diff --git a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs
index f2e9945b49c477..221be216885ebb 100644
--- a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs
+++ b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs
@@ -164,5 +164,34 @@ public static MemberInfo GetGenericMemberDefinition(this MemberInfo member)
return member;
}
+
+ ///
+ /// Determines whether the specified type is a ValueTuple or System.Tuple generic type.
+ ///
+ public static bool IsTupleType(this Type type)
+ {
+ if (!type.IsGenericType)
+ {
+ return false;
+ }
+
+ Type genericDef = type.GetGenericTypeDefinition();
+ return genericDef == typeof(ValueTuple<>) ||
+ genericDef == typeof(ValueTuple<,>) ||
+ genericDef == typeof(ValueTuple<,,>) ||
+ genericDef == typeof(ValueTuple<,,,>) ||
+ genericDef == typeof(ValueTuple<,,,,>) ||
+ genericDef == typeof(ValueTuple<,,,,,>) ||
+ genericDef == typeof(ValueTuple<,,,,,,>) ||
+ genericDef == typeof(ValueTuple<,,,,,,,>) ||
+ genericDef == typeof(Tuple<>) ||
+ genericDef == typeof(Tuple<,>) ||
+ genericDef == typeof(Tuple<,,>) ||
+ genericDef == typeof(Tuple<,,,>) ||
+ genericDef == typeof(Tuple<,,,,>) ||
+ genericDef == typeof(Tuple<,,,,,>) ||
+ genericDef == typeof(Tuple<,,,,,,>) ||
+ genericDef == typeof(Tuple<,,,,,,,>);
+ }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs
index 183f99e9a9a935..4d1e61da780e38 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs
@@ -62,6 +62,8 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte
typeInfo.UnmappedMemberHandling = unmappedMemberHandling;
}
+ bool isTupleType = typeInfo.Kind == JsonTypeInfoKind.Object && IsTupleType(type);
+
typeInfo.PopulatePolymorphismMetadata();
typeInfo.MapInterfaceTypesToCallbacks();
@@ -77,10 +79,22 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte
{
// NB parameter metadata must be populated *before* property metadata
// so that properties can be linked to their associated parameters.
- PopulateParameterInfoValues(typeInfo, nullabilityCtx);
+ if (isTupleType && type.IsGenericType && type.GetGenericArguments().Length == 8)
+ {
+ PopulateFlattenedTupleParameterInfoValues(typeInfo);
+ }
+ else
+ {
+ PopulateParameterInfoValues(typeInfo, nullabilityCtx);
+ }
}
- PopulateProperties(typeInfo, nullabilityCtx);
+ PopulateProperties(typeInfo, nullabilityCtx, isTupleType);
+
+ if (isTupleType && type.IsGenericType && type.GetGenericArguments().Length == 8)
+ {
+ FlattenTupleProperties(typeInfo);
+ }
typeInfo.ConstructorAttributeProvider = typeInfo.Converter.ConstructorInfo;
}
@@ -88,12 +102,21 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte
// Plug in any converter configuration -- should be run last.
converter.ConfigureJsonTypeInfo(typeInfo, options);
converter.ConfigureJsonTypeInfoUsingReflection(typeInfo, options);
+
+ // For tuple types with >7 elements, override the constructor delegate to handle
+ // nested Rest construction from flattened parameters.
+ if (isTupleType && typeInfo.CreateObjectWithArgs is not null &&
+ type.IsGenericType && type.GetGenericArguments().Length == 8)
+ {
+ typeInfo.CreateObjectWithArgs = CreateFlattenedTupleConstructorDelegate(type);
+ }
+
return typeInfo;
}
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
- private static void PopulateProperties(JsonTypeInfo typeInfo, NullabilityInfoContext nullabilityCtx)
+ private static void PopulateProperties(JsonTypeInfo typeInfo, NullabilityInfoContext nullabilityCtx, bool isTupleType = false)
{
Debug.Assert(!typeInfo.IsReadOnly);
Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object);
@@ -131,6 +154,7 @@ private static void PopulateProperties(JsonTypeInfo typeInfo, NullabilityInfoCon
nullabilityCtx,
typeIgnoreCondition,
constructorHasSetsRequiredMembersAttribute,
+ isTupleType,
ref state);
}
@@ -156,6 +180,7 @@ private static void AddMembersDeclaredBySuperType(
NullabilityInfoContext nullabilityCtx,
JsonIgnoreCondition? typeIgnoreCondition,
bool constructorHasSetsRequiredMembersAttribute,
+ bool isTupleType,
ref JsonTypeInfo.PropertyHierarchyResolutionState state)
{
Debug.Assert(!typeInfo.IsReadOnly);
@@ -197,7 +222,7 @@ private static void AddMembersDeclaredBySuperType(
foreach (FieldInfo fieldInfo in currentType.GetFields(AllInstanceMembers))
{
bool hasJsonIncludeAttribute = fieldInfo.GetCustomAttribute(inherit: false) != null;
- if (hasJsonIncludeAttribute || (fieldInfo.IsPublic && typeInfo.Options.IncludeFields))
+ if (hasJsonIncludeAttribute || (fieldInfo.IsPublic && (typeInfo.Options.IncludeFields || isTupleType)))
{
AddMember(
typeInfo,
@@ -305,6 +330,216 @@ private static void AddMember(
return numberHandlingAttribute?.UnmappedMemberHandling;
}
+ internal static bool IsTupleType(Type type) => type.IsTupleType();
+
+ ///
+ /// Flattens tuple properties so that Item1-Item7 + Rest becomes Item1-ItemN with
+ /// custom getters/setters that navigate through the Rest chain.
+ ///
+ [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
+ [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
+ private static void FlattenTupleProperties(JsonTypeInfo typeInfo)
+ {
+ var flattenedElements = new List<(Type ElementType, MemberInfo[] MemberChain)>();
+ CollectTupleElements(typeInfo.Type, [], flattenedElements);
+
+ typeInfo.PropertyList.Clear();
+
+ JsonTypeInfo.PropertyHierarchyResolutionState state = new(typeInfo.Options);
+ MemberAccessor accessor = MemberAccessor;
+ bool isValueTuple = typeInfo.Type.FullName?.StartsWith("System.ValueTuple`", StringComparison.Ordinal) == true;
+
+ for (int i = 0; i < flattenedElements.Count; i++)
+ {
+ (Type elementType, MemberInfo[] memberChain) = flattenedElements[i];
+ string name = $"Item{i + 1}";
+
+ Func