diff --git a/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs b/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs index 8a1a58a97a2cb..09ecc45077fe0 100644 --- a/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs +++ b/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs @@ -310,5 +310,18 @@ public static bool TryGetDeserializationConstructor( deserializationCtor = ctorWithAttribute ?? publicParameterlessCtor ?? lonePublicCtor; return true; } + + public static object? GetDefaultValue(this ParameterInfo parameterInfo) + { + object? defaultValue = parameterInfo.DefaultValue; + + // DBNull.Value is sometimes used as the default value (returned by reflection) of nullable params in place of null. + if (defaultValue == DBNull.Value && parameterInfo.ParameterType != typeof(DBNull)) + { + return null; + } + + return defaultValue; + } } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index c17eb09a805e7..ec6124a8222be 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -804,14 +804,19 @@ private string GenerateCtorParamMetadataInitFunc(TypeGenerationSpec typeGenerati { ParameterInfo reflectionInfo = parameters[i].ParameterInfo; + string parameterTypeRef = reflectionInfo.ParameterType.GetCompilableName(); + + object? defaultValue = reflectionInfo.GetDefaultValue(); + string defaultValueAsStr = GetParamDefaultValueAsString(defaultValue, parameterTypeRef); + sb.Append(@$" {InfoVarName} = new() {{ Name = ""{reflectionInfo.Name!}"", - ParameterType = typeof({reflectionInfo.ParameterType.GetCompilableName()}), + ParameterType = typeof({parameterTypeRef}), Position = {reflectionInfo.Position}, HasDefaultValue = {ToCSharpKeyword(reflectionInfo.HasDefaultValue)}, - DefaultValue = {GetParamDefaultValueAsString(reflectionInfo.DefaultValue)} + DefaultValue = {defaultValueAsStr} }}; {parametersVarName}[{i}] = {InfoVarName}; "); @@ -1313,12 +1318,12 @@ private static string GetNumberHandlingAsStr(JsonNumberHandling? numberHandling) private static string ToCSharpKeyword(bool value) => value.ToString().ToLowerInvariant(); - private static string GetParamDefaultValueAsString(object? value) + private static string GetParamDefaultValueAsString(object? value, string objectTypeAsStr) { switch (value) { case null: - return "null"; + return $"default({objectTypeAsStr})"; case bool boolVal: return ToCSharpKeyword(boolVal); default: diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index bbdd17bddb8c2..2785e66c7cd3a 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -26,6 +26,7 @@ private sealed class Parser private const string SystemTextJsonNamespace = "System.Text.Json"; private const string JsonConverterAttributeFullName = "System.Text.Json.Serialization.JsonConverterAttribute"; private const string JsonConverterFactoryFullName = "System.Text.Json.Serialization.JsonConverterFactory"; + private const string JsonConverterOfTFullName = "System.Text.Json.Serialization.JsonConverter`1"; private const string JsonArrayFullName = "System.Text.Json.Nodes.JsonArray"; private const string JsonElementFullName = "System.Text.Json.JsonElement"; private const string JsonExtensionDataAttributeFullName = "System.Text.Json.Serialization.JsonExtensionDataAttribute"; @@ -101,6 +102,9 @@ private sealed class Parser private readonly Type? _dateOnlyType; private readonly Type? _timeOnlyType; + // Needed for converter validation + private readonly Type _jsonConverterOfTType; + private readonly HashSet _numberTypes = new(); private readonly HashSet _knownTypes = new(); private readonly HashSet _knownUnsupportedTypes = new(); @@ -215,6 +219,8 @@ public Parser(Compilation compilation, in JsonSourceGenerationContext sourceGene _dateOnlyType = _metadataLoadContext.Resolve(DateOnlyFullName); _timeOnlyType = _metadataLoadContext.Resolve(TimeOnlyFullName); + _jsonConverterOfTType = _metadataLoadContext.Resolve(JsonConverterOfTFullName); + PopulateKnownTypes(); } @@ -224,8 +230,12 @@ public Parser(Compilation compilation, in JsonSourceGenerationContext sourceGene INamedTypeSymbol jsonSerializerContextSymbol = compilation.GetBestTypeByMetadataName(JsonSerializerContextFullName); INamedTypeSymbol jsonSerializableAttributeSymbol = compilation.GetBestTypeByMetadataName(JsonSerializerAttributeFullName); INamedTypeSymbol jsonSourceGenerationOptionsAttributeSymbol = compilation.GetBestTypeByMetadataName(JsonSourceGenerationOptionsAttributeFullName); + INamedTypeSymbol jsonConverterOfTAttributeSymbol = compilation.GetBestTypeByMetadataName(JsonConverterOfTFullName); - if (jsonSerializerContextSymbol == null || jsonSerializableAttributeSymbol == null || jsonSourceGenerationOptionsAttributeSymbol == null) + if (jsonSerializerContextSymbol == null || + jsonSerializableAttributeSymbol == null || + jsonSourceGenerationOptionsAttributeSymbol == null || + jsonConverterOfTAttributeSymbol == null) { return null; } @@ -1354,7 +1364,14 @@ private static bool PropertyAccessorCanBeReferenced(MethodInfo? accessor) return null; } - if (converterType.GetCompatibleBaseClass(JsonConverterFactoryFullName) != null) + // Validated when creating the source generation spec. + Debug.Assert(_jsonConverterOfTType != null); + + if (converterType.GetCompatibleGenericBaseClass(_jsonConverterOfTType) != null) + { + return $"new {converterType.GetCompilableName()}()"; + } + else if (converterType.GetCompatibleBaseClass(JsonConverterFactoryFullName) != null) { hasFactoryConverter = true; @@ -1368,7 +1385,7 @@ private static bool PropertyAccessorCanBeReferenced(MethodInfo? accessor) } } - return $"new {converterType.GetCompilableName()}()"; + return null; } private static string DetermineRuntimePropName(string clrPropName, string? jsonPropName, JsonKnownNamingPolicy namingPolicy) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs index 3e25b10c13a23..f820ffbfe3a85 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs @@ -67,10 +67,7 @@ protected sealed override void InitializeConstructorArgumentCaches(ref ReadStack { JsonParameterInfo? parameterInfo = cache[i].Value; Debug.Assert(parameterInfo != null); - - arguments[parameterInfo.ClrInfo.Position] = parameterInfo.ShouldDeserialize - ? parameterInfo.DefaultValue - : parameterInfo.ClrDefaultValue; + arguments[parameterInfo.ClrInfo.Position] = parameterInfo.DefaultValue; } state.Current.CtorArgumentState!.Arguments = arguments; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 2d23e7c5c9200..f81025bbc460e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -600,9 +600,7 @@ internal JsonTypeInfo GetOrAddClass(Type type) { _haveTypesBeenCreated = true; - // todo: for performance and reduced instances, consider using the converters and JsonTypeInfo from s_defaultOptions by cloning (or reference directly if no changes). - // https://github.com/dotnet/runtime/issues/32357 - if (!_classes.TryGetValue(type, out JsonTypeInfo? result)) + if (!TryGetClass(type, out JsonTypeInfo? result)) { result = _classes.GetOrAdd(type, GetClassFromContextOrCreate(type)); } @@ -644,6 +642,20 @@ internal JsonTypeInfo GetOrAddClassForRootType(Type type) return jsonTypeInfo; } + internal bool TryGetClass(Type type, [NotNullWhen(true)] out JsonTypeInfo? jsonTypeInfo) + { + // todo: for performance and reduced instances, consider using the converters and JsonTypeInfo from s_defaultOptions by cloning (or reference directly if no changes). + // https://github.com/dotnet/runtime/issues/32357 + if (!_classes.TryGetValue(type, out JsonTypeInfo? result)) + { + jsonTypeInfo = null; + return false; + } + + jsonTypeInfo = result; + return true; + } + internal bool TypeIsCached(Type type) { return _classes.ContainsKey(type); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/GenericMethodHolder.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/GenericMethodHolder.cs index a144c11d398c9..2c143f95e59cc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/GenericMethodHolder.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/GenericMethodHolder.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Text.Json.Reflection; @@ -12,10 +13,24 @@ namespace System.Text.Json.Serialization.Metadata /// internal abstract class GenericMethodHolder { + /// + /// Returns the default value for the specified type. + /// + public abstract object? DefaultValue { get; } + /// /// Returns true if contains only default values. /// public abstract bool IsDefaultValue(object value); + + /// + /// Creates a holder instance representing a type. + /// + public static GenericMethodHolder CreateHolder(Type type) + { + Type holderType = typeof(GenericMethodHolder<>).MakeGenericType(type); + return (GenericMethodHolder)Activator.CreateInstance(holderType)!; + } } /// @@ -23,6 +38,8 @@ internal abstract class GenericMethodHolder /// internal sealed class GenericMethodHolder : GenericMethodHolder { + public override object? DefaultValue => default(T); + public override bool IsDefaultValue(object value) { // For performance, we only want to call this method for non-nullable value types. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs index 7b33f3dd97a88..3905c70402024 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs @@ -17,8 +17,6 @@ internal abstract class JsonParameterInfo private protected bool MatchingPropertyCanBeNull { get; private set; } - internal abstract object? ClrDefaultValue { get; } - // The default value of the parameter. This is `DefaultValue` of the `ParameterInfo`, if specified, or the CLR `default` for the `ParameterType`. public object? DefaultValue { get; private protected set; } @@ -74,18 +72,49 @@ public virtual void Initialize(JsonParameterInfoValues parameterInfo, JsonProper MatchingPropertyCanBeNull = matchingProperty.PropertyTypeCanBeNull; } - // Create a parameter that is ignored at run time. It uses the same type (typeof(sbyte)) to help - // prevent issues with unsupported types and helps ensure we don't accidently (de)serialize it. - public static JsonParameterInfo CreateIgnoredParameterPlaceholder(JsonParameterInfoValues parameterInfo, JsonPropertyInfo matchingProperty) + /// + /// Create a parameter that is ignored at run time. It uses the same type (typeof(sbyte)) to help + /// prevent issues with unsupported types and helps ensure we don't accidently (de)serialize it. + /// + public static JsonParameterInfo CreateIgnoredParameterPlaceholder( + JsonParameterInfoValues parameterInfo, + JsonPropertyInfo matchingProperty, + bool sourceGenMode) { - JsonParameterInfo jsonParameterInfo = matchingProperty.ConverterBase.CreateJsonParameterInfo(); + JsonParameterInfo jsonParameterInfo = new JsonParameterInfo(); jsonParameterInfo.ClrInfo = parameterInfo; jsonParameterInfo.RuntimePropertyType = matchingProperty.RuntimePropertyType!; jsonParameterInfo.NameAsUtf8Bytes = matchingProperty.NameAsUtf8Bytes!; - jsonParameterInfo.InitializeDefaultValue(matchingProperty); + + // TODO: https://github.com/dotnet/runtime/issues/60082. + // Default value initialization for params mapping to ignored properties doesn't + // account for the default value of optional parameters. This should be fixed. + + if (sourceGenMode) + { + // The value in the matching JsonPropertyInfo instance matches the parameter type. + jsonParameterInfo.DefaultValue = matchingProperty.DefaultValue; + } + else + { + // The value in the created JsonPropertyInfo instance (sbyte) + // doesn't match the parameter type, use reflection to get the default value. + Type parameterType = parameterInfo.ParameterType; + + GenericMethodHolder holder; + if (matchingProperty.Options.TryGetClass(parameterType, out JsonTypeInfo? typeInfo)) + { + holder = typeInfo.GenericMethods; + } + else + { + holder = GenericMethodHolder.CreateHolder(parameterInfo.ParameterType); + } + + jsonParameterInfo.DefaultValue = holder.DefaultValue; + } + return jsonParameterInfo; } - - protected abstract void InitializeDefaultValue(JsonPropertyInfo matchingProperty); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs index 2195426a551ed..b275f01acd095 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs @@ -11,8 +11,6 @@ namespace System.Text.Json.Serialization.Metadata /// internal sealed class JsonParameterInfo : JsonParameterInfo { - internal override object? ClrDefaultValue => default(T); - public T TypedDefaultValue { get; private set; } = default!; public override void Initialize(JsonParameterInfoValues parameterInfo, JsonPropertyInfo matchingProperty, JsonSerializerOptions options) @@ -21,7 +19,7 @@ public override void Initialize(JsonParameterInfoValues parameterInfo, JsonPrope InitializeDefaultValue(matchingProperty); } - protected override void InitializeDefaultValue(JsonPropertyInfo matchingProperty) + private void InitializeDefaultValue(JsonPropertyInfo matchingProperty) { Debug.Assert(ClrInfo.ParameterType == matchingProperty.DeclaredPropertyType); 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 3b8c003ac3066..5e2258ec6dde8 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 @@ -42,16 +42,14 @@ internal static JsonPropertyInfo GetPropertyPlaceholder() // Create a property that is ignored at run-time. internal static JsonPropertyInfo CreateIgnoredPropertyPlaceholder( - JsonConverter converter, MemberInfo memberInfo, Type memberType, bool isVirtual, JsonSerializerOptions options) { - JsonPropertyInfo jsonPropertyInfo = converter.CreateJsonPropertyInfo(); + JsonPropertyInfo jsonPropertyInfo = new JsonPropertyInfo(); jsonPropertyInfo.Options = options; - jsonPropertyInfo.ConverterBase = converter; jsonPropertyInfo.MemberInfo = memberInfo; jsonPropertyInfo.IsIgnored = true; jsonPropertyInfo.DeclaredPropertyType = memberType; @@ -525,5 +523,10 @@ internal JsonTypeInfo RuntimeTypeInfo internal string? ClrName { get; set; } internal bool IsVirtual { get; set; } + + /// + /// Default value used for parameterized ctor invocation. + /// + internal abstract object? DefaultValue { get; } } } 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 f60f0450701b9..e98f08dd54cdc 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 @@ -28,8 +28,11 @@ internal sealed class JsonPropertyInfo : JsonPropertyInfo private bool _propertyTypeEqualsTypeToConvert; internal Func? Get { get; set; } + internal Action? Set { get; set; } + internal override object? DefaultValue => default(T); + public JsonConverter Converter { get; internal set; } = null!; internal override void Initialize( diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs index d3f2c3c9bee6f..f696cefe8f21c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs @@ -58,6 +58,12 @@ internal static JsonPropertyInfo AddProperty( JsonNumberHandling? parentTypeNumberHandling, JsonSerializerOptions options) { + JsonIgnoreCondition? ignoreCondition = JsonPropertyInfo.GetAttribute(memberInfo)?.Condition; + if (ignoreCondition == JsonIgnoreCondition.Always) + { + return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(memberInfo, memberType, isVirtual, options); + } + JsonConverter converter = GetConverter( memberType, parentClassType, @@ -65,12 +71,6 @@ internal static JsonPropertyInfo AddProperty( out Type runtimeType, options); - JsonIgnoreCondition? ignoreCondition = JsonPropertyInfo.GetAttribute(memberInfo)?.Condition; - if (ignoreCondition == JsonIgnoreCondition.Always) - { - return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(converter, memberInfo, memberType, isVirtual, options); - } - return CreateProperty( declaredPropertyType: memberType, runtimePropertyType: runtimeType, @@ -646,7 +646,7 @@ internal void InitializeParameterCache() return; } - InitializeConstructorParameters(array); + InitializeConstructorParameters(array, sourceGenMode: true); Debug.Assert(ParameterCache != null); } } 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 4f662c67cd65c..4bd852e447e2b 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 @@ -133,12 +133,7 @@ internal GenericMethodHolder GenericMethods { get { - if (_genericMethods == null) - { - Type runtimePropertyClass = typeof(GenericMethodHolder<>).MakeGenericType(new Type[] { Type })!; - _genericMethods = (GenericMethodHolder)Activator.CreateInstance(runtimePropertyClass)!; - } - + _genericMethods ??= GenericMethodHolder.CreateHolder(Type); return _genericMethods; } } @@ -454,7 +449,7 @@ public ParameterLookupValue(JsonPropertyInfo jsonPropertyInfo) public JsonPropertyInfo JsonPropertyInfo { get; } } - private void InitializeConstructorParameters(JsonParameterInfoValues[] jsonParameters) + private void InitializeConstructorParameters(JsonParameterInfoValues[] jsonParameters, bool sourceGenMode = false) { var parameterCache = new JsonPropertyDictionary(Options.PropertyNameCaseInsensitive, jsonParameters.Length); @@ -500,7 +495,7 @@ private void InitializeConstructorParameters(JsonParameterInfoValues[] jsonParam Debug.Assert(matchingEntry.JsonPropertyInfo != null); JsonPropertyInfo jsonPropertyInfo = matchingEntry.JsonPropertyInfo; - JsonParameterInfo jsonParameterInfo = CreateConstructorParameter(parameterInfo, jsonPropertyInfo, Options); + JsonParameterInfo jsonParameterInfo = CreateConstructorParameter(parameterInfo, jsonPropertyInfo, sourceGenMode, Options); parameterCache.Add(jsonPropertyInfo.NameAsString, jsonParameterInfo); } // It is invalid for the extension data property to bind with a constructor argument. @@ -530,7 +525,7 @@ private static JsonParameterInfoValues[] GetParameterInfoArray(ParameterInfo[] p ParameterType = reflectionInfo.ParameterType, Position = reflectionInfo.Position, HasDefaultValue = reflectionInfo.HasDefaultValue, - DefaultValue = reflectionInfo.DefaultValue + DefaultValue = reflectionInfo.GetDefaultValue() }; jsonParameters[i] = jsonInfo; @@ -580,11 +575,12 @@ private bool IsValidDataExtensionProperty(JsonPropertyInfo jsonPropertyInfo) private static JsonParameterInfo CreateConstructorParameter( JsonParameterInfoValues parameterInfo, JsonPropertyInfo jsonPropertyInfo, + bool sourceGenMode, JsonSerializerOptions options) { if (jsonPropertyInfo.IsIgnored) { - return JsonParameterInfo.CreateIgnoredParameterPlaceholder(parameterInfo, jsonPropertyInfo); + return JsonParameterInfo.CreateIgnoredParameterPlaceholder(parameterInfo, jsonPropertyInfo, sourceGenMode); } JsonConverter converter = jsonPropertyInfo.ConverterBase; 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 c04815dc85e6a..93aff74c1b4d0 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 @@ -1216,5 +1216,111 @@ public class TypeWithUri public TypeWithUri(Uri myUri = default) => MyUri = myUri; } + + [Fact] + public async Task SmallObject_ClrDefaultParamValueUsed_WhenMatchingPropIgnored() + { + string json = @"{""Prop"":20}"; + var obj1 = await JsonSerializerWrapperForString.DeserializeWrapper(json); + Assert.Equal(0, obj1.Prop); + + var obj2 = await JsonSerializerWrapperForString.DeserializeWrapper(json); + Assert.Equal(0, obj2.Prop); + } + + public class SmallType_IgnoredProp_Bind_ParamWithDefaultValue + { + [JsonIgnore] + public int Prop { get; set; } + + public SmallType_IgnoredProp_Bind_ParamWithDefaultValue(int prop = 5) + => Prop = prop; + } + + public class SmallType_IgnoredProp_Bind_Param + { + [JsonIgnore] + public int Prop { get; set; } + + public SmallType_IgnoredProp_Bind_Param(int prop) + => Prop = prop; + } + + [Fact] + public async Task LargeObject_ClrDefaultParamValueUsed_WhenMatchingPropIgnored() + { + string json = @"{""Prop"":20}"; + var obj1 = await JsonSerializerWrapperForString.DeserializeWrapper(json); + Assert.Equal(0, obj1.Prop); + + var obj2 = await JsonSerializerWrapperForString.DeserializeWrapper(json); + Assert.Equal(0, obj2.Prop); + } + + public class LargeType_IgnoredProp_Bind_ParamWithDefaultValue + { + public int W { get; set; } + + public int X { get; set; } + + public int Y { get; set; } + + public int Z { get; set; } + + [JsonIgnore] + public int Prop { get; set; } + + public LargeType_IgnoredProp_Bind_ParamWithDefaultValue(int w, int x, int y, int z, int prop = 5) + => Prop = prop; + } + + public class LargeType_IgnoredProp_Bind_Param + { + public int W { get; set; } + + public int X { get; set; } + + public int Y { get; set; } + + public int Z { get; set; } + + [JsonIgnore] + public int Prop { get; set; } + + public LargeType_IgnoredProp_Bind_Param(int w, int x, int y, int z, int prop) + => Prop = prop; + } + + [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34779")] + public async Task BindingBetweenRefProps() + { + string json = @"{""NameRef"":""John""}"; + await JsonSerializerWrapperForString.DeserializeWrapper(json); + } + + public class TypeWith_RefStringProp_ParamCtor + { + private string _name; + + public ref string NameRef => ref _name; + + public TypeWith_RefStringProp_ParamCtor(ref string nameRef) => _name = nameRef; + } + + [Fact] + public async Task BindToIgnoredPropOfSameType() + { + string json = @"{""Prop"":{}}"; + Assert.NotNull(await JsonSerializerWrapperForString.DeserializeWrapper(json)); + } + + public class ClassWithIgnoredSameType + { + [JsonIgnore] + public ClassWithIgnoredSameType Prop { get; } + + public ClassWithIgnoredSameType(ClassWithIgnoredSameType prop) { } + } } } diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs index a2b5845ca44dc..a41380e3e60d9 100644 --- a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs @@ -2742,5 +2742,80 @@ public async Task JsonIgnoreCondition_Polymorphic() Assert.Equal(3, obj.Abstract_Property); Assert.Equal(4, obj.Virtual_Property); } + + [Fact] + public async Task SerializationMetadataNotComputedWhenMemberIgnored() + { + string janePayload = @"{""Name"":""Jane Doe""}"; + +#if !BUILDING_SOURCE_GENERATOR_TESTS + // Without [JsonIgnore], serializer throws exceptions due to runtime-reflection-based property metadata inspection. + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(new TypeWith_RefStringProp())); + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("{}")); + + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(new TypeWith_PropWith_BadConverter())); + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("{}")); +#else + // Ref returns supported in source-gen mode + string expected = @"{""NameRef"":""John Doe"",""Name"":""John Doe""}"; + JsonTestHelper.AssertJsonEqual(expected, await JsonSerializerWrapperForString.SerializeWrapper(new TypeWith_RefStringProp())); + + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(janePayload); + Assert.Equal("Jane Doe", obj.Name); + Assert.Equal("Jane Doe", obj.NameRef); + + var obj2 = new TypeWith_PropWith_BadConverter(); + obj2.Property = "Hello"; + + // Invalid converter specified, fallback to built-in converter. This should be corrected. + // https://github.com/dotnet/runtime/issues/60020. + + Assert.Equal(@"{""Property"":""Hello""}", await JsonSerializerWrapperForString.SerializeWrapper(obj2)); + + obj2 = await JsonSerializerWrapperForString.DeserializeWrapper(@"{""Property"":""World""}"); + Assert.Equal("World", obj2.Property); +#endif + + // With [JsonIgnore], serializer skips property metadata inspection + Assert.Equal(@"{""Name"":""John Doe""}", await JsonSerializerWrapperForString.SerializeWrapper(new TypeWith_IgnoredRefStringProp())); + Assert.Equal("Jane Doe", (await JsonSerializerWrapperForString.DeserializeWrapper(janePayload)).Name); + + Assert.Equal("{}", await JsonSerializerWrapperForString.SerializeWrapper(new TypeWith_IgnoredPropWith_BadConverter())); + Assert.Null((await JsonSerializerWrapperForString.DeserializeWrapper("{}")).Property); + } + + internal class TypeWith_RefStringProp + { + public ref string NameRef => ref Name; + + [JsonInclude] // This is a field. + public string Name = "John Doe"; + } + + internal class TypeWith_IgnoredRefStringProp + { + [JsonIgnore] + public ref string NameRef => ref Name; + + [JsonInclude] // This is a field. + public string Name = "John Doe"; + } + + public class TypeWith_PropWith_BadConverter + { + [JsonConverter(typeof(BadConverter))] + public string? Property { get; set; } + } + + public class TypeWith_IgnoredPropWith_BadConverter + { + [JsonIgnore] + [JsonConverter(typeof(BadConverter))] + public string? Property { get; set; } + } + + public class BadConverter + { + } } } 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 f0d1cb897ce2d..deba91529ef37 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 @@ -125,6 +125,11 @@ protected ConstructorTests_Metadata(JsonSerializerWrapperForString stringWrapper [JsonSerializable(typeof(JsonElement))] [JsonSerializable(typeof(Parameterized_Class_With_ComplexTuple))] [JsonSerializable(typeof(Parameterized_Person_Simple))] + [JsonSerializable(typeof(SmallType_IgnoredProp_Bind_ParamWithDefaultValue))] + [JsonSerializable(typeof(SmallType_IgnoredProp_Bind_Param))] + [JsonSerializable(typeof(LargeType_IgnoredProp_Bind_ParamWithDefaultValue))] + [JsonSerializable(typeof(LargeType_IgnoredProp_Bind_Param))] + [JsonSerializable(typeof(ClassWithIgnoredSameType))] internal sealed partial class ConstructorTestsContext_Metadata : JsonSerializerContext { } @@ -241,6 +246,11 @@ public ConstructorTests_Default() [JsonSerializable(typeof(JsonElement))] [JsonSerializable(typeof(Parameterized_Class_With_ComplexTuple))] [JsonSerializable(typeof(Parameterized_Person_Simple))] + [JsonSerializable(typeof(SmallType_IgnoredProp_Bind_ParamWithDefaultValue))] + [JsonSerializable(typeof(SmallType_IgnoredProp_Bind_Param))] + [JsonSerializable(typeof(LargeType_IgnoredProp_Bind_ParamWithDefaultValue))] + [JsonSerializable(typeof(LargeType_IgnoredProp_Bind_Param))] + [JsonSerializable(typeof(ClassWithIgnoredSameType))] internal sealed partial class ConstructorTestsContext_Default : JsonSerializerContext { } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs index f4858804d6012..3b38cefe23e98 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs @@ -264,6 +264,10 @@ public override async Task HonorJsonPropertyName_PrivateSetter() [JsonSerializable(typeof(ClassWithValueAndReferenceTypes))] [JsonSerializable(typeof(ClassWithReadOnlyStringProperty_IgnoreWhenWritingDefault))] [JsonSerializable(typeof(ConcreteDerivedClass))] + [JsonSerializable(typeof(TypeWith_RefStringProp))] + [JsonSerializable(typeof(TypeWith_IgnoredRefStringProp))] + [JsonSerializable(typeof(TypeWith_PropWith_BadConverter))] + [JsonSerializable(typeof(TypeWith_IgnoredPropWith_BadConverter))] internal sealed partial class PropertyVisibilityTestsContext_Metadata : JsonSerializerContext { } @@ -431,6 +435,10 @@ public override async Task JsonIgnoreCondition_WhenWritingNull_OnValueType_Fail_ [JsonSerializable(typeof(ClassWithValueAndReferenceTypes))] [JsonSerializable(typeof(ClassWithReadOnlyStringProperty_IgnoreWhenWritingDefault))] [JsonSerializable(typeof(ConcreteDerivedClass))] + [JsonSerializable(typeof(TypeWith_RefStringProp))] + [JsonSerializable(typeof(TypeWith_IgnoredRefStringProp))] + [JsonSerializable(typeof(TypeWith_PropWith_BadConverter))] + [JsonSerializable(typeof(TypeWith_IgnoredPropWith_BadConverter))] internal sealed partial class PropertyVisibilityTestsContext_Default : JsonSerializerContext { }