diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 8ec68abadc4318..99da1613e5f57e 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -25,7 +25,7 @@ private sealed partial class Emitter private const string InvalidOperationExceptionTypeRef = "global::System.InvalidOperationException"; private const string JsonExceptionTypeRef = "global::System.Text.Json.JsonException"; private const string TypeTypeRef = "global::System.Type"; - private const string UnsafeTypeRef = "global::System.Runtime.CompilerServices.Unsafe"; + private const string StrongBoxTypeRef = "global::System.Runtime.CompilerServices.StrongBox"; private const string EqualityComparerTypeRef = "global::System.Collections.Generic.EqualityComparer"; private const string KeyValuePairTypeRef = "global::System.Collections.Generic.KeyValuePair"; private const string UnsafeAccessorAttributeTypeRef = "global::System.Runtime.CompilerServices.UnsafeAccessorAttribute"; @@ -643,7 +643,7 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene if (propInitMethodName != null) { writer.WriteLine(); - GeneratePropMetadataInitFunc(writer, contextSpec, propInitMethodName, typeMetadata); + GeneratePropMetadataInitFunc(writer, propInitMethodName, typeMetadata); } if (serializeMethodName != null) @@ -826,7 +826,7 @@ private static string FormatNullCast(UnionCaseSpec caseSpec) : $"({fqn}?)"; } - private void GeneratePropMetadataInitFunc(SourceWriter writer, ContextGenerationSpec contextSpec, string propInitMethodName, TypeGenerationSpec typeGenerationSpec) + private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMethodName, TypeGenerationSpec typeGenerationSpec) { ImmutableEquatableArray properties = typeGenerationSpec.PropertyGenSpecs; HashSet duplicateMemberNames = GetDuplicateMemberNames(properties); @@ -855,8 +855,8 @@ property.DefaultIgnoreCondition is JsonIgnoreCondition.Always && string propertyTypeFQN = isIgnoredPropertyOfUnusedType ? "object" : property.PropertyType.FullyQualifiedName; - string getterValue = GetPropertyGetterValue(contextSpec, property, typeGenerationSpec, propertyName, declaringTypeFQN, i, duplicateMemberNames.Contains(property.MemberName)); - string setterValue = GetPropertySetterValue(contextSpec, property, typeGenerationSpec, propertyName, declaringTypeFQN, i, duplicateMemberNames.Contains(property.MemberName)); + string getterValue = GetPropertyGetterValue(property, typeGenerationSpec, propertyName, declaringTypeFQN, i, duplicateMemberNames.Contains(property.MemberName)); + string setterValue = GetPropertySetterValue(property, typeGenerationSpec, propertyName, declaringTypeFQN, i, duplicateMemberNames.Contains(property.MemberName)); string ignoreConditionNamedArg = property.DefaultIgnoreCondition.HasValue ? $"{JsonIgnoreConditionTypeRef}.{property.DefaultIgnoreCondition.Value}" @@ -969,14 +969,7 @@ private static bool NeedsAccessorForSetter(PropertyGenerationSpec property) return false; } - private static string GetUnboxExpression(ContextGenerationSpec contextSpec, string declaringTypeFQN) - { - string expression = $"{UnsafeTypeRef}.Unbox<{declaringTypeFQN}>(obj)"; - return contextSpec.UseUpdatedMemorySafetyRules ? $"unsafe({expression})" : expression; - } - private static string GetPropertyGetterValue( - ContextGenerationSpec contextSpec, PropertyGenerationSpec property, TypeGenerationSpec typeGenerationSpec, string propertyName, @@ -991,7 +984,11 @@ private static string GetPropertyGetterValue( if (property.CanUseGetter) { - return $"static obj => (({declaringTypeFQN})obj).{propertyName}"; + // For value types, the getter may receive a StrongBox during deserialization (e.g. for populated properties or callbacks) + // or a boxed T during serialization. + return typeGenerationSpec.TypeRef.IsValueType + ? $"static obj => (obj is {StrongBoxTypeRef}<{declaringTypeFQN}> box ? box.Value : ({declaringTypeFQN})obj).{propertyName}" + : $"static obj => (({declaringTypeFQN})obj).{propertyName}"; } if (NeedsAccessorForGetter(property)) @@ -1000,33 +997,39 @@ private static string GetPropertyGetterValue( if (property.CanUseUnsafeAccessors) { - // UnsafeAccessor externs for value types take 'ref T'. - string castExpr = typeGenerationSpec.TypeRef.IsValueType - ? $"ref {GetUnboxExpression(contextSpec, declaringTypeFQN)}" - : $"({declaringTypeFQN})obj"; - string accessorName = property.IsProperty ? GetQualifiedAccessorName(property, typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation) : GetQualifiedAccessorName(property, typeFriendlyName, "field", property.MemberName, propertyIndex, needsDisambiguation); - return $"static obj => {accessorName}({castExpr})"; + // Value types pass ref StrongBox.Value during deserialization or ref temp during serialization. + if (typeGenerationSpec.TypeRef.IsValueType) + { + return $"static obj => {{ if (obj is {StrongBoxTypeRef}<{declaringTypeFQN}> box) return {accessorName}(ref box.Value); var temp = ({declaringTypeFQN})obj; return {accessorName}(ref temp); }}"; + } + + return $"static obj => {accessorName}(({declaringTypeFQN})obj)"; } string getterName = GetAccessorName(typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation); if (!property.IsProperty) { - return $"static obj => {getterName}(obj)"; + // Value types can be passed as StrongBox during deserialization or boxed T during serialization. + return typeGenerationSpec.TypeRef.IsValueType + ? $"static obj => {getterName}(obj is {StrongBoxTypeRef}<{declaringTypeFQN}> box ? box.Value : obj)" + : $"static obj => {getterName}(obj)"; } // Reflection fallback property wrappers are strongly typed; cast in the delegate. - return $"static obj => {getterName}(({declaringTypeFQN})obj)"; + // Value types can be passed as StrongBox during deserialization or boxed T during serialization. + return typeGenerationSpec.TypeRef.IsValueType + ? $"static obj => {getterName}(obj is {StrongBoxTypeRef}<{declaringTypeFQN}> box ? box.Value : ({declaringTypeFQN})obj)" + : $"static obj => {getterName}(({declaringTypeFQN})obj)"; } return "null"; } private static string GetPropertySetterValue( - ContextGenerationSpec contextSpec, PropertyGenerationSpec property, TypeGenerationSpec typeGenerationSpec, string propertyName, @@ -1041,19 +1044,19 @@ private static string GetPropertySetterValue( if (property is { CanUseSetter: true, IsInitOnlySetter: true }) { - return GetAccessorBasedSetterDelegate(contextSpec, property, typeGenerationSpec, declaringTypeFQN, propertyIndex, needsDisambiguation); + return GetAccessorBasedSetterDelegate(property, typeGenerationSpec, declaringTypeFQN, propertyIndex, needsDisambiguation); } if (property.CanUseSetter) { return typeGenerationSpec.TypeRef.IsValueType - ? $"""static (obj, value) => {GetUnboxExpression(contextSpec, declaringTypeFQN)}.{propertyName} = value!""" + ? $"""static (obj, value) => (({StrongBoxTypeRef}<{declaringTypeFQN}>)obj).Value.{propertyName} = value!""" : $"""static (obj, value) => (({declaringTypeFQN})obj).{propertyName} = value!"""; } if (NeedsAccessorForSetter(property)) { - return GetAccessorBasedSetterDelegate(contextSpec, property, typeGenerationSpec, declaringTypeFQN, propertyIndex, needsDisambiguation); + return GetAccessorBasedSetterDelegate(property, typeGenerationSpec, declaringTypeFQN, propertyIndex, needsDisambiguation); } return "null"; @@ -1064,7 +1067,6 @@ private static string GetPropertySetterValue( /// or the strongly typed reflection wrapper. /// private static string GetAccessorBasedSetterDelegate( - ContextGenerationSpec contextSpec, PropertyGenerationSpec property, TypeGenerationSpec typeGenerationSpec, string declaringTypeFQN, @@ -1076,7 +1078,7 @@ private static string GetAccessorBasedSetterDelegate( if (property.CanUseUnsafeAccessors) { string castExpr = typeGenerationSpec.TypeRef.IsValueType - ? $"ref {GetUnboxExpression(contextSpec, declaringTypeFQN)}" + ? $"ref (({StrongBoxTypeRef}<{declaringTypeFQN}>)obj).Value" : $"({declaringTypeFQN})obj"; if (property.IsProperty) @@ -1097,7 +1099,7 @@ private static string GetAccessorBasedSetterDelegate( // Reflection fallback property wrappers are strongly typed; cast in the delegate like UnsafeAccessor. string setterCastExpr = typeGenerationSpec.TypeRef.IsValueType - ? $"ref {GetUnboxExpression(contextSpec, declaringTypeFQN)}" + ? $"ref (({StrongBoxTypeRef}<{declaringTypeFQN}>)obj).Value" : $"({declaringTypeFQN})obj"; return $"static (obj, value) => {setterName}({setterCastExpr}, value!)"; @@ -1239,13 +1241,48 @@ private static bool GeneratePropertyAccessors(SourceWriter writer, ContextGenera if (needsGetterAccessor) { string wrapperName = GetAccessorName(typeFriendlyName, "get", property.MemberName, i, disambiguate); - writer.WriteLine($"private static {propertyTypeFQN} {wrapperName}(object obj) => ({propertyTypeFQN})({fieldCacheName} ??= {fieldExpr}).GetValue(obj)!;"); + if (typeGenerationSpec.TypeRef.IsValueType) + { + // Value types can be passed as StrongBox during deserialization or boxed T during serialization. + writer.WriteLine($"private static {propertyTypeFQN} {wrapperName}(object obj) => ({propertyTypeFQN})({fieldCacheName} ??= {fieldExpr}).GetValue(obj is {StrongBoxTypeRef}<{declaringTypeFQN}> box ? box.Value : obj)!;"); + } + else + { + writer.WriteLine($"private static {propertyTypeFQN} {wrapperName}(object obj) => ({propertyTypeFQN})({fieldCacheName} ??= {fieldExpr}).GetValue(obj)!;"); + } } if (needsSetterAccessor) { string wrapperName = GetAccessorName(typeFriendlyName, "set", property.MemberName, i, disambiguate); - writer.WriteLine($"private static void {wrapperName}(object obj, {propertyTypeFQN} value) => ({fieldCacheName} ??= {fieldExpr}).SetValue(obj, value);"); + if (typeGenerationSpec.TypeRef.IsValueType) + { + writer.WriteLine($"private static void {wrapperName}(object obj, {propertyTypeFQN} value)"); + writer.WriteLine('{'); + writer.Indentation++; + // Value types are wrapped in StrongBox during deserialization so mutating box.Value persists. + // If called on an unboxed or directly boxed struct instance, set directly on obj. + writer.WriteLine($"if (obj is {StrongBoxTypeRef}<{declaringTypeFQN}> box)"); + writer.WriteLine('{'); + writer.Indentation++; + writer.WriteLine("object boxed = box.Value;"); + writer.WriteLine($"({fieldCacheName} ??= {fieldExpr}).SetValue(boxed, value);"); + writer.WriteLine($"box.Value = ({declaringTypeFQN})boxed;"); + writer.Indentation--; + writer.WriteLine('}'); + writer.WriteLine("else"); + writer.WriteLine('{'); + writer.Indentation++; + writer.WriteLine($"({fieldCacheName} ??= {fieldExpr}).SetValue(obj, value);"); + writer.Indentation--; + writer.WriteLine('}'); + writer.Indentation--; + writer.WriteLine('}'); + } + else + { + writer.WriteLine($"private static void {wrapperName}(object obj, {propertyTypeFQN} value) => ({fieldCacheName} ??= {fieldExpr}).SetValue(obj, value);"); + } } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index 939918ac59ff7a..bc78a703125dcc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -40,7 +40,9 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, if (state.ParentProperty?.TryGetPrePopulatedValue(ref state) == true) { - obj = state.Current.ReturnValue!; + obj = IsValueType && jsonTypeInfo.IsSourceGenerated && state.Current.ReturnValue is not StrongBox + ? new StrongBox((T)state.Current.ReturnValue!) + : state.Current.ReturnValue!; } else { @@ -49,12 +51,16 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(jsonTypeInfo, ref reader, ref state); } - obj = jsonTypeInfo.CreateObject(); + obj = jsonTypeInfo.CreateObject()!; + if (IsValueType && jsonTypeInfo.IsSourceGenerated && obj is not StrongBox) + { + obj = new StrongBox((T)obj); + } } PopulatePropertiesFastPath(obj, jsonTypeInfo, options, ref reader, ref state); Debug.Assert(obj is not null); - value = (T)obj; + value = (obj is StrongBox fastBox ? fastBox.Value : (T)obj)!; return true; } else @@ -116,7 +122,9 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, if (state.ParentProperty?.TryGetPrePopulatedValue(ref state) == true) { - obj = state.Current.ReturnValue!; + obj = IsValueType && jsonTypeInfo.IsSourceGenerated && state.Current.ReturnValue is not StrongBox + ? new StrongBox((T)state.Current.ReturnValue!) + : state.Current.ReturnValue!; } else { @@ -125,7 +133,11 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(jsonTypeInfo, ref reader, ref state); } - obj = jsonTypeInfo.CreateObject(); + obj = jsonTypeInfo.CreateObject()!; + if (IsValueType && jsonTypeInfo.IsSourceGenerated && obj is not StrongBox) + { + obj = new StrongBox((T)obj); + } } if ((state.Current.MetadataPropertyNames & MetadataPropertyName.Id) != 0) @@ -136,7 +148,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, state.ReferenceId = null; } - jsonTypeInfo.OnDeserializing?.Invoke(obj); + InvokeOnDeserializing(jsonTypeInfo, obj); state.Current.ReturnValue = obj; state.Current.ObjectState = StackFrameObjectState.CreatedObject; @@ -256,12 +268,12 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, } } - jsonTypeInfo.OnDeserialized?.Invoke(obj); + InvokeOnDeserialized(jsonTypeInfo, obj); state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo); // Unbox Debug.Assert(obj is not null); - value = (T)obj; + value = (obj is StrongBox slowBox ? slowBox.Value : (T)obj)!; // Check if we are trying to update the UTF-8 property cache. if (state.Current.PropertyRefCacheBuilder is not null) @@ -276,7 +288,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void PopulatePropertiesFastPath(object obj, JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options, ref Utf8JsonReader reader, scoped ref ReadStack state) { - jsonTypeInfo.OnDeserializing?.Invoke(obj); + InvokeOnDeserializing(jsonTypeInfo, obj); state.Current.InitializePropertiesValidationState(jsonTypeInfo); // Process all properties. @@ -309,7 +321,7 @@ internal static void PopulatePropertiesFastPath(object obj, JsonTypeInfo jsonTyp ReadPropertyValue(obj, ref state, ref reader, jsonPropertyInfo, useExtensionProperty); } - jsonTypeInfo.OnDeserialized?.Invoke(obj); + InvokeOnDeserialized(jsonTypeInfo, obj); state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo); // Check if we are trying to update the UTF-8 property cache. @@ -319,6 +331,40 @@ internal static void PopulatePropertiesFastPath(object obj, JsonTypeInfo jsonTyp } } + protected static void InvokeOnDeserializing(JsonTypeInfo jsonTypeInfo, object obj) + { + if (jsonTypeInfo.OnDeserializing is { } onDeserializing) + { + if (obj is StrongBox box) + { + object boxed = box.Value!; + onDeserializing(boxed); + box.Value = (T)boxed; + } + else + { + onDeserializing(obj); + } + } + } + + protected static void InvokeOnDeserialized(JsonTypeInfo jsonTypeInfo, object obj) + { + if (jsonTypeInfo.OnDeserialized is { } onDeserialized) + { + if (obj is StrongBox box) + { + object boxed = box.Value!; + onDeserialized(boxed); + box.Value = (T)boxed; + } + else + { + onDeserialized(obj); + } + } + } + internal sealed override bool OnTryWrite( Utf8JsonWriter writer, T value, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index fd53f8fcacc421..b9d0f8e92ab095 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -48,8 +48,13 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo if (state.ParentProperty?.TryGetPrePopulatedValue(ref state) == true) { object populatedObject = state.Current.ReturnValue!; + if (IsValueType && jsonTypeInfo.IsSourceGenerated && populatedObject is not StrongBox) + { + populatedObject = new StrongBox((T)populatedObject); + } + PopulatePropertiesFastPath(populatedObject, jsonTypeInfo, options, ref reader, ref state); - value = (T)populatedObject; + value = (populatedObject is StrongBox box ? box.Value : (T)populatedObject)!; return true; } @@ -63,9 +68,10 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo // before calling the constructor which may throw. state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo); - obj = (T)CreateObject(ref state.Current); + T createdObj = (T)CreateObject(ref state.Current); + obj = IsValueType && jsonTypeInfo.IsSourceGenerated ? new StrongBox(createdObj) : (object)createdObj; - jsonTypeInfo.OnDeserializing?.Invoke(obj); + InvokeOnDeserializing(jsonTypeInfo, obj); if (argumentState.FoundPropertyCount > 0) { @@ -165,8 +171,13 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo if (state.ParentProperty?.TryGetPrePopulatedValue(ref state) == true) { object populatedObject = state.Current.ReturnValue!; + if (IsValueType && jsonTypeInfo.IsSourceGenerated && populatedObject is not StrongBox) + { + populatedObject = new StrongBox((T)populatedObject); + state.Current.ReturnValue = populatedObject; + } - jsonTypeInfo.OnDeserializing?.Invoke(populatedObject); + InvokeOnDeserializing(jsonTypeInfo, populatedObject); state.Current.ObjectState = StackFrameObjectState.CreatedObject; state.Current.InitializePropertiesValidationState(jsonTypeInfo); return base.OnTryRead(ref reader, typeToConvert, options, ref state, out value); @@ -202,7 +213,8 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo // before calling the constructor which may throw. state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo); - obj = (T)CreateObject(ref state.Current); + T createdObj = (T)CreateObject(ref state.Current); + obj = IsValueType && jsonTypeInfo.IsSourceGenerated ? new StrongBox(createdObj) : (object)createdObj; if ((state.Current.MetadataPropertyNames & MetadataPropertyName.Id) != 0) { @@ -212,7 +224,7 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo state.ReferenceId = null; } - jsonTypeInfo.OnDeserializing?.Invoke(obj); + InvokeOnDeserializing(jsonTypeInfo, obj); if (argumentState.FoundPropertyCount > 0) { @@ -228,7 +240,7 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo if (propValue is not null || !jsonPropertyInfo.IgnoreNullTokensOnRead || default(T) is not null) { - jsonPropertyInfo.Set(obj, propValue); + jsonPropertyInfo.SetValueAsObject(obj, propValue); } } else @@ -271,11 +283,11 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo } } - jsonTypeInfo.OnDeserialized?.Invoke(obj); + InvokeOnDeserialized(jsonTypeInfo, obj); // Unbox Debug.Assert(obj is not null); - value = (T)obj; + value = (obj is StrongBox slowBox ? slowBox.Value : (T)obj)!; // Check if we are trying to update the UTF-8 property cache. if (state.Current.PropertyRefCacheBuilder is not null) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index e2b65021f144db..98d46e42aec3bc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -163,7 +163,7 @@ internal static void CreateExtensionDataProperty( extensionData = new Dictionary(); } Debug.Assert(jsonPropertyInfo.Set is not null); - jsonPropertyInfo.Set(obj, extensionData); + jsonPropertyInfo.SetValueAsObject(obj, extensionData); return; } else if (jsonPropertyInfo.PropertyType == typeof(IReadOnlyDictionary)) @@ -183,7 +183,7 @@ internal static void CreateExtensionDataProperty( extensionData = new Dictionary(); } Debug.Assert(jsonPropertyInfo.Set is not null); - jsonPropertyInfo.Set(obj, extensionData); + jsonPropertyInfo.SetValueAsObject(obj, extensionData); return; } else @@ -194,7 +194,7 @@ internal static void CreateExtensionDataProperty( extensionData = createObjectForExtensionDataProp(); Debug.Assert(jsonPropertyInfo.Set is not null); - jsonPropertyInfo.Set(obj, extensionData); + jsonPropertyInfo.SetValueAsObject(obj, extensionData); } // We don't add the value to the dictionary here because we need to support the read-ahead functionality for Streams. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs index d103aaa9f7a550..ac66371d0d85bd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text.Json.Serialization.Converters; namespace System.Text.Json.Serialization.Metadata @@ -15,6 +16,7 @@ public static partial class JsonMetadataServices private static JsonTypeInfo CreateCore(JsonConverter converter, JsonSerializerOptions options) { var typeInfo = new JsonTypeInfo(converter, options); + typeInfo.IsSourceGenerated = true; PopulatePolymorphismMetadata(typeInfo, polymorphismOptions: null, typeClassifierFactory: null); typeInfo.MapInterfaceTypesToCallbacks(); @@ -31,6 +33,7 @@ private static JsonTypeInfo CreateCore(JsonSerializerOptions options, Json { JsonConverter converter = GetConverter(objectInfo); var typeInfo = new JsonTypeInfo(converter, options); + typeInfo.IsSourceGenerated = true; if (objectInfo.ObjectWithParameterizedConstructorCreator is not null) { // NB parameter metadata must be populated *before* property metadata @@ -84,6 +87,7 @@ private static JsonTypeInfo CreateCore( : converter; JsonTypeInfo typeInfo = new JsonTypeInfo(converter, options); + typeInfo.IsSourceGenerated = true; typeInfo.KeyTypeInfo = collectionInfo.KeyInfo; typeInfo.ElementTypeInfo = collectionInfo.ElementInfo; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index 7d8752f81757a5..e134b36f47f1da 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -744,6 +744,14 @@ private bool NumberHandingIsApplicable() internal abstract object? GetValueAsObject(object obj); + // Used instead of the public untyped Set property by internal call sites (e.g. extension + // data population) that may hand 'obj' a StrongBox wrapper: it goes + // through the typed, StrongBox-compatibility-aware Set implementation instead of + // whatever raw delegate a caller assigned to the public Set property, since that public + // property must preserve the exact delegate instance it was given (see JsonPropertyInfo + // SetSetter for details). + internal abstract void SetValueAsObject(object obj, object? value); + internal bool HasGetter => _untypedGet is not null; internal bool HasSetter => _untypedSet is not null; internal bool IgnoreNullTokensOnRead { get; private protected set; } @@ -959,7 +967,12 @@ internal bool TryGetPrePopulatedValue(scoped ref ReadStack state) Debug.Assert(EffectiveConverter.CanPopulate, "Property is marked with Populate but converter cannot populate. This should have been validated in Configure"); Debug.Assert(state.Parent.ReturnValue is not null, "Parent object is null"); Debug.Assert(!state.Current.IsPopulating, "We've called TryGetPrePopulatedValue more than once"); - object? value = Get!(state.Parent.ReturnValue); + // Use GetValueAsObject rather than Get! directly: state.Parent.ReturnValue may be a + // StrongBox wrapper (source-generated struct types), and Get's public + // contract must preserve whatever raw delegate a caller assigned to it (see + // JsonPropertyInfo.SetGetter), so the StrongBox-compatibility handling lives behind + // GetValueAsObject instead. + object? value = GetValueAsObject(state.Parent.ReturnValue); state.Current.ReturnValue = value; state.Current.IsPopulating = value is not null; return value is not null; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index a9bc21f9c1338f..1977e1ebb15ad3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; namespace System.Text.Json.Serialization.Metadata { @@ -16,6 +17,19 @@ internal sealed class JsonPropertyInfo : JsonPropertyInfo private Func? _typedGet; private Action? _typedSet; + // Whether the compiled Get/Set delegates supplied for this property understand + // 'obj' being a StrongBox wrapper (used internally for source-generated + // struct types so their members can be mutated without Unsafe.Unbox). This starts out + // unknown ('false') and is determined lazily, the first time 'obj' is actually a + // StrongBox, by InvokeGetter/InvokeSetter below. Delegates compiled by a + // source generator version that predates StrongBox-based struct accessors (or a resolver + // modifier written against that historical "obj is a boxed TDeclaringType" contract) still + // expect a plain boxed TDeclaringType, so we detect that case, cache it, and bridge + // into/out of the box via a one-time unbox/rebox instead of permanently breaking those + // delegates. + private bool _getterRequiresLegacyUnbox; + private bool _setterRequiresLegacyUnbox; + internal JsonPropertyInfo(Type declaringType, JsonTypeInfo? declaringTypeInfo, JsonSerializerOptions options) : base(declaringType, propertyType: typeof(T), declaringTypeInfo, options) { @@ -42,18 +56,23 @@ private protected override void SetGetter(Delegate? getter) { _typedGet = null; _untypedGet = null; + return; } - else if (getter is Func typedGetter) - { - _typedGet = typedGetter; - _untypedGet = getter is Func untypedGet ? untypedGet : obj => typedGetter(obj); - } - else - { - Func untypedGet = (Func)getter; - _typedGet = (obj => (T)untypedGet(obj)!); - _untypedGet = untypedGet; - } + + Func rawGetter = getter is Func typedGetter + ? typedGetter + : (obj => (T)((Func)getter)(obj)!); + + // _untypedGet keeps the exact delegate instance the caller supplied so that the + // public JsonPropertyInfo.Get getter/setter pair (below in the base class) preserves + // reference identity, e.g. 'propertyInfo.Get = someDelegate; Assert.Same(someDelegate, + // propertyInfo.Get)'. Only _typedGet (used internally by GetValueAsObject and by + // serialization/deserialization) is routed through InvokeGetter for StrongBox + // compatibility; the untyped accessor is bridged separately through + // JsonPropertyInfo.GetValueAsObject wherever internal code needs the compatibility + // handling (e.g. TryGetPrePopulatedValue). + _typedGet = obj => InvokeGetter(rawGetter, obj); + _untypedGet = getter is Func untypedGetter ? untypedGetter : obj => rawGetter(obj); } private protected override void SetSetter(Delegate? setter) @@ -65,20 +84,89 @@ private protected override void SetSetter(Delegate? setter) { _typedSet = null; _untypedSet = null; + return; } - else if (setter is Action typedSetter) + + Action rawSetter = setter is Action typedSetter + ? typedSetter + : (obj, value) => ((Action)setter)(obj, value); + + // See the identity-preservation comment in SetGetter above: _untypedSet keeps the + // exact delegate instance the caller supplied whenever that shape is untyped, and + // internal StrongBox-compatibility handling is applied via _typedSet (used by + // GetValueAsObject/SetValueAsObject wherever internal code needs it) instead. + _typedSet = (obj, value) => InvokeSetter(rawSetter, obj, value); + _untypedSet = setter is Action untypedSetter ? untypedSetter : (obj, value) => rawSetter(obj, (T)value!); + } + + // See the comment on _getterRequiresLegacyUnbox for background. + private T InvokeGetter(Func rawGetter, object obj) + { + if (_getterRequiresLegacyUnbox) { - _typedSet = typedSetter; - _untypedSet = setter is Action untypedSet ? untypedSet : (obj, value) => typedSetter(obj, (T)value!); + return rawGetter(((IStrongBox)obj).Value!); } - else + + if (obj is not IStrongBox strongBox) + { + // The overwhelmingly common case: 'obj' is either a reference-type instance or a + // plain boxed value type (e.g. reflection-based accessors always pass this shape). + return rawGetter(obj); + } + + try + { + // The common source-generated struct case: the compiled getter was itself + // generated to understand StrongBox. + return rawGetter(obj); + } + catch (InvalidCastException) { - Action untypedSet = (Action)setter; - _typedSet = ((obj, value) => untypedSet(obj, value)); - _untypedSet = untypedSet; + // 'rawGetter' does not understand StrongBox and expects a plain + // boxed TDeclaringType instead. Remember this so future calls skip straight to the + // compatible path below. + _getterRequiresLegacyUnbox = true; + return rawGetter(strongBox.Value!); } } + // See the comment on _setterRequiresLegacyUnbox for background. + private void InvokeSetter(Action rawSetter, object obj, T value) + { + if (_setterRequiresLegacyUnbox) + { + SetViaLegacyUnbox(rawSetter, (IStrongBox)obj, value); + return; + } + + if (obj is not IStrongBox strongBox) + { + rawSetter(obj, value); + return; + } + + try + { + rawSetter(obj, value); + } + catch (InvalidCastException) + { + _setterRequiresLegacyUnbox = true; + SetViaLegacyUnbox(rawSetter, strongBox, value); + } + } + + private static void SetViaLegacyUnbox(Action rawSetter, IStrongBox strongBox, T value) + { + // 'rawSetter' can only mutate a genuine boxed TDeclaringType in place (that's the only + // reason it predates StrongBox support), so box/rebox once here to + // bridge into and out of the StrongBox that the runtime already + // maintains for tracking mutations to source-generated struct types. + object boxed = strongBox.Value!; + rawSetter(boxed, value); + strongBox.Value = boxed; + } + internal new Func? ShouldSerialize { get => _shouldSerializeTyped; @@ -168,6 +256,12 @@ private protected override void DetermineEffectiveConverter(JsonTypeInfo jsonTyp return Get!(obj); } + internal override void SetValueAsObject(object obj, object? value) + { + Debug.Assert(HasSetter); + Set!(obj, (T)value!); + } + internal override bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf8JsonWriter writer) { T value = Get!(obj); 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 2736ab06cb2d37..bd5731a7cb61af 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 @@ -35,6 +35,7 @@ public abstract partial class JsonTypeInfo /// internal BitArray? OptionalPropertiesMask { get; private set; } internal bool ShouldTrackRequiredProperties => OptionalPropertiesMask is not null; + internal bool IsSourceGenerated { get; set; } internal JsonTypeInfo(Type type, JsonConverter converter, JsonSerializerOptions options) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs index e338a51435f810..4a0b35aec62d4e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace System.Text.Json.Serialization.Metadata { diff --git a/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs b/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs index 923a905d6730bb..d52a99a452900b 100644 --- a/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs @@ -51,6 +51,18 @@ public void TypeWithConstructor_TypeInfoReportsExpectedCtorProvider([Dynamically Assert.Same(expectedCtor, typeInfo.ConstructorAttributeProvider); } + [Theory] + [InlineData(typeof(ClassWithDefaultCtor))] + [InlineData(typeof(StructWithDefaultCtor))] + public void TypeWithConstructor_CreateObject_ReturnsInstanceOfType(Type type) + { + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type); + Assert.NotNull(typeInfo.CreateObject); + object? instance = typeInfo.CreateObject(); + Assert.NotNull(instance); + Assert.IsType(type, instance); + } + [Theory] [InlineData(typeof(ClassWithDefaultCtor))] [InlineData(typeof(StructWithDefaultCtor))] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs index 188df5a166201b..17af4a1424d035 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -1227,5 +1228,160 @@ public static void ClassWithMultipleByteArrayProperties_Roundtrip(byte[]? data1, Assert.Equal(value.Data2, deserialized.Data2); Assert.Equal(value.Data3, deserialized.Data3); } + + public struct StructWithCallbacks : IJsonOnDeserializing, IJsonOnDeserialized, IJsonOnSerializing, IJsonOnSerialized + { + public int MyInt { get; set; } + public int InitialValue { get; set; } + public int OnDeserializingCount { get; set; } + public int OnDeserializedCount { get; set; } + public int OnSerializingCount { get; set; } + public int OnSerializedCount { get; set; } + + public void OnDeserializing() + { + OnDeserializingCount++; + } + + public void OnDeserialized() + { + Assert.Equal(1, OnDeserializingCount); + OnDeserializedCount++; + } + + public void OnSerializing() + { + OnSerializingCount++; + } + + public void OnSerialized() + { + Assert.Equal(1, OnSerializingCount); + OnSerializedCount++; + } + } + + public struct ParameterizedStructWithCallbacks : IJsonOnDeserializing, IJsonOnDeserialized + { + public int MyInt { get; set; } + public int Extra { get; set; } + public int OnDeserializingCount { get; set; } + public int OnDeserializedCount { get; set; } + + public ParameterizedStructWithCallbacks(int myInt) + { + MyInt = myInt; + } + + public void OnDeserializing() + { + OnDeserializingCount++; + } + + public void OnDeserialized() + { + Assert.Equal(1, OnDeserializingCount); + OnDeserializedCount++; + } + } + + [JsonSerializable(typeof(StructWithCallbacks))] + [JsonSerializable(typeof(ParameterizedStructWithCallbacks))] + internal partial class StructWithCallbacksContext : JsonSerializerContext + { + } + + [Fact] + public static void SourceGeneratedStruct_CreateObject_ReturnsBoxedStructInstance() + { + JsonTypeInfo typeInfo = StructWithCallbacksContext.Default.StructWithCallbacks; + Assert.NotNull(typeInfo.CreateObject); + object instance = typeInfo.CreateObject(); + Assert.NotNull(instance); + Assert.IsType(instance); + } + + [Fact] + [RequiresUnreferencedCode("Uses a resolver modifier that relies on reflection-adjacent APIs.")] + [RequiresDynamicCode("Uses a resolver modifier that relies on reflection-adjacent APIs.")] + public static void SourceGeneratedStruct_CustomizedCreateObject_DeserializesSuccessfully() + { + var options = new JsonSerializerOptions + { + TypeInfoResolver = StructWithCallbacksContext.Default.WithAddedModifier(ti => + { + if (ti.Type == typeof(StructWithCallbacks)) + { + ti.CreateObject = () => new StructWithCallbacks { InitialValue = 42 }; + } + }) + }; + + StructWithCallbacks result = JsonSerializer.Deserialize("""{"MyInt":1}""", options); + Assert.Equal(42, result.InitialValue); + Assert.Equal(1, result.MyInt); + } + + [Fact] + public static void SourceGeneratedStruct_Parameterless_Callbacks_PreserveMutations() + { + StructWithCallbacks result = JsonSerializer.Deserialize("""{"MyInt":10}""", StructWithCallbacksContext.Default.StructWithCallbacks); + Assert.Equal(10, result.MyInt); + Assert.Equal(1, result.OnDeserializingCount); + Assert.Equal(1, result.OnDeserializedCount); + } + + [Fact] + public static void SourceGeneratedStruct_Parameterized_Callbacks_PreserveMutations() + { + ParameterizedStructWithCallbacks result = JsonSerializer.Deserialize("""{"MyInt":10,"Extra":20}""", StructWithCallbacksContext.Default.ParameterizedStructWithCallbacks); + Assert.Equal(10, result.MyInt); + Assert.Equal(20, result.Extra); + Assert.Equal(1, result.OnDeserializingCount); + Assert.Equal(1, result.OnDeserializedCount); + } + + public struct StructWithLegacyAccessorContract + { + public int MyInt { get; set; } + } + + [JsonSerializable(typeof(StructWithLegacyAccessorContract))] + internal partial class StructWithLegacyAccessorContractContext : JsonSerializerContext + { + } + + [Fact] + [RequiresUnreferencedCode("Uses a resolver modifier that relies on reflection-adjacent APIs.")] + [RequiresDynamicCode("Uses a resolver modifier that relies on reflection-adjacent APIs.")] + public static void SourceGeneratedStruct_LegacyStylePropertyAccessors_StillWork() + { + // Simulates Get/Set delegates compiled against the pre-StrongBox contract used + // by source generator versions that predate this feature (or a resolver modifier + // written against that historical contract): 'obj' is assumed to always be a genuine + // boxed StructWithLegacyAccessorContract, so the setter needs Unsafe.Unbox to obtain a + // mutable reference into it, exactly like pre-StrongBox generated code did. The + // runtime must transparently detect and accommodate delegates like this so they keep + // working even though 'obj' is now a StrongBox for + // value-type source-generated properties. + var options = new JsonSerializerOptions + { + TypeInfoResolver = StructWithLegacyAccessorContractContext.Default.WithAddedModifier(ti => + { + if (ti.Type == typeof(StructWithLegacyAccessorContract)) + { + JsonPropertyInfo property = ti.Properties.Single(p => p.Name == "MyInt"); + property.Get = static obj => ((StructWithLegacyAccessorContract)obj).MyInt; + property.Set = static (obj, value) => unsafe(System.Runtime.CompilerServices.Unsafe.Unbox(obj)).MyInt = (int)value!; + } + }) + }; + + StructWithLegacyAccessorContract result = JsonSerializer.Deserialize("""{"MyInt":42}""", options); + Assert.Equal(42, result.MyInt); + + string json = JsonSerializer.Serialize(result, options); + JsonTestHelper.AssertJsonEqual("""{"MyInt":42}""", json); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/CompilationHelper.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/CompilationHelper.cs index a6158ffe5995e1..811600f95ae7f6 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/CompilationHelper.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/CompilationHelper.cs @@ -73,6 +73,7 @@ public static Compilation CreateCompilation( MetadataReference.CreateFromFile(typeof(LinkedList<>).Assembly.Location), MetadataReference.CreateFromFile(systemRuntimeAssembly.Location), #else + MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.StrongBox<>).Assembly.Location), MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.Unsafe).Assembly.Location), #endif };