diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 879b41e23c324a..9f86e2f7707814 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Text.Json.Serialization; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using SourceGenerators; @@ -729,8 +730,11 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin { ImmutableEquatableArray parameters = typeGenerationSpec.CtorParamGenSpecs; ImmutableEquatableArray propertyInitializers = typeGenerationSpec.PropertyInitializerSpecs; - int paramCount = parameters.Count + propertyInitializers.Count(propInit => !propInit.MatchesConstructorParameter); - Debug.Assert(paramCount > 0); + + // out parameters don't appear in metadata - they don't receive values from JSON. + int nonOutParamCount = parameters.Count(p => p.RefKind != (int)RefKind.Out); + int paramCount = nonOutParamCount + propertyInitializers.Count(propInit => !propInit.MatchesConstructorParameter); + Debug.Assert(paramCount > 0 || parameters.Any(p => p.RefKind == (int)RefKind.Out)); writer.WriteLine($"private static {JsonParameterInfoValuesTypeRef}[] {ctorParamMetadataInitMethodName}() => new {JsonParameterInfoValuesTypeRef}[]"); writer.WriteLine('{'); @@ -739,12 +743,19 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin int i = 0; foreach (ParameterGenerationSpec spec in parameters) { + // Skip out parameters - they don't receive values from JSON deserialization. + if (spec.RefKind == (int)RefKind.Out) + { + continue; + } + + Debug.Assert(spec.ArgsIndex >= 0); writer.WriteLine($$""" new() { Name = {{FormatStringLiteral(spec.Name)}}, ParameterType = typeof({{spec.ParameterType.FullyQualifiedName}}), - Position = {{spec.ParameterIndex}}, + Position = {{spec.ArgsIndex}}, HasDefaultValue = {{FormatBoolLiteral(spec.HasDefaultValue)}}, DefaultValue = {{(spec.HasDefaultValue ? CSharpSyntaxUtilities.FormatLiteral(spec.DefaultValue, spec.ParameterType) : "null")}}, IsNullable = {{FormatBoolLiteral(spec.IsNullable)}}, @@ -928,6 +939,9 @@ static void ThrowPropertyNullException(string propertyName) writer.WriteLine('}'); } + // RefKind.RefReadOnlyParameter was added in Roslyn 4.4 + private const int RefKindRefReadOnlyParameter = 4; + private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec typeGenerationSpec) { ImmutableEquatableArray parameters = typeGenerationSpec.CtorParamGenSpecs; @@ -935,14 +949,37 @@ private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec type const string ArgsVarName = "args"; - StringBuilder sb = new($"static {ArgsVarName} => new {typeGenerationSpec.TypeRef.FullyQualifiedName}("); + bool hasRefOrRefReadonlyParams = parameters.Any(p => p.RefKind == (int)RefKind.Ref || p.RefKind == RefKindRefReadOnlyParameter); + + StringBuilder sb; + + if (hasRefOrRefReadonlyParams) + { + // For ref/ref readonly parameters, we need a block lambda with temp variables + sb = new($"static {ArgsVarName} => {{ "); + + // Declare temp variables for ref and ref readonly parameters + foreach (ParameterGenerationSpec param in parameters) + { + if (param.RefKind == (int)RefKind.Ref || param.RefKind == RefKindRefReadOnlyParameter) + { + // Use ArgsIndex to access the args array (out params don't have entries in args) + sb.Append($"var __temp{param.ParameterIndex} = ({param.ParameterType.FullyQualifiedName}){ArgsVarName}[{param.ArgsIndex}]; "); + } + } + + sb.Append($"return new {typeGenerationSpec.TypeRef.FullyQualifiedName}("); + } + else + { + sb = new($"static {ArgsVarName} => new {typeGenerationSpec.TypeRef.FullyQualifiedName}("); + } if (parameters.Count > 0) { foreach (ParameterGenerationSpec param in parameters) { - int index = param.ParameterIndex; - sb.Append($"{GetParamUnboxing(param.ParameterType, index)}, "); + sb.Append($"{GetParamExpression(param, ArgsVarName)}, "); } sb.Length -= 2; // delete the last ", " token @@ -955,17 +992,31 @@ private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec type sb.Append("{ "); foreach (PropertyInitializerGenerationSpec property in propertyInitializers) { - sb.Append($"{property.Name} = {GetParamUnboxing(property.ParameterType, property.ParameterIndex)}, "); + sb.Append($"{property.Name} = ({property.ParameterType.FullyQualifiedName}){ArgsVarName}[{property.ParameterIndex}], "); } sb.Length -= 2; // delete the last ", " token sb.Append(" }"); } + if (hasRefOrRefReadonlyParams) + { + sb.Append("; }"); + } + return sb.ToString(); - static string GetParamUnboxing(TypeRef type, int index) - => $"({type.FullyQualifiedName}){ArgsVarName}[{index}]"; + static string GetParamExpression(ParameterGenerationSpec param, string argsVarName) + { + return param.RefKind switch + { + (int)RefKind.Ref => $"ref __temp{param.ParameterIndex}", + (int)RefKind.Out => $"out var __discard{param.ParameterIndex}", + RefKindRefReadOnlyParameter => $"in __temp{param.ParameterIndex}", + // Use ArgsIndex to access the args array (out params don't have entries in args) + _ => $"({param.ParameterType.FullyQualifiedName}){argsVarName}[{param.ArgsIndex}]", // None or In (in doesn't require keyword at call site) + }; + } } private static string? GetPrimitiveWriterMethod(TypeGenerationSpec type) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index b119734aea4943..e14790cd605bd2 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -1494,6 +1494,9 @@ private void ProcessMember( constructionStrategy = ObjectConstructionStrategy.ParameterizedConstructor; constructorParameters = new ParameterGenerationSpec[paramCount]; + // Compute ArgsIndex for each parameter. + // out parameters don't have entries in the args array. + int argsIndex = 0; for (int i = 0; i < paramCount; i++) { IParameterSymbol parameterInfo = constructor.Parameters[i]; @@ -1507,6 +1510,9 @@ private void ProcessMember( TypeRef parameterTypeRef = EnqueueType(parameterInfo.Type, typeToGenerate.Mode); + // out parameters don't receive values from JSON, so they have ArgsIndex = -1. + int currentArgsIndex = parameterInfo.RefKind == RefKind.Out ? -1 : argsIndex++; + constructorParameters[i] = new ParameterGenerationSpec { ParameterType = parameterTypeRef, @@ -1514,7 +1520,9 @@ private void ProcessMember( HasDefaultValue = parameterInfo.HasExplicitDefaultValue, DefaultValue = parameterInfo.HasExplicitDefaultValue ? parameterInfo.ExplicitDefaultValue : null, ParameterIndex = i, + ArgsIndex = currentArgsIndex, IsNullable = parameterInfo.IsNullable(), + RefKind = (int)parameterInfo.RefKind, }; } } @@ -1535,7 +1543,9 @@ private void ProcessMember( HashSet? memberInitializerNames = null; List? propertyInitializers = null; - int paramCount = constructorParameters?.Length ?? 0; + + // Count non-out constructor parameters - out params don't have entries in the args array. + int paramCount = constructorParameters?.Count(p => p.RefKind != (int)RefKind.Out) ?? 0; // Determine potential init-only or required properties that need to be part of the constructor delegate signature. foreach (PropertyGenerationSpec property in properties) @@ -1575,7 +1585,8 @@ private void ProcessMember( Name = property.NameSpecifiedInSourceCode, ParameterType = property.PropertyType, MatchesConstructorParameter = matchingConstructorParameter is not null, - ParameterIndex = matchingConstructorParameter?.ParameterIndex ?? paramCount++, + // Use ArgsIndex for matching ctor params (excludes out params), or paramCount++ for new ones + ParameterIndex = matchingConstructorParameter?.ArgsIndex ?? paramCount++, IsNullable = property.PropertyType.CanBeNull && !property.IsSetterNonNullableAnnotation, }; @@ -1587,7 +1598,9 @@ private void ProcessMember( return paramGenSpecs?.FirstOrDefault(MatchesConstructorParameter); bool MatchesConstructorParameter(ParameterGenerationSpec paramSpec) - => propSpec.MemberName.Equals(paramSpec.Name, StringComparison.OrdinalIgnoreCase); + // Don't match out parameters - they don't receive values from JSON. + => paramSpec.RefKind != (int)RefKind.Out && + propSpec.MemberName.Equals(paramSpec.Name, StringComparison.OrdinalIgnoreCase); } } } diff --git a/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs index 2dea67e2159ccb..59835587124055 100644 --- a/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs @@ -31,7 +31,24 @@ public sealed record ParameterGenerationSpec // The default value of a constructor parameter can only be a constant // so it always satisfies the structural equality requirement for the record. public required object? DefaultValue { get; init; } + + /// + /// The zero-based position of the parameter in the constructor's formal parameter list. + /// public required int ParameterIndex { get; init; } + + /// + /// The zero-based index into the args array for this parameter. + /// For out parameters, this is -1 since they don't receive values from the args array. + /// + public required int ArgsIndex { get; init; } + public required bool IsNullable { get; init; } + + /// + /// The ref kind of the parameter: None (0), Ref (1), Out (2), In (3), RefReadOnlyParameter (4). + /// Using int instead of Microsoft.CodeAnalysis.RefKind to avoid dependency issues. + /// + public required int RefKind { get; init; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverterFactory.cs index b82733f5917920..3fd974fdc5516a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverterFactory.cs @@ -62,7 +62,14 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer foreach (ParameterInfo parameter in parameters) { // Every argument must be of supported type. - JsonTypeInfo.ValidateType(parameter.ParameterType); + // For byref parameters (in/ref/out), validate the underlying element type. + Type parameterType = parameter.ParameterType; + if (parameterType.IsByRef) + { + parameterType = parameterType.GetElementType()!; + } + + JsonTypeInfo.ValidateType(parameterType); } if (parameterCount <= JsonConstants.UnboxedParameterCountThreshold) @@ -75,7 +82,15 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer { if (i < parameterCount) { - typeArguments[i + 1] = parameters[i].ParameterType; + // For byref parameters (in/ref/out), use the underlying element type + // since byref types cannot be used as generic type arguments. + Type parameterType = parameters[i].ParameterType; + if (parameterType.IsByRef) + { + parameterType = parameterType.GetElementType()!; + } + + typeArguments[i + 1] = parameterType; } else { 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 d7aef7399c0b49..7130e4bdc829c3 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 @@ -287,13 +287,30 @@ private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, Nullabili { Debug.Assert(typeInfo.Converter.ConstructorInfo != null); ParameterInfo[] parameters = typeInfo.Converter.ConstructorInfo.GetParameters(); - int parameterCount = parameters.Length; - JsonParameterInfoValues[] jsonParameters = new JsonParameterInfoValues[parameterCount]; - for (int i = 0; i < parameterCount; i++) + // Count non-out parameters - out parameters don't receive values from JSON. + int nonOutParameterCount = 0; + foreach (ParameterInfo param in parameters) + { + if (!param.IsOut) + { + nonOutParameterCount++; + } + } + + JsonParameterInfoValues[] jsonParameters = new JsonParameterInfoValues[nonOutParameterCount]; + + int jsonParamIndex = 0; + for (int i = 0; i < parameters.Length; i++) { ParameterInfo reflectionInfo = parameters[i]; + // Skip out parameters - they don't receive values from JSON deserialization. + if (reflectionInfo.IsOut) + { + continue; + } + // Trimmed parameter names are reported as null in CoreCLR or "" in Mono. if (string.IsNullOrEmpty(reflectionInfo.Name)) { @@ -301,17 +318,25 @@ private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, Nullabili ThrowHelper.ThrowNotSupportedException_ConstructorContainsNullParameterNames(typeInfo.Converter.ConstructorInfo.DeclaringType); } + // For byref parameters (in/ref), use the underlying element type. + Type parameterType = reflectionInfo.ParameterType; + if (parameterType.IsByRef) + { + parameterType = parameterType.GetElementType()!; + } + JsonParameterInfoValues jsonInfo = new() { Name = reflectionInfo.Name, - ParameterType = reflectionInfo.ParameterType, - Position = reflectionInfo.Position, + ParameterType = parameterType, + Position = jsonParamIndex, // Use the position in the args array, not the constructor parameter index HasDefaultValue = reflectionInfo.HasDefaultValue, DefaultValue = reflectionInfo.GetDefaultValue(), IsNullable = DetermineParameterNullability(reflectionInfo, nullabilityCtx) is not NullabilityState.NotNull, }; - jsonParameters[i] = jsonInfo; + jsonParameters[jsonParamIndex] = jsonInfo; + jsonParamIndex++; } typeInfo.PopulateParameterInfoValues(jsonParameters); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 5456030b9bcaea..400817503b9bdc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -1145,11 +1145,6 @@ internal void ConfigureProperties() internal void PopulateParameterInfoValues(JsonParameterInfoValues[] parameterInfoValues) { - if (parameterInfoValues.Length == 0) - { - return; - } - Dictionary parameterIndex = new(parameterInfoValues.Length); foreach (JsonParameterInfoValues parameterInfoValue in parameterInfoValues) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs index 05f453ef7181a5..b04e1fdcafb8ec 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs @@ -91,14 +91,80 @@ private static DynamicMethod CreateParameterizedConstructor(ConstructorInfo cons ILGenerator generator = dynamicMethod.GetILGenerator(); + // For byref parameters, we need to store values in local variables and pass addresses. + // For out parameters, we just need a default-initialized local to pass by address. + LocalBuilder?[] locals = new LocalBuilder?[parameterCount]; + + // Track the mapping from constructor parameter index to args[] index. + // out parameters don't have entries in args[]. + int argsIndex = 0; + int[] argsIndices = new int[parameterCount]; + for (int i = 0; i < parameterCount; i++) + { + if (parameters[i].IsOut) + { + argsIndices[i] = -1; // out parameters don't have an args entry + } + else + { + argsIndices[i] = argsIndex++; + } + } + + for (int i = 0; i < parameterCount; i++) + { + Type paramType = parameters[i].ParameterType; + if (paramType.IsByRef) + { + // Declare a local for the underlying type. + Type elementType = paramType.GetElementType()!; + locals[i] = generator.DeclareLocal(elementType); + + if (parameters[i].IsOut) + { + // For out parameters, just initialize the local to default. + // We don't load from args[] since out params aren't in the metadata. + if (elementType.IsValueType) + { + generator.Emit(OpCodes.Ldloca, locals[i]!); + generator.Emit(OpCodes.Initobj, elementType); + } + else + { + generator.Emit(OpCodes.Ldnull); + generator.Emit(OpCodes.Stloc, locals[i]!); + } + } + else + { + // Load value from object array, unbox it, and store in the local. + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Ldc_I4, argsIndices[i]); + generator.Emit(OpCodes.Ldelem_Ref); + generator.Emit(OpCodes.Unbox_Any, elementType); + generator.Emit(OpCodes.Stloc, locals[i]!); + } + } + } + + // Now push all arguments onto the stack. for (int i = 0; i < parameterCount; i++) { Type paramType = parameters[i].ParameterType; - generator.Emit(OpCodes.Ldarg_0); - generator.Emit(OpCodes.Ldc_I4, i); - generator.Emit(OpCodes.Ldelem_Ref); - generator.Emit(OpCodes.Unbox_Any, paramType); + if (paramType.IsByRef) + { + // Load address of the local variable. + generator.Emit(OpCodes.Ldloca, locals[i]!); + } + else + { + // Load value from object array and unbox. + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Ldc_I4, argsIndices[i]); + generator.Emit(OpCodes.Ldelem_Ref); + generator.Emit(OpCodes.Unbox_Any, paramType); + } } generator.Emit(OpCodes.Newobj, constructor); @@ -136,15 +202,25 @@ public override JsonTypeInfo.ParameterizedConstructorDelegate OpCodes.Ldarg_0, - 1 => OpCodes.Ldarg_1, - 2 => OpCodes.Ldarg_2, - 3 => OpCodes.Ldarg_3, - _ => throw new InvalidOperationException() - }); + // For byref parameters (in/ref/out), load the address of the argument instead of the value. + bool isByRef = parameters[index].ParameterType.IsByRef; + + if (isByRef) + { + generator.Emit(OpCodes.Ldarga_S, index); + } + else + { + generator.Emit( + index switch + { + 0 => OpCodes.Ldarg_0, + 1 => OpCodes.Ldarg_1, + 2 => OpCodes.Ldarg_2, + 3 => OpCodes.Ldarg_3, + _ => throw new InvalidOperationException() + }); + } } generator.Emit(OpCodes.Newobj, constructor); diff --git a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs index 6fd5c9735f41ad..3febdaaa59886e 100644 --- a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs +++ b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs @@ -1841,5 +1841,335 @@ public class ClassWithRequiredProperty { public required string? Bar { get; set; } } + + [Fact] + public async Task DeserializeType_WithInParameters() + { + string json = @"{""DateTime"":""2020-12-15T00:00:00"",""TimeSpan"":""01:02:03""}"; + TypeWith_InParameters result = await Serializer.DeserializeWrapper(json); + Assert.Equal(new DateTime(2020, 12, 15), result.DateTime); + Assert.Equal(new TimeSpan(1, 2, 3), result.TimeSpan); + } + + public class TypeWith_InParameters + { + public TypeWith_InParameters(in DateTime dateTime, in TimeSpan timeSpan) + { + DateTime = dateTime; + TimeSpan = timeSpan; + } + + public DateTime DateTime { get; set; } + public TimeSpan TimeSpan { get; set; } + } + + [Fact] + public async Task DeserializeType_WithMixedByRefParameters() + { + string json = @"{""Value1"":42,""Value2"":""hello"",""Value3"":3.14,""Value4"":true}"; + TypeWith_MixedByRefParameters result = await Serializer.DeserializeWrapper(json); + Assert.Equal(42, result.Value1); + Assert.Equal("hello", result.Value2); + Assert.Equal(3.14, result.Value3); + Assert.True(result.Value4); + } + + public class TypeWith_MixedByRefParameters + { + public TypeWith_MixedByRefParameters(in int value1, string value2, in double value3, bool value4) + { + Value1 = value1; + Value2 = value2; + Value3 = value3; + Value4 = value4; + } + + public int Value1 { get; set; } + public string Value2 { get; set; } + public double Value3 { get; set; } + public bool Value4 { get; set; } + } + + [Fact] + public async Task DeserializeType_WithLargeInParameters() + { + string json = @"{""A"":1,""B"":2,""C"":3,""D"":4,""E"":5}"; + TypeWith_LargeInParameters result = await Serializer.DeserializeWrapper(json); + Assert.Equal(1, result.A); + Assert.Equal(2, result.B); + Assert.Equal(3, result.C); + Assert.Equal(4, result.D); + Assert.Equal(5, result.E); + } + + public class TypeWith_LargeInParameters + { + public TypeWith_LargeInParameters(in int a, in int b, in int c, in int d, in int e) + { + A = a; + B = b; + C = c; + D = d; + E = e; + } + + public int A { get; set; } + public int B { get; set; } + public int C { get; set; } + public int D { get; set; } + public int E { get; set; } + } + + [Fact] + public async Task DeserializeType_WithRefParameters() + { + string json = @"{""Value1"":42,""Value2"":""hello""}"; + TypeWith_RefParameters result = await Serializer.DeserializeWrapper(json); + Assert.Equal(42, result.Value1); + Assert.Equal("hello", result.Value2); + } + + public class TypeWith_RefParameters + { + public TypeWith_RefParameters(ref int value1, ref string value2) + { + Value1 = value1; + Value2 = value2; + } + + public int Value1 { get; set; } + public string Value2 { get; set; } + } + + [Fact] + public async Task DeserializeType_WithOutParameters() + { + string json = @"{""Value1"":42,""Value2"":""hello""}"; + TypeWith_OutParameters result = await Serializer.DeserializeWrapper(json); + // out parameters are excluded from the constructor delegate's metadata, + // so JSON values are set via property setters after construction. + // The constructor's assigned values (99, "default") are overwritten by the JSON values. + Assert.Equal(42, result.Value1); + Assert.Equal("hello", result.Value2); + } + + public class TypeWith_OutParameters + { + public TypeWith_OutParameters(out int value1, out string value2) + { + // Out parameters must be assigned by the constructor. + // Since they're excluded from metadata, these values won't come from JSON. + value1 = 99; + value2 = "default"; + Value1 = value1; + Value2 = value2; + } + + public int Value1 { get; set; } + public string Value2 { get; set; } + } + + // Comprehensive tests for all byref parameter modifiers with different types + // Modifiers: in, ref, out, ref readonly + // Types: primitives (int), structs (DateTime), reference types (string) + + #region In parameter tests with all types + + [Fact] + public async Task DeserializeType_WithInParameter_Primitive() + { + string json = @"{""Value"":42}"; + TypeWith_InParameter_Primitive result = await Serializer.DeserializeWrapper(json); + Assert.Equal(42, result.Value); + } + + public class TypeWith_InParameter_Primitive + { + public TypeWith_InParameter_Primitive(in int value) => Value = value; + public int Value { get; set; } + } + + [Fact] + public async Task DeserializeType_WithInParameter_Struct() + { + string json = @"{""Value"":""2020-12-15T00:00:00""}"; + TypeWith_InParameter_Struct result = await Serializer.DeserializeWrapper(json); + Assert.Equal(new DateTime(2020, 12, 15), result.Value); + } + + public class TypeWith_InParameter_Struct + { + public TypeWith_InParameter_Struct(in DateTime value) => Value = value; + public DateTime Value { get; set; } + } + + [Fact] + public async Task DeserializeType_WithInParameter_ReferenceType() + { + string json = @"{""Value"":""hello""}"; + TypeWith_InParameter_ReferenceType result = await Serializer.DeserializeWrapper(json); + Assert.Equal("hello", result.Value); + } + + public class TypeWith_InParameter_ReferenceType + { + public TypeWith_InParameter_ReferenceType(in string value) => Value = value; + public string Value { get; set; } + } + + #endregion + + #region Ref parameter tests with all types + + [Fact] + public async Task DeserializeType_WithRefParameter_Primitive() + { + string json = @"{""Value"":42}"; + TypeWith_RefParameter_Primitive result = await Serializer.DeserializeWrapper(json); + Assert.Equal(42, result.Value); + } + + public class TypeWith_RefParameter_Primitive + { + public TypeWith_RefParameter_Primitive(ref int value) => Value = value; + public int Value { get; set; } + } + + [Fact] + public async Task DeserializeType_WithRefParameter_Struct() + { + string json = @"{""Value"":""2020-12-15T00:00:00""}"; + TypeWith_RefParameter_Struct result = await Serializer.DeserializeWrapper(json); + Assert.Equal(new DateTime(2020, 12, 15), result.Value); + } + + public class TypeWith_RefParameter_Struct + { + public TypeWith_RefParameter_Struct(ref DateTime value) => Value = value; + public DateTime Value { get; set; } + } + + [Fact] + public async Task DeserializeType_WithRefParameter_ReferenceType() + { + string json = @"{""Value"":""hello""}"; + TypeWith_RefParameter_ReferenceType result = await Serializer.DeserializeWrapper(json); + Assert.Equal("hello", result.Value); + } + + public class TypeWith_RefParameter_ReferenceType + { + public TypeWith_RefParameter_ReferenceType(ref string value) => Value = value; + public string Value { get; set; } + } + + #endregion + + #region Out parameter tests with all types + + [Fact] + public async Task DeserializeType_WithOutParameter_Primitive() + { + string json = @"{""Value"":42}"; + TypeWith_OutParameter_Primitive result = await Serializer.DeserializeWrapper(json); + // Out parameters are excluded from metadata, so JSON values are set via property setters + Assert.Equal(42, result.Value); + } + + public class TypeWith_OutParameter_Primitive + { + public TypeWith_OutParameter_Primitive(out int value) + { + value = 99; + Value = value; + } + public int Value { get; set; } + } + + [Fact] + public async Task DeserializeType_WithOutParameter_Struct() + { + string json = @"{""Value"":""2020-12-15T00:00:00""}"; + TypeWith_OutParameter_Struct result = await Serializer.DeserializeWrapper(json); + // Out parameters are excluded from metadata, so JSON values are set via property setters + Assert.Equal(new DateTime(2020, 12, 15), result.Value); + } + + public class TypeWith_OutParameter_Struct + { + public TypeWith_OutParameter_Struct(out DateTime value) + { + value = new DateTime(1999, 1, 1); + Value = value; + } + public DateTime Value { get; set; } + } + + [Fact] + public async Task DeserializeType_WithOutParameter_ReferenceType() + { + string json = @"{""Value"":""hello""}"; + TypeWith_OutParameter_ReferenceType result = await Serializer.DeserializeWrapper(json); + // Out parameters are excluded from metadata, so JSON values are set via property setters + Assert.Equal("hello", result.Value); + } + + public class TypeWith_OutParameter_ReferenceType + { + public TypeWith_OutParameter_ReferenceType(out string value) + { + value = "default"; + Value = value; + } + public string Value { get; set; } + } + + #endregion + + #region Ref readonly parameter tests with all types + + [Fact] + public async Task DeserializeType_WithRefReadonlyParameter_Primitive() + { + string json = @"{""Value"":42}"; + TypeWith_RefReadonlyParameter_Primitive result = await Serializer.DeserializeWrapper(json); + Assert.Equal(42, result.Value); + } + + public class TypeWith_RefReadonlyParameter_Primitive + { + public TypeWith_RefReadonlyParameter_Primitive(ref readonly int value) => Value = value; + public int Value { get; set; } + } + + [Fact] + public async Task DeserializeType_WithRefReadonlyParameter_Struct() + { + string json = @"{""Value"":""2020-12-15T00:00:00""}"; + TypeWith_RefReadonlyParameter_Struct result = await Serializer.DeserializeWrapper(json); + Assert.Equal(new DateTime(2020, 12, 15), result.Value); + } + + public class TypeWith_RefReadonlyParameter_Struct + { + public TypeWith_RefReadonlyParameter_Struct(ref readonly DateTime value) => Value = value; + public DateTime Value { get; set; } + } + + [Fact] + public async Task DeserializeType_WithRefReadonlyParameter_ReferenceType() + { + string json = @"{""Value"":""hello""}"; + TypeWith_RefReadonlyParameter_ReferenceType result = await Serializer.DeserializeWrapper(json); + Assert.Equal("hello", result.Value); + } + + public class TypeWith_RefReadonlyParameter_ReferenceType + { + public TypeWith_RefReadonlyParameter_ReferenceType(ref readonly string value) => Value = value; + public string Value { get; set; } + } + + #endregion } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs index 0a4c1a21f19fde..2ce4ae4b69cff9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs @@ -159,6 +159,23 @@ protected ConstructorTests_Metadata(JsonSerializerWrapper stringWrapper) [JsonSerializable(typeof(Class_ExtraProperty_JsonElementDictionaryExtData))] [JsonSerializable(typeof(Class_ManyParameters_ExtraProperty_ExtData))] [JsonSerializable(typeof(ClassWithRequiredProperty))] + [JsonSerializable(typeof(TypeWith_InParameters))] + [JsonSerializable(typeof(TypeWith_MixedByRefParameters))] + [JsonSerializable(typeof(TypeWith_LargeInParameters))] + [JsonSerializable(typeof(TypeWith_RefParameters))] + [JsonSerializable(typeof(TypeWith_OutParameters))] + [JsonSerializable(typeof(TypeWith_InParameter_Primitive))] + [JsonSerializable(typeof(TypeWith_InParameter_Struct))] + [JsonSerializable(typeof(TypeWith_InParameter_ReferenceType))] + [JsonSerializable(typeof(TypeWith_RefParameter_Primitive))] + [JsonSerializable(typeof(TypeWith_RefParameter_Struct))] + [JsonSerializable(typeof(TypeWith_RefParameter_ReferenceType))] + [JsonSerializable(typeof(TypeWith_OutParameter_Primitive))] + [JsonSerializable(typeof(TypeWith_OutParameter_Struct))] + [JsonSerializable(typeof(TypeWith_OutParameter_ReferenceType))] + [JsonSerializable(typeof(TypeWith_RefReadonlyParameter_Primitive))] + [JsonSerializable(typeof(TypeWith_RefReadonlyParameter_Struct))] + [JsonSerializable(typeof(TypeWith_RefReadonlyParameter_ReferenceType))] internal sealed partial class ConstructorTestsContext_Metadata : JsonSerializerContext { } @@ -313,6 +330,23 @@ public ConstructorTests_Default(JsonSerializerWrapper jsonSerializer) : base(jso [JsonSerializable(typeof(Class_ExtraProperty_JsonElementDictionaryExtData))] [JsonSerializable(typeof(Class_ManyParameters_ExtraProperty_ExtData))] [JsonSerializable(typeof(ClassWithRequiredProperty))] + [JsonSerializable(typeof(TypeWith_InParameters))] + [JsonSerializable(typeof(TypeWith_MixedByRefParameters))] + [JsonSerializable(typeof(TypeWith_LargeInParameters))] + [JsonSerializable(typeof(TypeWith_RefParameters))] + [JsonSerializable(typeof(TypeWith_OutParameters))] + [JsonSerializable(typeof(TypeWith_InParameter_Primitive))] + [JsonSerializable(typeof(TypeWith_InParameter_Struct))] + [JsonSerializable(typeof(TypeWith_InParameter_ReferenceType))] + [JsonSerializable(typeof(TypeWith_RefParameter_Primitive))] + [JsonSerializable(typeof(TypeWith_RefParameter_Struct))] + [JsonSerializable(typeof(TypeWith_RefParameter_ReferenceType))] + [JsonSerializable(typeof(TypeWith_OutParameter_Primitive))] + [JsonSerializable(typeof(TypeWith_OutParameter_Struct))] + [JsonSerializable(typeof(TypeWith_OutParameter_ReferenceType))] + [JsonSerializable(typeof(TypeWith_RefReadonlyParameter_Primitive))] + [JsonSerializable(typeof(TypeWith_RefReadonlyParameter_Struct))] + [JsonSerializable(typeof(TypeWith_RefReadonlyParameter_ReferenceType))] internal sealed partial class ConstructorTestsContext_Default : JsonSerializerContext { }