diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 14f66ef020e1..0fff894391db 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -569,9 +569,9 @@ private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMe string getterValue = property switch { { DefaultIgnoreCondition: JsonIgnoreCondition.Always } => "null", - { CanUseGetter: true } => $"static (obj) => (({declaringTypeFQN})obj).{propertyName}", + { CanUseGetter: true } => $"static obj => (({declaringTypeFQN})obj).{propertyName}", { CanUseGetter: false, HasJsonInclude: true } - => $"""static (obj) => throw new {InvalidOperationExceptionTypeRef}("{string.Format(ExceptionMessages.InaccessibleJsonIncludePropertiesNotSupported, typeGenerationSpec.TypeRef.Name, propertyName)}")""", + => $"""static _ => throw new {InvalidOperationExceptionTypeRef}("{string.Format(ExceptionMessages.InaccessibleJsonIncludePropertiesNotSupported, typeGenerationSpec.TypeRef.Name, propertyName)}")""", _ => "null" }; @@ -838,7 +838,7 @@ private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec type const string ArgsVarName = "args"; - StringBuilder sb = new($"static ({ArgsVarName}) => new {typeGenerationSpec.TypeRef.FullyQualifiedName}("); + StringBuilder sb = new($"static {ArgsVarName} => new {typeGenerationSpec.TypeRef.FullyQualifiedName}("); if (parameters.Count > 0) { 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 75b81cf6f076..8edd9878660a 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 @@ -102,7 +102,7 @@ private static void PopulateProperties(JsonTypeInfo typeInfo) if (state.IsPropertyOrderSpecified) { - typeInfo.SortProperties(); + typeInfo.PropertyList.SortProperties(); } } @@ -210,7 +210,7 @@ private static void PopulateProperties(JsonTypeInfo typeInfo) } Debug.Assert(jsonPropertyInfo.Name != null); - typeInfo.AddProperty(jsonPropertyInfo, ref state); + typeInfo.PropertyList.AddPropertyWithConflictResolution(jsonPropertyInfo, ref state); } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] 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 381f3c407a59..74012964a2b5 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 @@ -14,7 +14,7 @@ public static partial class JsonMetadataServices /// private static JsonTypeInfo CreateCore(JsonConverter converter, JsonSerializerOptions options) { - JsonTypeInfo typeInfo = new JsonTypeInfo(converter, options); + var typeInfo = new JsonTypeInfo(converter, options); typeInfo.PopulatePolymorphismMetadata(); typeInfo.MapInterfaceTypesToCallbacks(); @@ -29,7 +29,7 @@ private static JsonTypeInfo CreateCore(JsonConverter converter, JsonSerial private static JsonTypeInfo CreateCore(JsonSerializerOptions options, JsonObjectInfoValues objectInfo) { JsonConverter converter = GetConverter(objectInfo); - JsonTypeInfo typeInfo = new JsonTypeInfo(converter, options); + var typeInfo = new JsonTypeInfo(converter, options); if (objectInfo.ObjectWithParameterizedConstructorCreator != null) { typeInfo.CreateObjectWithArgs = objectInfo.ObjectWithParameterizedConstructorCreator; @@ -41,7 +41,15 @@ private static JsonTypeInfo CreateCore(JsonSerializerOptions options, Json typeInfo.CreateObjectForExtensionDataProperty = ((JsonTypeInfo)typeInfo).CreateObject; } - PopulateProperties(typeInfo, objectInfo.PropertyMetadataInitializer); + if (objectInfo.PropertyMetadataInitializer != null) + { + typeInfo.SourceGenDelayedPropertyInitializer = objectInfo.PropertyMetadataInitializer; + } + else + { + typeInfo.PropertyMetadataSerializationNotSupported = true; + } + typeInfo.SerializeHandler = objectInfo.SerializeHandler; typeInfo.NumberHandling = objectInfo.NumberHandling; typeInfo.PopulatePolymorphismMetadata(); @@ -62,15 +70,16 @@ private static JsonTypeInfo CreateCore(JsonSerializerOptions options, Json object? createObjectWithArgs = null, object? addFunc = null) { + if (collectionInfo is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(collectionInfo)); + } + converter = collectionInfo.SerializeHandler != null ? new JsonMetadataServicesConverter(converter) : converter; JsonTypeInfo typeInfo = new JsonTypeInfo(converter, options); - if (collectionInfo is null) - { - ThrowHelper.ThrowArgumentNullException(nameof(collectionInfo)); - } typeInfo.KeyTypeInfo = collectionInfo.KeyInfo; typeInfo.ElementTypeInfo = collectionInfo.ElementInfo; @@ -116,28 +125,15 @@ private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, Func? propInitFunc) + internal static void PopulateProperties(JsonTypeInfo typeInfo, JsonTypeInfo.JsonPropertyInfoList propertyList, Func propInitFunc) { Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object); - Debug.Assert(!typeInfo.IsReadOnly); - - JsonSerializerContext? context = (typeInfo.OriginatingResolver ?? typeInfo.Options.TypeInfoResolver) as JsonSerializerContext; - if (propInitFunc?.Invoke(context!) is not JsonPropertyInfo[] properties) - { - if (typeInfo.Type == JsonTypeInfo.ObjectType) - { - return; - } + Debug.Assert(!typeInfo.IsConfigured); + Debug.Assert(typeInfo.Type != JsonTypeInfo.ObjectType); + Debug.Assert(typeInfo.Converter.ElementType is null); - if (typeInfo.Converter.ElementType != null) - { - // Nullable<> or F# optional converter strategy is set to element strategy - return; - } - - typeInfo.PropertyMetadataSerializationNotSupported = true; - return; - } + JsonSerializerContext? context = typeInfo.Options.TypeInfoResolver as JsonSerializerContext; + JsonPropertyInfo[] properties = propInitFunc(context!); // TODO update the source generator so that all property // hierarchy resolution is happening at compile time. @@ -161,7 +157,7 @@ private static void PopulateProperties(JsonTypeInfo typeInfo, Func entries are unique up to , /// however this will only be validated on serialization, once the metadata instance gets locked for further modification. /// - public IList Properties => _properties ??= new(this); - private JsonPropertyInfoList? _properties; + public IList Properties => PropertyList; - internal void SortProperties() + internal JsonPropertyInfoList PropertyList { - Debug.Assert(!IsConfigured); - Debug.Assert(_properties != null && _properties.Count > 0); + get + { + return _properties ?? CreatePropertyList(); + JsonPropertyInfoList CreatePropertyList() + { + var list = new JsonPropertyInfoList(this); + if (_sourceGenDelayedPropertyInitializer is { } propInit) + { + // .NET 6 source gen backward compatibility -- ensure that the + // property initializer delegate is invoked lazily. + JsonMetadataServices.PopulateProperties(this, list, propInit); + } - _properties.SortProperties(); - PropertyCache?.List.StableSortByKey(static propInfo => propInfo.Value.Order); + JsonPropertyInfoList? result = Interlocked.CompareExchange(ref _properties, list, null); + _sourceGenDelayedPropertyInitializer = null; + return result ?? list; + } + } } + /// + /// Stores the .NET 6-style property initialization delegate for delayed evaluation. + /// + internal Func? SourceGenDelayedPropertyInitializer + { + get => _sourceGenDelayedPropertyInitializer; + set + { + Debug.Assert(!IsReadOnly); + Debug.Assert(_properties is null, "must not be set if a property list has been initialized."); + _sourceGenDelayedPropertyInitializer = value; + } + } + + private Func? _sourceGenDelayedPropertyInitializer; + private JsonPropertyInfoList? _properties; + /// /// Gets or sets a configuration object specifying polymorphism metadata. /// @@ -956,76 +985,6 @@ public JsonPropertyInfo CreateJsonPropertyInfo(Type propertyType, string name) internal abstract ValueTask DeserializeAsObjectAsync(Stream utf8Json, CancellationToken cancellationToken); internal abstract object? DeserializeAsObject(Stream utf8Json); - /// - /// Used by the built-in resolvers to add property metadata applying conflict resolution. - /// - internal void AddProperty(JsonPropertyInfo jsonPropertyInfo, ref PropertyHierarchyResolutionState state) - { - Debug.Assert(jsonPropertyInfo.MemberName != null, "MemberName can be null in custom JsonPropertyInfo instances and should never be passed in this method"); - string memberName = jsonPropertyInfo.MemberName; - - JsonPropertyInfoList properties = _properties ??= new(this); - - if (state.AddedProperties.TryAdd(jsonPropertyInfo.Name, (jsonPropertyInfo, properties.Count))) - { - properties.Add(jsonPropertyInfo); - state.IsPropertyOrderSpecified |= jsonPropertyInfo.Order != 0; - } - else - { - // The JsonPropertyNameAttribute or naming policy resulted in a collision. - (JsonPropertyInfo other, int index) = state.AddedProperties[jsonPropertyInfo.Name]; - - if (other.IsIgnored) - { - // Overwrite previously cached property since it has [JsonIgnore]. - state.AddedProperties[jsonPropertyInfo.Name] = (jsonPropertyInfo, index); - properties[index] = jsonPropertyInfo; - state.IsPropertyOrderSpecified |= jsonPropertyInfo.Order != 0; - } - else - { - bool ignoreCurrentProperty; - - if (!Type.IsInterface) - { - ignoreCurrentProperty = - // Does the current property have `JsonIgnoreAttribute`? - jsonPropertyInfo.IsIgnored || - // Is the current property hidden by the previously cached property - // (with `new` keyword, or by overriding)? - other.MemberName == memberName || - // Was a property with the same CLR name ignored? That property hid the current property, - // thus, if it was ignored, the current property should be ignored too. - state.IgnoredProperties?.ContainsKey(memberName) == true; - } - else - { - // Unlike classes, interface hierarchies reject all naming conflicts for non-ignored properties. - // Conflicts like this are possible in two cases: - // 1. Diamond ambiguity in property names, or - // 2. Linear interface hierarchies that use properties with DIMs. - // - // Diamond ambiguities are not supported. Assuming there is demand, we might consider - // adding support for DIMs in the future, however that would require adding more APIs - // for the case of source gen. - - ignoreCurrentProperty = jsonPropertyInfo.IsIgnored; - } - - if (!ignoreCurrentProperty) - { - ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, jsonPropertyInfo.Name); - } - } - } - - if (jsonPropertyInfo.IsIgnored) - { - (state.IgnoredProperties ??= new()).Add(memberName, jsonPropertyInfo); - } - } - internal ref struct PropertyHierarchyResolutionState { public PropertyHierarchyResolutionState() { } @@ -1076,9 +1035,9 @@ internal void ConfigureProperties() Debug.Assert(PropertyCache is null); Debug.Assert(ExtensionDataProperty is null); - IList properties = (IList?)_properties ?? Array.Empty(); - + JsonPropertyInfoList properties = PropertyList; JsonPropertyDictionary propertyCache = CreatePropertyCache(capacity: properties.Count); + int numberOfRequiredProperties = 0; bool arePropertiesSorted = true; int previousPropertyOrder = int.MinValue; @@ -1123,15 +1082,16 @@ internal void ConfigureProperties() property.Configure(); } - NumberOfRequiredProperties = numberOfRequiredProperties; - PropertyCache = propertyCache; - if (!arePropertiesSorted) { // Properties have been configured by the user and require sorting. - SortProperties(); + properties.SortProperties(); + propertyCache.List.StableSortByKey(static propInfo => propInfo.Value.Order); } + NumberOfRequiredProperties = numberOfRequiredProperties; + PropertyCache = propertyCache; + // Override global UnmappedMemberHandling configuration // if type specifies an extension data property. EffectiveUnmappedMemberHandling = UnmappedMemberHandling ?? @@ -1206,6 +1166,7 @@ internal void ConfigureConstructorParameters() ParameterCount = jsonParameters.Length; ParameterCache = parameterCache; + ParameterInfoValues = null; } internal static void ValidateType(Type type) @@ -1346,7 +1307,7 @@ private static JsonTypeInfoKind GetTypeInfoKind(Type type, JsonConverter convert } } - private sealed class JsonPropertyInfoList : ConfigurationList + internal sealed class JsonPropertyInfoList : ConfigurationList { private readonly JsonTypeInfo _jsonTypeInfo; @@ -1355,10 +1316,13 @@ public JsonPropertyInfoList(JsonTypeInfo jsonTypeInfo) _jsonTypeInfo = jsonTypeInfo; } - public override bool IsReadOnly => _jsonTypeInfo.IsReadOnly || _jsonTypeInfo.Kind != JsonTypeInfoKind.Object; + public override bool IsReadOnly => _jsonTypeInfo._properties == this && _jsonTypeInfo.IsReadOnly || _jsonTypeInfo.Kind != JsonTypeInfoKind.Object; protected override void OnCollectionModifying() { - _jsonTypeInfo.VerifyMutable(); + if (_jsonTypeInfo._properties == this) + { + _jsonTypeInfo.VerifyMutable(); + } if (_jsonTypeInfo.Kind != JsonTypeInfoKind.Object) { @@ -1372,7 +1336,78 @@ protected override void ValidateAddedValue(JsonPropertyInfo item) } public void SortProperties() - => _list.StableSortByKey(static propInfo => propInfo.Order); + { + _list.StableSortByKey(static propInfo => propInfo.Order); + } + + /// + /// Used by the built-in resolvers to add property metadata applying conflict resolution. + /// + public void AddPropertyWithConflictResolution(JsonPropertyInfo jsonPropertyInfo, ref PropertyHierarchyResolutionState state) + { + Debug.Assert(!_jsonTypeInfo.IsConfigured); + Debug.Assert(jsonPropertyInfo.MemberName != null, "MemberName can be null in custom JsonPropertyInfo instances and should never be passed in this method"); + string memberName = jsonPropertyInfo.MemberName; + + if (state.AddedProperties.TryAdd(jsonPropertyInfo.Name, (jsonPropertyInfo, Count))) + { + Add(jsonPropertyInfo); + state.IsPropertyOrderSpecified |= jsonPropertyInfo.Order != 0; + } + else + { + // The JsonPropertyNameAttribute or naming policy resulted in a collision. + (JsonPropertyInfo other, int index) = state.AddedProperties[jsonPropertyInfo.Name]; + + if (other.IsIgnored) + { + // Overwrite previously cached property since it has [JsonIgnore]. + state.AddedProperties[jsonPropertyInfo.Name] = (jsonPropertyInfo, index); + this[index] = jsonPropertyInfo; + state.IsPropertyOrderSpecified |= jsonPropertyInfo.Order != 0; + } + else + { + bool ignoreCurrentProperty; + + if (!_jsonTypeInfo.Type.IsInterface) + { + ignoreCurrentProperty = + // Does the current property have `JsonIgnoreAttribute`? + jsonPropertyInfo.IsIgnored || + // Is the current property hidden by the previously cached property + // (with `new` keyword, or by overriding)? + other.MemberName == memberName || + // Was a property with the same CLR name ignored? That property hid the current property, + // thus, if it was ignored, the current property should be ignored too. + state.IgnoredProperties?.ContainsKey(memberName) == true; + } + else + { + // Unlike classes, interface hierarchies reject all naming conflicts for non-ignored properties. + // Conflicts like this are possible in two cases: + // 1. Diamond ambiguity in property names, or + // 2. Linear interface hierarchies that use properties with DIMs. + // + // Diamond ambiguities are not supported. Assuming there is demand, we might consider + // adding support for DIMs in the future, however that would require adding more APIs + // for the case of source gen. + + ignoreCurrentProperty = jsonPropertyInfo.IsIgnored; + } + + if (!ignoreCurrentProperty) + { + ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(_jsonTypeInfo.Type, jsonPropertyInfo.Name); + } + } + } + + if (jsonPropertyInfo.IsIgnored) + { + (state.IgnoredProperties ??= new()).Add(memberName, jsonPropertyInfo); + } + } } [DebuggerBrowsable(DebuggerBrowsableState.Never)] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.GetJsonTypeInfo.g.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.GetJsonTypeInfo.g.cs index 61eec78d7932..5d78355f68bb 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.GetJsonTypeInfo.g.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.GetJsonTypeInfo.g.cs @@ -27,6 +27,11 @@ public partial class Net60GeneratedContext return this.ClassWithCustomConverter; } + if (type == typeof(global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList)) + { + return this.MyLinkedList; + } + return null!; } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.ListDateTimeOffset.g.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.ListDateTimeOffset.g.cs index f969da8b4c88..576f4f054ca4 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.ListDateTimeOffset.g.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.ListDateTimeOffset.g.cs @@ -1,4 +1,11 @@ -// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Source files represent a source generated JsonSerializerContext as produced by the .NET 6 SDK. +// Used to validate correctness of contexts generated by previous SDKs against the current System.Text.Json runtime components. +// Unless absolutely necessary DO NOT MODIFY any of these files -- it would invalidate the purpose of the regression tests. + +// #nullable enable // Suppress warnings about [Obsolete] member usage in generated code. diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.MyLinkedList.g.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.MyLinkedList.g.cs new file mode 100644 index 000000000000..fecbc6b07355 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.MyLinkedList.g.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Source files represent a source generated JsonSerializerContext as produced by the .NET 6 SDK. +// Used to validate correctness of contexts generated by previous SDKs against the current System.Text.Json runtime components. +// Unless absolutely necessary DO NOT MODIFY any of these files -- it would invalidate the purpose of the regression tests. + +// +#nullable enable + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 + +namespace System.Text.Json.Tests.SourceGenRegressionTests.Net60 +{ + public partial class Net60GeneratedContext + { + private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo? _MyLinkedList; + public global::System.Text.Json.Serialization.Metadata.JsonTypeInfo MyLinkedList + { + get + { + if (_MyLinkedList == null) + { + global::System.Text.Json.Serialization.JsonConverter? customConverter; + if (Options.Converters.Count > 0 && (customConverter = GetRuntimeProvidedCustomConverter(typeof(global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList))) != null) + { + _MyLinkedList = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateValueInfo(Options, customConverter); + } + else + { + global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues objectInfo = new global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues() + { + ObjectCreator = null, + ObjectWithParameterizedConstructorCreator = static (args) => new global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList((global::System.Int32)args[0], (global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList)args[1]), + PropertyMetadataInitializer = MyLinkedListPropInit, + ConstructorParameterMetadataInitializer = MyLinkedListCtorParamInit, + NumberHandling = default, + SerializeHandler = MyLinkedListSerializeHandler + }; + + _MyLinkedList = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo(Options, objectInfo); + } + } + + return _MyLinkedList; + } + } + + private static global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] MyLinkedListPropInit(global::System.Text.Json.Serialization.JsonSerializerContext context) + { + global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.Net60GeneratedContext jsonContext = (global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.Net60GeneratedContext)context; + global::System.Text.Json.JsonSerializerOptions options = context.Options; + + global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] properties = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[2]; + + global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues info0 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues() + { + IsProperty = true, + IsPublic = true, + IsVirtual = false, + DeclaringType = typeof(global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList), + PropertyTypeInfo = jsonContext.Int32, + Converter = null, + Getter = static (obj) => ((global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList)obj).Value, + Setter = static (obj, value) => ((global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList)obj).Value = value!, + IgnoreCondition = null, + HasJsonInclude = false, + IsExtensionData = false, + NumberHandling = default, + PropertyName = "Value", + JsonPropertyName = null + }; + + properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info0); + + global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues() + { + IsProperty = true, + IsPublic = true, + IsVirtual = false, + DeclaringType = typeof(global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList), + PropertyTypeInfo = jsonContext.MyLinkedList, + Converter = null, + Getter = static (obj) => ((global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList)obj).Nested!, + Setter = static (obj, value) => ((global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList)obj).Nested = value!, + IgnoreCondition = null, + HasJsonInclude = false, + IsExtensionData = false, + NumberHandling = default, + PropertyName = "Nested", + JsonPropertyName = null + }; + + properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info1); + + return properties; + } + + private static void MyLinkedListSerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList? value) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartObject(); + writer.WriteNumber(PropName_Value, value.Value); + writer.WritePropertyName(PropName_Nested); + MyLinkedListSerializeHandler(writer, value.Nested!); + + writer.WriteEndObject(); + } + + private static global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[] MyLinkedListCtorParamInit() + { + global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[] parameters = new global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[2]; + global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues info; + + info = new() + { + Name = "value", + ParameterType = typeof(global::System.Int32), + Position = 0, + HasDefaultValue = false, + DefaultValue = default(global::System.Int32) + }; + parameters[0] = info; + + info = new() + { + Name = "nested", + ParameterType = typeof(global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList), + Position = 1, + HasDefaultValue = false, + DefaultValue = default(global::System.Text.Json.Tests.SourceGenRegressionTests.Net60.MyLinkedList) + }; + parameters[1] = info; + + return parameters; + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.PropertyNames.g.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.PropertyNames.g.cs index 84f4b425d6e7..21963c2a94c8 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.PropertyNames.g.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.PropertyNames.g.cs @@ -24,5 +24,7 @@ public partial class Net60GeneratedContext private static readonly global::System.Text.Json.JsonEncodedText PropName_SummaryWords = global::System.Text.Json.JsonEncodedText.Encode("SummaryWords"); private static readonly global::System.Text.Json.JsonEncodedText PropName_High = global::System.Text.Json.JsonEncodedText.Encode("High"); private static readonly global::System.Text.Json.JsonEncodedText PropName_Low = global::System.Text.Json.JsonEncodedText.Encode("Low"); + private static readonly global::System.Text.Json.JsonEncodedText PropName_Value = global::System.Text.Json.JsonEncodedText.Encode("Value"); + private static readonly global::System.Text.Json.JsonEncodedText PropName_Nested = global::System.Text.Json.JsonEncodedText.Encode("Nested"); } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.cs index 7158db219830..207878bdbfb1 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.cs @@ -12,6 +12,7 @@ namespace System.Text.Json.Tests.SourceGenRegressionTests.Net60 { //[JsonSerializable(typeof(WeatherForecastWithPOCOs))] //[JsonSerializable(typeof(ClassWithCustomConverter))] + //[JsonSerializable(typeof(MyLinkedList))] public partial class Net60GeneratedContext : JsonSerializerContext { } public class WeatherForecastWithPOCOs @@ -31,6 +32,18 @@ public class HighLowTemps public int Low { get; set; } } + public class MyLinkedList + { + public MyLinkedList(int value, MyLinkedList? nested) + { + Value = value; + Nested = nested; + } + + public int Value { get; set; } + public MyLinkedList? Nested { get; set; } + } + [JsonConverter(typeof(CustomConverter))] public class ClassWithCustomConverter { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.g.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.g.cs index 6b6bf15a8e54..caf7ac474467 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.g.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60/Net60GeneratedContext.g.cs @@ -14,7 +14,7 @@ namespace System.Text.Json.Tests.SourceGenRegressionTests.Net60 { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Text.Json.SourceGeneration", "6.0.6.21309")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Text.Json.SourceGeneration", "6.0.8.17311")] public partial class Net60GeneratedContext { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60RegressionTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60RegressionTests.cs index 21a87bbb113c..3dba6f8d0d37 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60RegressionTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net60RegressionTests.cs @@ -140,6 +140,26 @@ public static void HighLowTemps_ContextReportsCorrectMetadata() Assert.Equal(2, jsonPropertyInfo.Get(instance)); } + [Fact] + public static void SupportsRecursiveTypeSerialization() + { + JsonTypeInfo jsonTypeInfo = Net60GeneratedContext.Default.MyLinkedList; + + MyLinkedList linkedList = new( + value: 0, + nested: new( + value: 1, + nested: new( + value: 2, + nested: null))); + + string json = JsonSerializer.Serialize(linkedList, jsonTypeInfo); + Assert.Equal("""{"Value":0,"Nested":{"Value":1,"Nested":{"Value":2,"Nested":null}}}""", json); + + linkedList = JsonSerializer.Deserialize(json, jsonTypeInfo); + Assert.Equal(2, linkedList.Nested.Nested.Value); + } + [Fact] public static void CombinedContexts_ThrowsInvalidOperationException() { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.GetJsonTypeInfo.g.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.GetJsonTypeInfo.g.cs index 1d59adb8af1c..ccb1c8002ea0 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.GetJsonTypeInfo.g.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.GetJsonTypeInfo.g.cs @@ -65,6 +65,11 @@ public partial class Net70GeneratedContext: global::System.Text.Json.Serializati return this.ClassWithCustomConverter; } + if (type == typeof(global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList)) + { + return this.MyLinkedList; + } + return null!; } @@ -115,6 +120,11 @@ public partial class Net70GeneratedContext: global::System.Text.Json.Serializati return Create_ClassWithCustomConverter(options, makeReadOnly: false); } + if (type == typeof(global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList)) + { + return Create_MyLinkedList(options, makeReadOnly: false); + } + return null; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.MyLinkedList.g.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.MyLinkedList.g.cs new file mode 100644 index 000000000000..78c6e81f1cea --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.MyLinkedList.g.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Source files represent a source generated JsonSerializerContext as produced by the .NET 7 SDK. +// Used to validate correctness of contexts generated by previous SDKs against the current System.Text.Json runtime components. +// Unless absolutely necessary DO NOT MODIFY any of these files -- it would invalidate the purpose of the regression tests. + +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0618 + +namespace System.Text.Json.Tests.SourceGenRegressionTests.Net70 +{ + public partial class Net70GeneratedContext + { + private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo? _MyLinkedList; + /// + /// Defines the source generated JSON serialization contract metadata for a given type. + /// + public global::System.Text.Json.Serialization.Metadata.JsonTypeInfo MyLinkedList + { + get => _MyLinkedList ??= Create_MyLinkedList(Options, makeReadOnly: true); + } + + private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo Create_MyLinkedList(global::System.Text.Json.JsonSerializerOptions options, bool makeReadOnly) + { + global::System.Text.Json.Serialization.Metadata.JsonTypeInfo? jsonTypeInfo = null; + global::System.Text.Json.Serialization.JsonConverter? customConverter; + if (options.Converters.Count > 0 && (customConverter = GetRuntimeProvidedCustomConverter(options, typeof(global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList))) != null) + { + jsonTypeInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateValueInfo(options, customConverter); + } + else + { + global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues objectInfo = new global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues() + { + ObjectCreator = null, + ObjectWithParameterizedConstructorCreator = static (args) => new global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList((global::System.Int32)args[0], (global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList)args[1]), + PropertyMetadataInitializer = _ => MyLinkedListPropInit(options), + ConstructorParameterMetadataInitializer = MyLinkedListCtorParamInit, + NumberHandling = default, + SerializeHandler = MyLinkedListSerializeHandler + }; + + jsonTypeInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo(options, objectInfo); + } + + if (makeReadOnly) + { + jsonTypeInfo.MakeReadOnly(); + } + + return jsonTypeInfo; + } + + private static global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] MyLinkedListPropInit(global::System.Text.Json.JsonSerializerOptions options) + { + global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] properties = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[2]; + + global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues info0 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues() + { + IsProperty = true, + IsPublic = true, + IsVirtual = false, + DeclaringType = typeof(global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList), + Converter = null, + Getter = static (obj) => ((global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList)obj).Value, + Setter = static (obj, value) => ((global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList)obj).Value = value!, + IgnoreCondition = null, + HasJsonInclude = false, + IsExtensionData = false, + NumberHandling = default, + PropertyName = "Value", + JsonPropertyName = null + }; + + global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo propertyInfo0 = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info0); + properties[0] = propertyInfo0; + + global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues() + { + IsProperty = true, + IsPublic = true, + IsVirtual = false, + DeclaringType = typeof(global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList), + Converter = null, + Getter = static (obj) => ((global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList)obj).Nested!, + Setter = static (obj, value) => ((global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList)obj).Nested = value!, + IgnoreCondition = null, + HasJsonInclude = false, + IsExtensionData = false, + NumberHandling = default, + PropertyName = "Nested", + JsonPropertyName = null + }; + + global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo propertyInfo1 = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info1); + properties[1] = propertyInfo1; + + return properties; + } + + // Intentionally not a static method because we create a delegate to it. Invoking delegates to instance + // methods is almost as fast as virtual calls. Static methods need to go through a shuffle thunk. + private void MyLinkedListSerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList? value) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartObject(); + writer.WriteNumber(PropName_Value, value.Value); + writer.WritePropertyName(PropName_Nested); + MyLinkedListSerializeHandler(writer, value.Nested!); + + writer.WriteEndObject(); + } + + private static global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[] MyLinkedListCtorParamInit() + { + global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[] parameters = new global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[2]; + global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues info; + + info = new() + { + Name = "value", + ParameterType = typeof(global::System.Int32), + Position = 0, + HasDefaultValue = false, + DefaultValue = default(global::System.Int32) + }; + parameters[0] = info; + + info = new() + { + Name = "nested", + ParameterType = typeof(global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList), + Position = 1, + HasDefaultValue = false, + DefaultValue = default(global::System.Text.Json.Tests.SourceGenRegressionTests.Net70.MyLinkedList) + }; + parameters[1] = info; + + return parameters; + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.PropertyNames.g.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.PropertyNames.g.cs index d2edcb1433c7..7ad6a3ba0a52 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.PropertyNames.g.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.PropertyNames.g.cs @@ -26,5 +26,7 @@ public partial class Net70GeneratedContext private static readonly global::System.Text.Json.JsonEncodedText PropName_SummaryWords = global::System.Text.Json.JsonEncodedText.Encode("SummaryWords"); private static readonly global::System.Text.Json.JsonEncodedText PropName_High = global::System.Text.Json.JsonEncodedText.Encode("High"); private static readonly global::System.Text.Json.JsonEncodedText PropName_Low = global::System.Text.Json.JsonEncodedText.Encode("Low"); + private static readonly global::System.Text.Json.JsonEncodedText PropName_Value = global::System.Text.Json.JsonEncodedText.Encode("Value"); + private static readonly global::System.Text.Json.JsonEncodedText PropName_Nested = global::System.Text.Json.JsonEncodedText.Encode("Nested"); } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.cs index ebfbbd7c79f0..bcc9cf20751e 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.cs @@ -12,6 +12,7 @@ namespace System.Text.Json.Tests.SourceGenRegressionTests.Net70 { //[JsonSerializable(typeof(WeatherForecastWithPOCOs))] //[JsonSerializable(typeof(ClassWithCustomConverter))] + //[JsonSerializable(typeof(MyLinkedList))] public partial class Net70GeneratedContext : JsonSerializerContext { } public class WeatherForecastWithPOCOs @@ -31,6 +32,18 @@ public class HighLowTemps public int Low { get; set; } } + public class MyLinkedList + { + public MyLinkedList(int value, MyLinkedList? nested) + { + Value = value; + Nested = nested; + } + + public int Value { get; set; } + public MyLinkedList? Nested { get; set; } + } + [JsonConverter(typeof(CustomConverter))] public class ClassWithCustomConverter { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.g.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.g.cs index d6427a7c7848..daa9d4acb0f1 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.g.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70/Net70GeneratedContext.g.cs @@ -16,7 +16,7 @@ namespace System.Text.Json.Tests.SourceGenRegressionTests.Net70 { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Text.Json.SourceGeneration", "7.0.7.1805")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Text.Json.SourceGeneration", "7.0.8.17405")] public partial class Net70GeneratedContext { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70RegressionTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70RegressionTests.cs index 38c06bc04a21..af0bcfd73427 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70RegressionTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/SourceGenRegressionTests/Net70RegressionTests.cs @@ -140,6 +140,26 @@ public static void HighLowTemps_ContextReportsCorrectMetadata() Assert.Equal(2, jsonPropertyInfo.Get(instance)); } + [Fact] + public static void SupportsRecursiveTypeSerialization() + { + JsonTypeInfo jsonTypeInfo = Net70GeneratedContext.Default.MyLinkedList; + + MyLinkedList linkedList = new( + value: 0, + nested: new( + value: 1, + nested: new( + value: 2, + nested: null))); + + string json = JsonSerializer.Serialize(linkedList, jsonTypeInfo); + Assert.Equal("""{"Value":0,"Nested":{"Value":1,"Nested":{"Value":2,"Nested":null}}}""", json); + + linkedList = JsonSerializer.Deserialize(json, jsonTypeInfo); + Assert.Equal(2, linkedList.Nested.Nested.Value); + } + [Fact] public static void CombinedContexts_WorksAsExpected() { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index 8d0e4e67d833..d34d3d1208f8 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -227,6 +227,7 @@ + @@ -241,6 +242,7 @@ +