diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContext/Person.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContext/Person.cs index 36785e95bd05d..8049845e1a218 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContext/Person.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/JsonContext/Person.cs @@ -47,12 +47,15 @@ private static JsonPropertyInfo[] PersonPropInitFunc(JsonSerializerContext conte properties[0] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: true, + isPublic: true, + isVirtual: false, declaringType: typeof(Person), propertyTypeInfo: jsonContext.Int32, converter: null, getter: static (obj) => { return ((Person)obj).Age; }, setter: static (obj, value) => { ((Person)obj).Age = value; }, ignoreCondition: default, + hasJsonInclude: false, numberHandling: default, propertyName: nameof(Tests.Person.Age), jsonPropertyName: null); @@ -60,12 +63,15 @@ private static JsonPropertyInfo[] PersonPropInitFunc(JsonSerializerContext conte properties[1] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: true, + isPublic: true, + isVirtual: false, declaringType: typeof(Person), propertyTypeInfo: jsonContext.String, converter: null, getter: static (obj) => { return ((Person)obj).Name; }, setter: static (obj, value) => { ((Person)obj).Name = value; }, ignoreCondition: default, + hasJsonInclude: false, numberHandling: default, propertyName: nameof(Tests.Person.Name), jsonPropertyName: null); @@ -73,12 +79,15 @@ private static JsonPropertyInfo[] PersonPropInitFunc(JsonSerializerContext conte properties[2] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: true, + isPublic: true, + isVirtual: false, declaringType: typeof(Person), propertyTypeInfo: jsonContext.Person, converter: null, getter: static (obj) => { return ((Person)obj).Parent; }, setter: static (obj, value) => { ((Person)obj).Parent = value; }, ignoreCondition: default, + hasJsonInclude: false, numberHandling: default, propertyName: nameof(Tests.Person.Parent), jsonPropertyName: null); @@ -86,12 +95,15 @@ private static JsonPropertyInfo[] PersonPropInitFunc(JsonSerializerContext conte properties[3] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: true, + isPublic: true, + isVirtual: false, declaringType: typeof(Person), propertyTypeInfo: jsonContext.String, converter: null, getter: static (obj) => { return ((Person)obj).PlaceOfBirth; }, setter: static (obj, value) => { ((Person)obj).PlaceOfBirth = value; }, ignoreCondition: default, + hasJsonInclude: false, numberHandling: default, propertyName: nameof(Tests.Person.PlaceOfBirth), jsonPropertyName: null); diff --git a/src/libraries/System.Text.Json/Common/JsonHelpers.cs b/src/libraries/System.Text.Json/Common/JsonHelpers.cs new file mode 100644 index 0000000000000..96a2872621f40 --- /dev/null +++ b/src/libraries/System.Text.Json/Common/JsonHelpers.cs @@ -0,0 +1,28 @@ +// 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.Generic; + +namespace System.Text.Json +{ + internal static partial class JsonHelpers + { + /// + /// Emulates Dictionary.TryAdd on netstandard. + /// + public static bool TryAdd(this Dictionary dictionary, in TKey key, in TValue value) where TKey : notnull + { +#if NETSTANDARD2_0 || NETFRAMEWORK + if (!dictionary.ContainsKey(key)) + { + dictionary[key] = value; + return true; + } + + return false; +#else + return dictionary.TryAdd(key, value); +#endif + } + } +} diff --git a/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs b/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs new file mode 100644 index 0000000000000..5d9ef545b8d44 --- /dev/null +++ b/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Reflection; + +namespace System.Text.Json.Reflection +{ + internal static partial class ReflectionExtensions + { + public static bool IsVirtual(this PropertyInfo? propertyInfo) + { + Debug.Assert(propertyInfo != null); + return propertyInfo != null && (propertyInfo.GetMethod?.IsVirtual == true || propertyInfo.SetMethod?.IsVirtual == true); + } + } +} diff --git a/src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs b/src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs index f4b27c2db1187..8020f798086e9 100644 --- a/src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -using System.Text.Json.SourceGeneration.Reflection; +using System.Text.Json.Reflection; namespace System.Text.Json.SourceGeneration { @@ -19,6 +19,8 @@ internal sealed class ContextGenerationSpec public List RootSerializableTypes { get; } = new(); + public HashSet? NullableUnderlyingTypes { get; } = new(); + public List ContextClassDeclarationList { get; init; } /// diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 811a4ea8365e6..cbab663971eed 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; @@ -35,8 +37,9 @@ private sealed partial class Emitter private const string ArrayTypeRef = "global::System.Array"; private const string InvalidOperationExceptionTypeRef = "global::System.InvalidOperationException"; private const string TypeTypeRef = "global::System.Type"; - private const string UnsafeTypeRef = "global::System.CompilerServices.Unsafe"; + private const string UnsafeTypeRef = "global::System.Runtime.CompilerServices.Unsafe"; private const string NullableTypeRef = "global::System.Nullable"; + private const string EqualityComparerTypeRef = "global::System.Collections.Generic.EqualityComparer"; private const string IListTypeRef = "global::System.Collections.Generic.IList"; private const string KeyValuePairTypeRef = "global::System.Collections.Generic.KeyValuePair"; private const string ListTypeRef = "global::System.Collections.Generic.List"; @@ -210,9 +213,9 @@ private void GenerateTypeInfo(TypeGenerationSpec typeGenerationSpec) { source = GenerateForObject(typeGenerationSpec); - if (typeGenerationSpec.PropertiesMetadata != null) + if (typeGenerationSpec.PropertyGenSpecList != null) { - foreach (PropertyGenerationSpec metadata in typeGenerationSpec.PropertiesMetadata) + foreach (PropertyGenerationSpec metadata in typeGenerationSpec.PropertyGenSpecList) { GenerateTypeInfo(metadata.TypeGenerationSpec); } @@ -279,7 +282,7 @@ private string GenerateForTypeWithUnknownConverter(TypeGenerationSpec typeMetada }} // Allow nullable handling to forward to the underlying type's converter. - converter = {JsonMetadataServicesTypeRef}.GetNullableConverter<{typeCompilableName}>(({JsonConverterTypeRef}<{typeCompilableName}>)actualConverter); + converter = {JsonMetadataServicesTypeRef}.GetNullableConverter<{typeCompilableName}>(this.{typeFriendlyName}); }} else {{ @@ -471,7 +474,6 @@ private string GenerateFastPathFuncForDictionary( private string GenerateForObject(TypeGenerationSpec typeMetadata) { - string typeCompilableName = typeMetadata.TypeRef; string typeFriendlyName = typeMetadata.TypeInfoPropertyName; string createObjectFuncTypeArg = typeMetadata.ConstructionStrategy == ObjectConstructionStrategy.ParameterlessConstructor @@ -486,11 +488,9 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata) string? serializeFuncSource = null; string serializeFuncNamedArg; - List? properties = typeMetadata.PropertiesMetadata; - if (typeMetadata.GenerateMetadata) { - propMetadataInitFuncSource = GeneratePropMetadataInitFunc(typeMetadata.IsValueType, propInitMethodName, properties); + propMetadataInitFuncSource = GeneratePropMetadataInitFunc(typeMetadata.IsValueType, propInitMethodName, typeMetadata.PropertyGenSpecList!); propMetadataInitFuncNamedArg = $@"propInitFunc: {propInitMethodName}"; } else @@ -500,13 +500,7 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata) if (typeMetadata.GenerateSerializationLogic) { - serializeFuncSource = GenerateFastPathFuncForObject( - typeCompilableName, - serializeMethodName, - typeMetadata.CanBeNull, - typeMetadata.ImplementsIJsonOnSerialized, - typeMetadata.ImplementsIJsonOnSerializing, - properties); + serializeFuncSource = GenerateFastPathFuncForObject(typeMetadata, serializeMethodName); serializeFuncNamedArg = $@"serializeFunc: {serializeMethodName}"; } else @@ -514,14 +508,12 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata) serializeFuncNamedArg = @"serializeFunc: null"; } - string objectInfoInitSource = $@"{JsonTypeInfoTypeRef}<{typeCompilableName}> objectInfo = {JsonMetadataServicesTypeRef}.CreateObjectInfo<{typeCompilableName}>( + string objectInfoInitSource = $@"_{typeFriendlyName} = {JsonMetadataServicesTypeRef}.CreateObjectInfo<{typeMetadata.TypeRef}>( {OptionsInstanceVariableName}, {createObjectFuncTypeArg}, {propMetadataInitFuncNamedArg}, {GetNumberHandlingAsStr(typeMetadata.NumberHandling)}, - {serializeFuncNamedArg}); - - _{typeFriendlyName} = objectInfo;"; + {serializeFuncNamedArg});"; string additionalSource; if (propMetadataInitFuncSource == null || serializeFuncSource == null) @@ -539,14 +531,16 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata) private string GeneratePropMetadataInitFunc( bool declaringTypeIsValueType, string propInitMethodName, - List? properties) + List properties) { const string PropVarName = "properties"; const string JsonContextVarName = "jsonContext"; + int propCount = properties.Count; + string propertyArrayInstantiationValue = properties == null ? $"{ArrayTypeRef}.Empty<{JsonPropertyInfoTypeRef}>()" - : $"new {JsonPropertyInfoTypeRef}[{properties.Count}]"; + : $"new {JsonPropertyInfoTypeRef}[{propCount}]"; string contextTypeRef = _currentContext.ContextTypeRef; @@ -562,72 +556,72 @@ private string GeneratePropMetadataInitFunc( {JsonPropertyInfoTypeRef}[] {PropVarName} = {propertyArrayInstantiationValue}; "); - if (properties != null) + for (int i = 0; i < propCount; i++) { - for (int i = 0; i < properties.Count; i++) - { - PropertyGenerationSpec memberMetadata = properties[i]; + PropertyGenerationSpec memberMetadata = properties[i]; - TypeGenerationSpec memberTypeMetadata = memberMetadata.TypeGenerationSpec; + TypeGenerationSpec memberTypeMetadata = memberMetadata.TypeGenerationSpec; - string clrPropertyName = memberMetadata.ClrName; + string clrPropertyName = memberMetadata.ClrName; - string declaringTypeCompilableName = memberMetadata.DeclaringTypeRef; + string declaringTypeCompilableName = memberMetadata.DeclaringTypeRef; - string memberTypeFriendlyName = memberTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen - ? "null" - : $"{JsonContextVarName}.{memberTypeMetadata.TypeInfoPropertyName}"; + string memberTypeFriendlyName = memberTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen + ? "null" + : $"{JsonContextVarName}.{memberTypeMetadata.TypeInfoPropertyName}"; - string typeTypeInfoNamedArg = $"propertyTypeInfo: {memberTypeFriendlyName}"; + string typeTypeInfoNamedArg = $"propertyTypeInfo: {memberTypeFriendlyName}"; - string jsonPropertyNameNamedArg = memberMetadata.JsonPropertyName != null - ? @$"jsonPropertyName: ""{memberMetadata.JsonPropertyName}""" - : "jsonPropertyName: null"; + string jsonPropertyNameNamedArg = memberMetadata.JsonPropertyName != null + ? @$"jsonPropertyName: ""{memberMetadata.JsonPropertyName}""" + : "jsonPropertyName: null"; - string getterNamedArg = memberMetadata.CanUseGetter - ? $"getter: static (obj) => {{ return (({declaringTypeCompilableName})obj).{clrPropertyName}; }}" - : "getter: null"; + string getterNamedArg = memberMetadata.CanUseGetter + ? $"getter: static (obj) => (({declaringTypeCompilableName})obj).{clrPropertyName}" + : "getter: null"; - string setterNamedArg; - if (memberMetadata.CanUseSetter) - { - string propMutation = declaringTypeIsValueType - ? @$"{{ {UnsafeTypeRef}.Unbox<{declaringTypeCompilableName}>(obj).{clrPropertyName} = value; }}" - : $@"{{ (({declaringTypeCompilableName})obj).{clrPropertyName} = value; }}"; + string setterNamedArg; + if (memberMetadata.CanUseSetter) + { + string propMutation = declaringTypeIsValueType + ? @$"{UnsafeTypeRef}.Unbox<{declaringTypeCompilableName}>(obj).{clrPropertyName} = value" + : $@"(({declaringTypeCompilableName})obj).{clrPropertyName} = value"; - setterNamedArg = $"setter: static (obj, value) => {propMutation}"; - } - else - { - setterNamedArg = "setter: null"; - } + setterNamedArg = $"setter: static (obj, value) => {propMutation}"; + } + else + { + setterNamedArg = "setter: null"; + } - JsonIgnoreCondition? ignoreCondition = memberMetadata.DefaultIgnoreCondition; - string ignoreConditionNamedArg = ignoreCondition.HasValue - ? $"ignoreCondition: JsonIgnoreCondition.{ignoreCondition.Value}" - : "ignoreCondition: default"; + JsonIgnoreCondition? ignoreCondition = memberMetadata.DefaultIgnoreCondition; + string ignoreConditionNamedArg = ignoreCondition.HasValue + ? $"ignoreCondition: {JsonIgnoreConditionTypeRef}.{ignoreCondition.Value}" + : "ignoreCondition: null"; - string converterNamedArg = memberMetadata.ConverterInstantiationLogic == null - ? "converter: null" - : $"converter: {memberMetadata.ConverterInstantiationLogic}"; + string converterNamedArg = memberMetadata.ConverterInstantiationLogic == null + ? "converter: null" + : $"converter: {memberMetadata.ConverterInstantiationLogic}"; - string memberTypeCompilableName = memberTypeMetadata.TypeRef; + string memberTypeCompilableName = memberTypeMetadata.TypeRef; - sb.Append($@" + sb.Append($@" {PropVarName}[{i}] = {JsonMetadataServicesTypeRef}.CreatePropertyInfo<{memberTypeCompilableName}>( options, - isProperty: {memberMetadata.IsProperty.ToString().ToLowerInvariant()}, + isProperty: {ToCSharpKeyword(memberMetadata.IsProperty)}, + isPublic: {ToCSharpKeyword(memberMetadata.IsPublic)}, + isVirtual: {ToCSharpKeyword(memberMetadata.IsVirtual)}, declaringType: typeof({memberMetadata.DeclaringTypeRef}), {typeTypeInfoNamedArg}, {converterNamedArg}, {getterNamedArg}, {setterNamedArg}, {ignoreConditionNamedArg}, + hasJsonInclude: {ToCSharpKeyword(memberMetadata.HasJsonInclude)}, numberHandling: {GetNumberHandlingAsStr(memberMetadata.NumberHandling)}, propertyName: ""{clrPropertyName}"", {jsonPropertyNameNamedArg}); "); - } } sb.Append(@$" @@ -637,24 +631,29 @@ private string GeneratePropMetadataInitFunc( return sb.ToString(); } - private string GenerateFastPathFuncForObject( - string typeInfoTypeRef, - string serializeMethodName, - bool canBeNull, - bool implementsIJsonOnSerialized, - bool implementsIJsonOnSerializing, - List? properties) + private string GenerateFastPathFuncForObject(TypeGenerationSpec typeGenSpec, string serializeMethodName) { JsonSourceGenerationOptionsAttribute options = _currentContext.GenerationOptions; + string typeRef = typeGenSpec.TypeRef; - // Add the property names to the context-wide cache; we'll generate the source to initialize them at the end of generation. - string[] runtimePropNames = GetRuntimePropNames(properties, options.PropertyNamingPolicy); - _currentContext.RuntimePropertyNames.UnionWith(runtimePropNames); + if (!typeGenSpec.TryFilterSerializableProps( + options, + out Dictionary? serializableProperties, + out bool castingRequiredForProps)) + { + string exceptionMessage = @$"""Invalid serializable-property configuration specified for type '{typeRef}'. For more information, use 'JsonSourceGenerationMode.Serialization'."""; + + return GenerateFastPathFuncForType( + serializeMethodName, + typeRef, + $@"throw new {InvalidOperationExceptionTypeRef}({exceptionMessage});", + canBeNull: false); // Skip null check since we want to throw an exception straightaway. + } StringBuilder sb = new(); - // Begin method definition - if (implementsIJsonOnSerializing) + // Begin method logic. + if (typeGenSpec.ImplementsIJsonOnSerializing) { sb.Append($@"(({IJsonOnSerializingFullName}){ValueVarName}).OnSerializing();"); sb.Append($@"{Environment.NewLine} "); @@ -662,98 +661,119 @@ private string GenerateFastPathFuncForObject( sb.Append($@"{WriterVarName}.WriteStartObject();"); - if (properties != null) + // Provide generation logic for each prop. + Debug.Assert(serializableProperties != null); + + foreach (PropertyGenerationSpec propertyGenSpec in serializableProperties.Values) { - // Provide generation logic for each prop. - for (int i = 0; i < properties.Count; i++) + if (!ShouldIncludePropertyForFastPath(propertyGenSpec, options)) { - PropertyGenerationSpec propertySpec = properties[i]; - TypeGenerationSpec propertyTypeSpec = propertySpec.TypeGenerationSpec; - - if (propertyTypeSpec.ClassType == ClassType.TypeUnsupportedBySourceGen) - { - continue; - } + continue; + } - if (propertySpec.IsReadOnly) - { - if (propertySpec.IsProperty) - { - if (options.IgnoreReadOnlyProperties) - { - continue; - } - } - else if (options.IgnoreReadOnlyFields) - { - continue; - } - } + TypeGenerationSpec propertyTypeSpec = propertyGenSpec.TypeGenerationSpec; - if (!propertySpec.IsProperty && !propertySpec.HasJsonInclude && !options.IncludeFields) - { - continue; - } + string runtimePropName = propertyGenSpec.RuntimePropertyName; - Type propertyType = propertyTypeSpec.Type; - string propName = $"{runtimePropNames[i]}PropName"; - string propValue = $"{ValueVarName}.{propertySpec.ClrName}"; - string methodArgs = $"{propName}, {propValue}"; + // Add the property names to the context-wide cache; we'll generate the source to initialize them at the end of generation. + _currentContext.RuntimePropertyNames.Add(runtimePropName); - string? methodToCall = GetWriterMethod(propertyType); + Type propertyType = propertyTypeSpec.Type; + string propName = $"{runtimePropName}PropName"; + string? objectRef = castingRequiredForProps ? $"(({propertyGenSpec.DeclaringTypeRef}){ValueVarName})" : ValueVarName; + string propValue = $"{objectRef}.{propertyGenSpec.ClrName}"; + string methodArgs = $"{propName}, {propValue}"; - if (propertyType == _generationSpec.CharType) - { - methodArgs = $"{methodArgs}.ToString()"; - } + string? methodToCall = GetWriterMethod(propertyType); - string serializationLogic; + if (propertyType == _generationSpec.CharType) + { + methodArgs = $"{methodArgs}.ToString()"; + } - if (methodToCall != null) - { - serializationLogic = $@" - {methodToCall}({methodArgs});"; - } - else - { - serializationLogic = $@" - {WriterVarName}.WritePropertyName({propName}); - {GetSerializeLogicForNonPrimitiveType(propertyTypeSpec.TypeInfoPropertyName, propValue, propertyTypeSpec.GenerateSerializationLogic)}"; - } + string serializationLogic; - JsonIgnoreCondition ignoreCondition = propertySpec.DefaultIgnoreCondition ?? options.DefaultIgnoreCondition; - DefaultCheckType defaultCheckType; - bool typeCanBeNull = propertyTypeSpec.CanBeNull; + if (methodToCall != null) + { + serializationLogic = $@" + {methodToCall}({methodArgs});"; + } + else + { + serializationLogic = $@" + {WriterVarName}.WritePropertyName({propName}); + {GetSerializeLogicForNonPrimitiveType(propertyTypeSpec.TypeInfoPropertyName, propValue, propertyTypeSpec.GenerateSerializationLogic)}"; + } - switch (ignoreCondition) - { - case JsonIgnoreCondition.WhenWritingNull: - defaultCheckType = typeCanBeNull ? DefaultCheckType.Null : DefaultCheckType.None; - break; - case JsonIgnoreCondition.WhenWritingDefault: - defaultCheckType = typeCanBeNull ? DefaultCheckType.Null : DefaultCheckType.Default; - break; - default: - defaultCheckType = DefaultCheckType.None; - break; - } + JsonIgnoreCondition ignoreCondition = propertyGenSpec.DefaultIgnoreCondition ?? options.DefaultIgnoreCondition; + DefaultCheckType defaultCheckType; + bool typeCanBeNull = propertyTypeSpec.CanBeNull; - sb.Append(WrapSerializationLogicInDefaultCheckIfRequired(serializationLogic, propValue, defaultCheckType)); + switch (ignoreCondition) + { + case JsonIgnoreCondition.WhenWritingNull: + defaultCheckType = typeCanBeNull ? DefaultCheckType.Null : DefaultCheckType.None; + break; + case JsonIgnoreCondition.WhenWritingDefault: + defaultCheckType = typeCanBeNull ? DefaultCheckType.Null : DefaultCheckType.Default; + break; + default: + defaultCheckType = DefaultCheckType.None; + break; } + + sb.Append(WrapSerializationLogicInDefaultCheckIfRequired(serializationLogic, propValue, propertyTypeSpec.TypeRef, defaultCheckType)); } - // End method definition + // End method logic. sb.Append($@" - {WriterVarName}.WriteEndObject();"); + {WriterVarName}.WriteEndObject();"); - if (implementsIJsonOnSerialized) + if (typeGenSpec.ImplementsIJsonOnSerialized) { sb.Append($@"{Environment.NewLine} "); sb.Append($@"(({IJsonOnSerializedFullName}){ValueVarName}).OnSerialized();"); }; - return GenerateFastPathFuncForType(serializeMethodName, typeInfoTypeRef, sb.ToString(), canBeNull); + return GenerateFastPathFuncForType(serializeMethodName, typeRef, sb.ToString(), typeGenSpec.CanBeNull); + } + + private static bool ShouldIncludePropertyForFastPath(PropertyGenerationSpec propertyGenSpec, JsonSourceGenerationOptionsAttribute options) + { + TypeGenerationSpec propertyTypeSpec = propertyGenSpec.TypeGenerationSpec; + + if (propertyTypeSpec.ClassType == ClassType.TypeUnsupportedBySourceGen || !propertyGenSpec.CanUseGetter) + { + return false; + } + + if (!propertyGenSpec.IsProperty && !propertyGenSpec.HasJsonInclude && !options.IncludeFields) + { + return false; + } + + if (propertyGenSpec.DefaultIgnoreCondition == JsonIgnoreCondition.Always) + { + return false; + } + + if (propertyGenSpec.IsReadOnly) + { + if (propertyGenSpec.IsProperty) + { + if (options.IgnoreReadOnlyProperties) + { + return false; + } + } + else if (options.IgnoreReadOnlyFields) + { + return false; + } + } + + return true; } private string? GetWriterMethod(Type type) @@ -829,62 +849,28 @@ private enum DefaultCheckType Default, } - private string WrapSerializationLogicInDefaultCheckIfRequired(string serializationLogic, string propValue, DefaultCheckType defaultCheckType) - { - if (defaultCheckType == DefaultCheckType.None) - { - return serializationLogic; - } - - string defaultLiteral = defaultCheckType == DefaultCheckType.Null ? "null" : "default"; - return $@" - if ({propValue} != {defaultLiteral}) - {{{serializationLogic} - }}"; - } - - private string[] GetRuntimePropNames(List? properties, JsonKnownNamingPolicy namingPolicy) + private string WrapSerializationLogicInDefaultCheckIfRequired(string serializationLogic, string propValue, string propTypeRef, DefaultCheckType defaultCheckType) { - if (properties == null) - { - return Array.Empty(); - } - - int propCount = properties.Count; - string[] runtimePropNames = new string[propCount]; + string comparisonLogic; - // Compute JsonEncodedText values to represent each property name. This gives the best throughput performance - for (int i = 0; i < propCount; i++) + switch (defaultCheckType) { - PropertyGenerationSpec propertySpec = properties[i]; - - string propName = DetermineRuntimePropName(propertySpec.ClrName, propertySpec.JsonPropertyName, namingPolicy); - Debug.Assert(propName != null); - - runtimePropNames[i] = propName; - } - - return runtimePropNames; - } - - private string DetermineRuntimePropName(string clrPropName, string? jsonPropName, JsonKnownNamingPolicy namingPolicy) - { - string runtimePropName; - - if (jsonPropName != null) - { - runtimePropName = jsonPropName; - } - else if (namingPolicy == JsonKnownNamingPolicy.CamelCase) - { - runtimePropName = JsonNamingPolicy.CamelCase.ConvertName(clrPropName); - } - else - { - runtimePropName = clrPropName; + case DefaultCheckType.None: + return serializationLogic; + case DefaultCheckType.Null: + comparisonLogic = $"{propValue} != null"; + break; + case DefaultCheckType.Default: + comparisonLogic = $"!{EqualityComparerTypeRef}<{propTypeRef}>.Default.Equals(default, {propValue})"; + break; + default: + throw new InvalidOperationException(); } - return runtimePropName; + return $@" + if ({comparisonLogic}) + {{{IndentSource(serializationLogic, numIndentations: 1)} + }}"; } private string GenerateForType(TypeGenerationSpec typeMetadata, string metadataInitSource, string? additionalSource = null) @@ -964,10 +950,10 @@ private string GetLogicForDefaultSerializerOptionsInit() private static {JsonSerializerOptionsTypeRef} {DefaultOptionsStaticVarName} {{ get; }} = new {JsonSerializerOptionsTypeRef}() {{ DefaultIgnoreCondition = {JsonIgnoreConditionTypeRef}.{options.DefaultIgnoreCondition}, - IgnoreReadOnlyFields = {options.IgnoreReadOnlyFields.ToString().ToLowerInvariant()}, - IgnoreReadOnlyProperties = {options.IgnoreReadOnlyProperties.ToString().ToLowerInvariant()}, - IncludeFields = {options.IncludeFields.ToString().ToLowerInvariant()}, - WriteIndented = {options.WriteIndented.ToString().ToLowerInvariant()},{namingPolicyInit} + IgnoreReadOnlyFields = {ToCSharpKeyword(options.IgnoreReadOnlyFields)}, + IgnoreReadOnlyProperties = {ToCSharpKeyword(options.IgnoreReadOnlyProperties)}, + IncludeFields = {ToCSharpKeyword(options.IncludeFields)}, + WriteIndented = {ToCSharpKeyword(options.WriteIndented)},{namingPolicyInit} }};"; } @@ -1013,8 +999,11 @@ private string GetGetTypeInfoImplementation() sb.Append(@$"public override {JsonTypeInfoTypeRef} GetTypeInfo({TypeTypeRef} type) {{"); + HashSet types = new(_currentContext.RootSerializableTypes); + types.UnionWith(_currentContext.NullableUnderlyingTypes); + // TODO (https://github.com/dotnet/runtime/issues/52218): Make this Dictionary-lookup-based if root-serializable type count > 64. - foreach (TypeGenerationSpec metadata in _currentContext.RootSerializableTypes) + foreach (TypeGenerationSpec metadata in types) { if (metadata.ClassType != ClassType.TypeUnsupportedBySourceGen) { @@ -1065,5 +1054,7 @@ private static string GetNumberHandlingAsStr(JsonNumberHandling? numberHandling) private static string GetCreateValueInfoMethodRef(string typeCompilableName) => $"{CreateValueInfoMethodName}<{typeCompilableName}>"; } + + private static string ToCSharpKeyword(bool value) => value.ToString().ToLowerInvariant(); } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 70c8a412b6e59..cefb9de2d7164 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -4,15 +4,15 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; +using System.Text.Json.Reflection; using System.Text.Json.Serialization; -using System.Text.Json.SourceGeneration.Reflection; -using System.Linq; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; namespace System.Text.Json.SourceGeneration { @@ -62,6 +62,10 @@ private sealed class Parser /// private readonly Dictionary _typeGenerationSpecCache = new(); + private readonly HashSet _nullableTypeGenerationSpecCache = new(); + + private JsonKnownNamingPolicy _currentContextNamingPolicy; + private static DiagnosticDescriptor ContextClassesMustBePartial { get; } = new DiagnosticDescriptor( id: "SYSLIB1032", title: new LocalizableResourceString(nameof(SR.ContextClassesMustBePartialTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), @@ -164,7 +168,10 @@ public Parser(in GeneratorExecutionContext executionContext) ContextClassDeclarationList = classDeclarationList }; - foreach(AttributeSyntax attribute in serializableAttributeList) + // Set the naming policy for the current context. + _currentContextNamingPolicy = contextGenSpec.GenerationOptions.PropertyNamingPolicy; + + foreach (AttributeSyntax attribute in serializableAttributeList) { TypeGenerationSpec? metadata = GetRootSerializableType(compilationSemanticModel, attribute, contextGenSpec.GenerationOptions.GenerationMode); if (metadata != null) @@ -178,11 +185,14 @@ public Parser(in GeneratorExecutionContext executionContext) continue; } + contextGenSpec.NullableUnderlyingTypes.UnionWith(_nullableTypeGenerationSpecCache); + contextGenSpecList ??= new List(); contextGenSpecList.Add(contextGenSpec); // Clear the cache of generated metadata between the processing of context classes. _typeGenerationSpecCache.Clear(); + _nullableTypeGenerationSpecCache.Clear(); } if (contextGenSpecList == null) @@ -231,7 +241,7 @@ private bool DerivesFromJsonSerializerContext( return match != null; } - private static bool TryGetClassDeclarationList(INamedTypeSymbol typeSymbol, [NotNullWhenAttribute(true)] out List? classDeclarationList) + private static bool TryGetClassDeclarationList(INamedTypeSymbol typeSymbol, [NotNullWhen(true)] out List? classDeclarationList) { INamedTypeSymbol currentSymbol = typeSymbol; classDeclarationList = null; @@ -479,14 +489,14 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener } // Add metadata to cache now to prevent stack overflow when the same type is found somewhere else in the object graph. - typeMetadata = new(); + typeMetadata = new TypeGenerationSpec(); _typeGenerationSpecCache[type] = typeMetadata; ClassType classType; Type? collectionKeyType = null; Type? collectionValueType = null; - Type? nullableUnderlyingType = null; - List? propertiesMetadata = null; + TypeGenerationSpec? nullableUnderlyingTypeGenSpec = null; + List? propGenSpecList = null; CollectionType collectionType = CollectionType.NotApplicable; ObjectConstructionStrategy constructionStrategy = default; JsonNumberHandling? numberHandling = null; @@ -524,10 +534,12 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener { classType = ClassType.KnownType; } - else if (type.IsNullableValueType(_nullableOfTType, out nullableUnderlyingType)) + else if (type.IsNullableValueType(_nullableOfTType, out Type? nullableUnderlyingType)) { Debug.Assert(nullableUnderlyingType != null); classType = ClassType.Nullable; + nullableUnderlyingTypeGenSpec = GetOrAddTypeGenerationSpec(nullableUnderlyingType, generationMode); + _nullableTypeGenerationSpecCache.Add(nullableUnderlyingTypeGenSpec); } else if (type.IsEnum) { @@ -585,38 +597,40 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener implementsIJsonOnSerialized = interfaces.FirstOrDefault(interfaceName => interfaceName == IJsonOnSerializedFullName) != null; implementsIJsonOnSerializing = interfaces.FirstOrDefault(interfaceName => interfaceName == IJsonOnSerializingFullName) != null; + propGenSpecList = new List(); + Dictionary? ignoredMembers = null; + + const BindingFlags bindingFlags = + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.DeclaredOnly; + for (Type? currentType = type; currentType != null; currentType = currentType.BaseType) { - const BindingFlags bindingFlags = - BindingFlags.Instance | - BindingFlags.Public | - BindingFlags.NonPublic | - BindingFlags.DeclaredOnly; - foreach (PropertyInfo propertyInfo in currentType.GetProperties(bindingFlags)) { - PropertyGenerationSpec metadata = GetPropertyGenerationSpec(propertyInfo, generationMode); + bool isVirtual = propertyInfo.IsVirtual(); - // Ignore indexers. - if (propertyInfo.GetIndexParameters().Length > 0) + if (propertyInfo.GetIndexParameters().Length > 0 || + PropertyIsOverridenAndIgnored(propertyInfo.Name, propertyInfo.PropertyType, isVirtual, ignoredMembers)) { continue; } - if (metadata.CanUseGetter || metadata.CanUseSetter) - { - (propertiesMetadata ??= new()).Add(metadata); - } + PropertyGenerationSpec spec = GetPropertyGenerationSpec(propertyInfo, isVirtual, generationMode); + CacheMember(spec, ref propGenSpecList, ref ignoredMembers); } foreach (FieldInfo fieldInfo in currentType.GetFields(bindingFlags)) { - PropertyGenerationSpec metadata = GetPropertyGenerationSpec(fieldInfo, generationMode); - - if (metadata.CanUseGetter || metadata.CanUseSetter) + if (PropertyIsOverridenAndIgnored(fieldInfo.Name, fieldInfo.FieldType, currentMemberIsVirtual: false, ignoredMembers)) { - (propertiesMetadata ??= new()).Add(metadata); + continue; } + + PropertyGenerationSpec spec = GetPropertyGenerationSpec(fieldInfo, isVirtual: false, generationMode); + CacheMember(spec, ref propGenSpecList, ref ignoredMembers); } } } @@ -629,12 +643,12 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener classType, isValueType: type.IsValueType, numberHandling, - propertiesMetadata, + propGenSpecList, collectionType, collectionKeyTypeMetadata: collectionKeyType != null ? GetOrAddTypeGenerationSpec(collectionKeyType, generationMode) : null, collectionValueTypeMetadata: collectionValueType != null ? GetOrAddTypeGenerationSpec(collectionValueType, generationMode) : null, constructionStrategy, - nullableUnderlyingTypeMetadata: nullableUnderlyingType != null ? GetOrAddTypeGenerationSpec(nullableUnderlyingType, generationMode) : null, + nullableUnderlyingTypeMetadata: nullableUnderlyingTypeGenSpec, converterInstatiationLogic, implementsIJsonOnSerialized, implementsIJsonOnSerializing); @@ -642,7 +656,37 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener return typeMetadata; } - private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, JsonSourceGenerationMode generationMode) + private void CacheMember( + PropertyGenerationSpec propGenSpec, + ref List propGenSpecList, + ref Dictionary ignoredMembers) + { + propGenSpecList.Add(propGenSpec); + + if (propGenSpec.DefaultIgnoreCondition == JsonIgnoreCondition.Always) + { + ignoredMembers ??= new Dictionary(); + ignoredMembers.Add(propGenSpec.ClrName, propGenSpec); + } + } + + private static bool PropertyIsOverridenAndIgnored( + string currentMemberName, + Type currentMemberType, + bool currentMemberIsVirtual, + Dictionary? ignoredMembers) + { + if (ignoredMembers == null || !ignoredMembers.TryGetValue(currentMemberName, out PropertyGenerationSpec? ignoredMember)) + { + return false; + } + + return currentMemberType == ignoredMember.TypeGenerationSpec.Type && + currentMemberIsVirtual && + ignoredMember.IsVirtual; + } + + private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, bool isVirtual, JsonSourceGenerationMode generationMode) { IList attributeDataList = CustomAttributeData.GetCustomAttributes(memberInfo); @@ -709,8 +753,9 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, Type memberCLRType; bool isReadOnly; - bool canUseGetter; - bool canUseSetter; + bool isPublic = false; + bool canUseGetter = false; + bool canUseSetter = false; bool getterIsVirtual = false; bool setterIsVirtual = false; @@ -718,33 +763,75 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, { case PropertyInfo propertyInfo: { - MethodInfo setMethod = propertyInfo.SetMethod; memberCLRType = propertyInfo.PropertyType; - isReadOnly = setMethod == null; - canUseGetter = PropertyAccessorCanBeReferenced(propertyInfo.GetMethod, hasJsonInclude); - canUseSetter = PropertyAccessorCanBeReferenced(setMethod, hasJsonInclude) && !setMethod.IsInitOnly(); - getterIsVirtual = propertyInfo.GetMethod?.IsVirtual == true; - setterIsVirtual = propertyInfo.SetMethod?.IsVirtual == true; + + MethodInfo? getMethod = propertyInfo.GetMethod; + MethodInfo? setMethod = propertyInfo.SetMethod; + + if (getMethod != null) + { + if (getMethod.IsPublic) + { + isPublic = true; + canUseGetter = true; + } + else if (getMethod.IsAssembly) + { + canUseGetter = hasJsonInclude; + } + + getterIsVirtual = getMethod.IsVirtual; + } + + if (setMethod != null) + { + isReadOnly = false; + + if (setMethod.IsPublic) + { + isPublic = true; + canUseSetter = !setMethod.IsInitOnly(); + } + else if (setMethod.IsAssembly) + { + canUseSetter = hasJsonInclude && !setMethod.IsInitOnly(); + } + + setterIsVirtual = setMethod.IsVirtual; + } + else + { + isReadOnly = true; + } } break; case FieldInfo fieldInfo: { - Debug.Assert(fieldInfo.IsPublic); memberCLRType = fieldInfo.FieldType; + isPublic = fieldInfo.IsPublic; isReadOnly = fieldInfo.IsInitOnly; - canUseGetter = true; - canUseSetter = !isReadOnly; + + if (!fieldInfo.IsPrivate && !fieldInfo.IsFamily) + { + canUseGetter = true; + canUseSetter = !isReadOnly; + } } break; default: throw new InvalidOperationException(); } + string clrName = memberInfo.Name; + return new PropertyGenerationSpec { - ClrName = memberInfo.Name, + ClrName = clrName, IsProperty = memberInfo.MemberType == MemberTypes.Property, + IsPublic = isPublic, + IsVirtual = isVirtual, JsonPropertyName = jsonPropertyName, + RuntimePropertyName = DetermineRuntimePropName(clrName, jsonPropertyName, _currentContextNamingPolicy), IsReadOnly = isReadOnly, CanUseGetter = canUseGetter, CanUseSetter = canUseSetter, @@ -759,8 +846,8 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, }; } - private static bool PropertyAccessorCanBeReferenced(MethodInfo? memberAccessor, bool hasJsonInclude) => - (memberAccessor != null && !memberAccessor.IsPrivate) && (memberAccessor.IsPublic || hasJsonInclude); + private static bool PropertyAccessorCanBeReferenced(MethodInfo? accessor) + => accessor != null && (accessor.IsPublic || accessor.IsAssembly); private string? GetConverterInstantiationLogic(CustomAttributeData attributeData) { @@ -780,6 +867,26 @@ private static bool PropertyAccessorCanBeReferenced(MethodInfo? memberAccessor, return $"new {converterType.GetUniqueCompilableTypeName()}()"; } + private static string DetermineRuntimePropName(string clrPropName, string? jsonPropName, JsonKnownNamingPolicy namingPolicy) + { + string runtimePropName; + + if (jsonPropName != null) + { + runtimePropName = jsonPropName; + } + else if (namingPolicy == JsonKnownNamingPolicy.CamelCase) + { + runtimePropName = JsonNamingPolicy.CamelCase.ConvertName(clrPropName); + } + else + { + runtimePropName = clrPropName; + } + + return runtimePropName; + } + private void PopulateNumberTypes() { Debug.Assert(_numberTypes != null); diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs index 7e8c45ff4299c..399b893e8e8e9 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Reflection; -using System.Text.Json.SourceGeneration.Reflection; +using System.Text.Json.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -37,6 +37,12 @@ public void Initialize(GeneratorInitializationContext context) /// public void Execute(GeneratorExecutionContext executionContext) { +#if LAUNCH_DEBUGGER + if (!Diagnostics.Debugger.IsAttached) + { + Diagnostics.Debugger.Launch(); + } +#endif SyntaxReceiver receiver = (SyntaxReceiver)executionContext.SyntaxReceiver; List? contextClasses = receiver.ClassDeclarationSyntaxList; if (contextClasses == null) @@ -55,12 +61,6 @@ public void Execute(GeneratorExecutionContext executionContext) } } - /// - /// Helper for unit tests. - /// - public Dictionary? GetSerializableTypes() => _rootTypes?.ToDictionary(p => p.Type.FullName, p => p.Type); - private List? _rootTypes; - private sealed class SyntaxReceiver : ISyntaxReceiver { public List? ClassDeclarationSyntaxList { get; private set; } @@ -73,5 +73,11 @@ public void OnVisitSyntaxNode(SyntaxNode syntaxNode) } } } + + /// + /// Helper for unit tests. + /// + public Dictionary? GetSerializableTypes() => _rootTypes?.ToDictionary(p => p.Type.FullName, p => p.Type); + private List? _rootTypes; } } diff --git a/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs b/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs index 25ae73692ecb0..5d52ae2e3553e 100644 --- a/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs @@ -9,9 +9,6 @@ namespace System.Text.Json.SourceGeneration [DebuggerDisplay("Name={Name}, Type={TypeMetadata}")] internal sealed class PropertyGenerationSpec { - /// - /// The CLR name of the property. - /// public string ClrName { get; init; } /// @@ -19,11 +16,22 @@ internal sealed class PropertyGenerationSpec /// public bool IsProperty { get; init; } + public bool IsPublic { get; init; } + + public bool IsVirtual { get; init; } + /// /// The property name specified via JsonPropertyNameAttribute, if available. /// public string? JsonPropertyName { get; init; } + /// + /// The pre-determined JSON property name, accounting for + /// specified ahead-of-time via . + /// Only used in fast-path serialization logic. + /// + public string RuntimePropertyName { get; init; } + /// /// Whether the property has a set method. /// diff --git a/src/libraries/System.Text.Json/gen/Reflection/AssemblyWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/AssemblyWrapper.cs index d5ed28e4800a1..a3282b6984439 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/AssemblyWrapper.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/AssemblyWrapper.cs @@ -5,7 +5,7 @@ using System.Reflection; using Microsoft.CodeAnalysis; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { internal class AssemblyWrapper : Assembly { diff --git a/src/libraries/System.Text.Json/gen/Reflection/ConstructorInfoWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/ConstructorInfoWrapper.cs index 979d32c8f7c99..42d167a462207 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/ConstructorInfoWrapper.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/ConstructorInfoWrapper.cs @@ -6,7 +6,7 @@ using System.Reflection; using Microsoft.CodeAnalysis; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { internal class ConstructorInfoWrapper : ConstructorInfo { diff --git a/src/libraries/System.Text.Json/gen/Reflection/CustomAttributeDataWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/CustomAttributeDataWrapper.cs index 75b31db587a8f..e518212ce65b0 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/CustomAttributeDataWrapper.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/CustomAttributeDataWrapper.cs @@ -6,7 +6,7 @@ using System.Reflection; using Microsoft.CodeAnalysis; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { internal class CustomAttributeDataWrapper : CustomAttributeData { diff --git a/src/libraries/System.Text.Json/gen/Reflection/FieldInfoWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/FieldInfoWrapper.cs index cf36ceacbeee5..66e00c0de20bb 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/FieldInfoWrapper.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/FieldInfoWrapper.cs @@ -6,7 +6,7 @@ using Microsoft.CodeAnalysis; using System.Globalization; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { internal class FieldInfoWrapper : FieldInfo { @@ -33,6 +33,11 @@ public override FieldAttributes Attributes _attributes |= FieldAttributes.Static; } + if (_field.IsReadOnly) + { + _attributes |= FieldAttributes.InitOnly; + } + switch (_field.DeclaredAccessibility) { case Accessibility.Public: @@ -41,6 +46,9 @@ public override FieldAttributes Attributes case Accessibility.Private: _attributes |= FieldAttributes.Private; break; + case Accessibility.Protected: + _attributes |= FieldAttributes.Family; + break; } } diff --git a/src/libraries/System.Text.Json/gen/Reflection/MemberInfoWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/MemberInfoWrapper.cs index 1b4de7811fd74..ccfd0fb7c6e4c 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/MemberInfoWrapper.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/MemberInfoWrapper.cs @@ -5,7 +5,7 @@ using System.Reflection; using Microsoft.CodeAnalysis; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { internal class MemberInfoWrapper : MemberInfo { diff --git a/src/libraries/System.Text.Json/gen/Reflection/MetadataLoadContextInternal.cs b/src/libraries/System.Text.Json/gen/Reflection/MetadataLoadContextInternal.cs index 885885325fe53..d12f3f8ed60f8 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/MetadataLoadContextInternal.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/MetadataLoadContextInternal.cs @@ -8,7 +8,7 @@ using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { internal class MetadataLoadContextInternal { diff --git a/src/libraries/System.Text.Json/gen/Reflection/MethodInfoWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/MethodInfoWrapper.cs index 44eecf0b5919d..637483847e599 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/MethodInfoWrapper.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/MethodInfoWrapper.cs @@ -6,7 +6,7 @@ using System.Reflection; using Microsoft.CodeAnalysis; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { internal class MethodInfoWrapper : MethodInfo { @@ -41,7 +41,7 @@ public override MethodAttributes Attributes _attributes |= MethodAttributes.Static; } - if (_method.IsVirtual) + if (_method.IsVirtual || _method.IsOverride) { _attributes |= MethodAttributes.Virtual; } @@ -54,6 +54,9 @@ public override MethodAttributes Attributes case Accessibility.Private: _attributes |= MethodAttributes.Private; break; + case Accessibility.Internal: + _attributes |= MethodAttributes.Assembly; + break; } } diff --git a/src/libraries/System.Text.Json/gen/Reflection/ParameterInfoWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/ParameterInfoWrapper.cs index b8891251c7e62..ad9091017728f 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/ParameterInfoWrapper.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/ParameterInfoWrapper.cs @@ -5,7 +5,7 @@ using System.Reflection; using Microsoft.CodeAnalysis; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { internal class ParameterInfoWrapper : ParameterInfo { diff --git a/src/libraries/System.Text.Json/gen/Reflection/PropertyInfoWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/PropertyInfoWrapper.cs index 294c6dd105916..bfb593f992fbd 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/PropertyInfoWrapper.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/PropertyInfoWrapper.cs @@ -6,7 +6,7 @@ using System.Reflection; using Microsoft.CodeAnalysis; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { internal class PropertyInfoWrapper : PropertyInfo { diff --git a/src/libraries/System.Text.Json/gen/Reflection/ReflectionExtensions.cs b/src/libraries/System.Text.Json/gen/Reflection/ReflectionExtensions.cs index 1c0619ddda58e..599cae49aebd5 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/ReflectionExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/ReflectionExtensions.cs @@ -5,9 +5,9 @@ using System.Linq; using System.Reflection; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { - internal static class ReflectionExtensions + internal static partial class ReflectionExtensions { public static CustomAttributeData GetCustomAttributeData(this MemberInfo memberInfo, Type type) { diff --git a/src/libraries/System.Text.Json/gen/Reflection/RoslynExtensions.cs b/src/libraries/System.Text.Json/gen/Reflection/RoslynExtensions.cs index 610e7fe4eef6a..3dd1e925e2f19 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/RoslynExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/RoslynExtensions.cs @@ -5,7 +5,7 @@ using System.Reflection; using Microsoft.CodeAnalysis; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { internal static class RoslynExtensions { diff --git a/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs b/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs index 8ff487ab0c6ee..6ad76f42b7b4a 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Linq; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { internal static class TypeExtensions { diff --git a/src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs b/src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs index 2ffc4725922ab..abeed3dfaaf0d 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs @@ -9,7 +9,7 @@ using System.Reflection; using Microsoft.CodeAnalysis; -namespace System.Text.Json.SourceGeneration.Reflection +namespace System.Text.Json.Reflection { internal class TypeWrapper : Type { @@ -231,18 +231,32 @@ public override FieldInfo GetField(string name, BindingFlags bindingAttr) public override FieldInfo[] GetFields(BindingFlags bindingAttr) { - var fields = new List(); + List fields = new(); + foreach (ISymbol item in _typeSymbol.GetMembers()) { - // Associated Symbol checks the field is not a backingfield. - if (item is IFieldSymbol field && field.AssociatedSymbol == null && !field.IsReadOnly) + if (item is IFieldSymbol fieldSymbol) { - if ((item.DeclaredAccessibility & Accessibility.Public) == Accessibility.Public) + // Skip if: + if ( + // this is a backing field + fieldSymbol.AssociatedSymbol != null || + // we want a static field and this is not static + (BindingFlags.Static & bindingAttr) != 0 && !fieldSymbol.IsStatic || + // we want an instance field and this is static or a constant + (BindingFlags.Instance & bindingAttr) != 0 && (fieldSymbol.IsStatic || fieldSymbol.IsConst)) + { + continue; + } + + if ((BindingFlags.Public & bindingAttr) != 0 && item.DeclaredAccessibility == Accessibility.Public || + (BindingFlags.NonPublic & bindingAttr) != 0) { - fields.Add(new FieldInfoWrapper(field, _metadataLoadContext)); + fields.Add(new FieldInfoWrapper(fieldSymbol, _metadataLoadContext)); } } } + return fields.ToArray(); } @@ -300,18 +314,28 @@ public override Type[] GetNestedTypes(BindingFlags bindingAttr) return nestedTypes.ToArray(); } - // TODO: make sure to use bindingAttr for correctness. Current implementation assumes public and non-static. public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) { - var properties = new List(); + List properties = new(); foreach (ISymbol item in _typeSymbol.GetMembers()) { - if (item is IPropertySymbol property) + if (item is IPropertySymbol propertySymbol) { - if ((item.DeclaredAccessibility & Accessibility.Public) == Accessibility.Public) + // Skip if: + if ( + // we want a static property and this is not static + (BindingFlags.Static & bindingAttr) != 0 && !propertySymbol.IsStatic || + // we want an instance property and this is static + (BindingFlags.Instance & bindingAttr) != 0 && propertySymbol.IsStatic) + { + continue; + } + + if ((BindingFlags.Public & bindingAttr) != 0 && item.DeclaredAccessibility == Accessibility.Public || + (BindingFlags.NonPublic & bindingAttr) != 0) { - properties.Add(new PropertyInfoWrapper(property, _metadataLoadContext)); + properties.Add(new PropertyInfoWrapper(propertySymbol, _metadataLoadContext)); } } } diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj index 3e449e71aab1a..008a5b221895d 100644 --- a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj +++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj @@ -13,6 +13,7 @@ $(DefineConstants);BUILDING_SOURCE_GENERATOR + $(DefineConstants);LAUNCH_DEBUGGER @@ -27,12 +28,14 @@ + + diff --git a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs index 36a0c58a17a31..865134823f9a3 100644 --- a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs @@ -3,8 +3,9 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Reflection; using System.Text.Json.Serialization; -using System.Text.Json.SourceGeneration.Reflection; namespace System.Text.Json.SourceGeneration { @@ -42,7 +43,7 @@ internal class TypeGenerationSpec public JsonNumberHandling? NumberHandling { get; private set; } - public List? PropertiesMetadata { get; private set; } + public List? PropertyGenSpecList { get; private set; } public CollectionType CollectionType { get; private set; } @@ -64,7 +65,7 @@ public void Initialize( ClassType classType, bool isValueType, JsonNumberHandling? numberHandling, - List? propertiesMetadata, + List? propertyGenSpecList, CollectionType collectionType, TypeGenerationSpec? collectionKeyTypeMetadata, TypeGenerationSpec? collectionValueTypeMetadata, @@ -82,7 +83,7 @@ public void Initialize( IsValueType = isValueType; CanBeNull = !isValueType || nullableUnderlyingTypeMetadata != null; NumberHandling = numberHandling; - PropertiesMetadata = propertiesMetadata; + PropertyGenSpecList = propertyGenSpecList; CollectionType = collectionType; CollectionKeyTypeMetadata = collectionKeyTypeMetadata; CollectionValueTypeMetadata = collectionValueTypeMetadata; @@ -93,6 +94,86 @@ public void Initialize( ImplementsIJsonOnSerializing = implementsIJsonOnSerializing; } + public bool TryFilterSerializableProps( + JsonSourceGenerationOptionsAttribute options, + [NotNullWhen(true)] out Dictionary? serializableProperties, + out bool castingRequiredForProps) + { + serializableProperties = new Dictionary(); + Dictionary? ignoredMembers = null; + + for (int i = 0; i < PropertyGenSpecList.Count; i++) + { + PropertyGenerationSpec propGenSpec = PropertyGenSpecList[i]; + bool hasJsonInclude = propGenSpec.HasJsonInclude; + JsonIgnoreCondition? ignoreCondition = propGenSpec.DefaultIgnoreCondition; + + if (ignoreCondition == JsonIgnoreCondition.WhenWritingNull && !propGenSpec.TypeGenerationSpec.CanBeNull) + { + goto ReturnFalse; + } + + if (!propGenSpec.IsPublic) + { + if (hasJsonInclude) + { + goto ReturnFalse; + } + + continue; + } + + if (!propGenSpec.IsProperty && !hasJsonInclude && !options.IncludeFields) + { + continue; + } + + string memberName = propGenSpec.ClrName!; + + // The JsonPropertyNameAttribute or naming policy resulted in a collision. + if (!serializableProperties.TryAdd(propGenSpec.RuntimePropertyName, propGenSpec)) + { + PropertyGenerationSpec other = serializableProperties[propGenSpec.RuntimePropertyName]!; + + if (other.DefaultIgnoreCondition == JsonIgnoreCondition.Always) + { + // Overwrite previously cached property since it has [JsonIgnore]. + serializableProperties[propGenSpec.RuntimePropertyName] = propGenSpec; + } + else if ( + // Does the current property have `JsonIgnoreAttribute`? + propGenSpec.DefaultIgnoreCondition != JsonIgnoreCondition.Always && + // Is the current property hidden by the previously cached property + // (with `new` keyword, or by overriding)? + other.ClrName != memberName && + // Was a property with the same CLR name was ignored? That property hid the current property, + // thus, if it was ignored, the current property should be ignored too. + ignoredMembers?.ContainsKey(memberName) != true) + { + // We throw if we have two public properties that have the same JSON property name, and neither have been ignored. + serializableProperties = null; + castingRequiredForProps = false; + return false; + } + // Ignore the current property. + } + + if (propGenSpec.DefaultIgnoreCondition == JsonIgnoreCondition.Always) + { + (ignoredMembers ??= new Dictionary()).Add(memberName, propGenSpec); + } + } + + Debug.Assert(PropertyGenSpecList.Count >= serializableProperties.Count); + castingRequiredForProps = PropertyGenSpecList.Count > serializableProperties.Count; + return true; + +ReturnFalse: + serializableProperties = null; + castingRequiredForProps = false; + return false; + } + private bool FastPathIsSupported() { if (ClassType == ClassType.Object) diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 2d74d3535c3fc..8e87154e80357 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -932,7 +932,7 @@ public static partial class JsonMetadataServices public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateDictionaryInfo(System.Text.Json.JsonSerializerOptions options, System.Func createObjectFunc, System.Text.Json.Serialization.Metadata.JsonTypeInfo keyInfo, System.Text.Json.Serialization.Metadata.JsonTypeInfo valueInfo, System.Text.Json.Serialization.JsonNumberHandling numberHandling, System.Action? serializeFunc) where TCollection : System.Collections.Generic.Dictionary where TKey : notnull { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateListInfo(System.Text.Json.JsonSerializerOptions options, System.Func? createObjectFunc, System.Text.Json.Serialization.Metadata.JsonTypeInfo elementInfo, System.Text.Json.Serialization.JsonNumberHandling numberHandling, System.Action? serializeFunc) where TCollection : System.Collections.Generic.List { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateObjectInfo(System.Text.Json.JsonSerializerOptions options, System.Func? createObjectFunc, System.Func? propInitFunc, System.Text.Json.Serialization.JsonNumberHandling numberHandling, System.Action? serializeFunc) where T : notnull { throw null; } - public static System.Text.Json.Serialization.Metadata.JsonPropertyInfo CreatePropertyInfo(System.Text.Json.JsonSerializerOptions options, bool isProperty, System.Type declaringType, System.Text.Json.Serialization.Metadata.JsonTypeInfo propertyTypeInfo, System.Text.Json.Serialization.JsonConverter? converter, System.Func? getter, System.Action? setter, System.Text.Json.Serialization.JsonIgnoreCondition ignoreCondition, System.Text.Json.Serialization.JsonNumberHandling numberHandling, string propertyName, string? jsonPropertyName) { throw null; } + public static System.Text.Json.Serialization.Metadata.JsonPropertyInfo CreatePropertyInfo(System.Text.Json.JsonSerializerOptions options, bool isProperty, bool isPublic, bool isVirtual, System.Type declaringType, System.Text.Json.Serialization.Metadata.JsonTypeInfo propertyTypeInfo, System.Text.Json.Serialization.JsonConverter? converter, System.Func? getter, System.Action? setter, System.Text.Json.Serialization.JsonIgnoreCondition? ignoreCondition, bool hasJsonInclude, System.Text.Json.Serialization.JsonNumberHandling? numberHandling, string propertyName, string? jsonPropertyName) { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateValueInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.JsonConverter converter) { throw null; } public static System.Text.Json.Serialization.JsonConverter GetEnumConverter(System.Text.Json.JsonSerializerOptions options) where T : struct { throw null; } public static System.Text.Json.Serialization.JsonConverter GetNullableConverter(System.Text.Json.Serialization.Metadata.JsonTypeInfo underlyingTypeInfo) where T : struct { throw null; } diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index c650f6ebad8bb..e289dfbb1ea6d 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -608,4 +608,7 @@ To specify a serialization implementation for type '{0}'', context '{0}' must specify default options. + + A 'field' member cannot be 'virtual'. See arguments for the '{0}' and '{1}' parameters. + \ No newline at end of file diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 9d94ae9bdd4f4..195c640c85aa7 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -22,12 +22,14 @@ + + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs index 8dce0c594e1fe..0bdd85880bd0f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace System.Text.Json @@ -100,24 +99,6 @@ public static string Utf8GetString(ReadOnlySpan bytes) ); } - /// - /// Emulates Dictionary.TryAdd on netstandard. - /// - public static bool TryAdd(Dictionary dictionary, in TKey key, in TValue value) where TKey : notnull - { -#if NETSTANDARD2_0 || NETFRAMEWORK - if (!dictionary.ContainsKey(key)) - { - dictionary[key] = value; - return true; - } - - return false; -#else - return dictionary.TryAdd(key, value); -#endif - } - /// /// Emulates Dictionary(IEnumerable{KeyValuePair}) on netstandard. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs index 5268573219dd2..731869ec4a9bb 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs @@ -18,6 +18,8 @@ public static partial class JsonMetadataServices /// The type that the converter for the property returns or accepts when converting JSON data. /// The to initialize the metadata with. /// Whether the CLR member is a property or field. + /// Whether the CLR member is public. + /// Whether the CLR member is a virtual property. /// The declaring type of the property or field. /// The info for the property or field's type. /// A for the property or field, specified by . @@ -25,19 +27,23 @@ public static partial class JsonMetadataServices /// Provides a mechanism to set the property or field's value. /// Specifies a condition for the property to be ignored. /// If the property or field is a number, specifies how it should processed when serializing and deserializing. + /// Whether the property was annotated with . /// The CLR name of the property or field. /// The name to be used when processing the property or field, specified by . /// A instance intialized with the provided metadata. public static JsonPropertyInfo CreatePropertyInfo( JsonSerializerOptions options, bool isProperty, + bool isPublic, + bool isVirtual, Type declaringType, JsonTypeInfo propertyTypeInfo, JsonConverter? converter, Func? getter, Action? setter, - JsonIgnoreCondition ignoreCondition, - JsonNumberHandling numberHandling, + JsonIgnoreCondition? ignoreCondition, + bool hasJsonInclude, + JsonNumberHandling? numberHandling, string propertyName, string? jsonPropertyName) { @@ -70,16 +76,23 @@ public static JsonPropertyInfo CreatePropertyInfo( } } + if (!isProperty && isVirtual) + { + throw new InvalidOperationException(SR.Format(SR.FieldCannotBeVirtual, nameof(isProperty), nameof(isVirtual))); + } + JsonPropertyInfo jsonPropertyInfo = new JsonPropertyInfo(); jsonPropertyInfo.InitializeForSourceGen( options, isProperty, + isPublic, declaringType, propertyTypeInfo, converter, getter, setter, ignoreCondition, + hasJsonInclude, numberHandling, propertyName, jsonPropertyName); 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 2a97240d67b97..95081b8519dea 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,17 +42,19 @@ internal static JsonPropertyInfo GetPropertyPlaceholder() // Create a property 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. - internal static JsonPropertyInfo CreateIgnoredPropertyPlaceholder(MemberInfo memberInfo, JsonSerializerOptions options) + internal static JsonPropertyInfo CreateIgnoredPropertyPlaceholder(MemberInfo memberInfo, Type memberType, bool isVirtual, JsonSerializerOptions options) { JsonPropertyInfo jsonPropertyInfo = new JsonPropertyInfo(); + jsonPropertyInfo.Options = options; jsonPropertyInfo.MemberInfo = memberInfo; - jsonPropertyInfo.DeterminePropertyName(); jsonPropertyInfo.IsIgnored = true; + jsonPropertyInfo.DeclaredPropertyType = memberType; + jsonPropertyInfo.IsVirtual = isVirtual; + jsonPropertyInfo.DeterminePropertyName(); Debug.Assert(!jsonPropertyInfo.ShouldDeserialize); Debug.Assert(!jsonPropertyInfo.ShouldSerialize); - return jsonPropertyInfo; } @@ -81,6 +83,8 @@ private void DeterminePropertyName() { Debug.Assert(MemberInfo != null); + ClrName = MemberInfo.Name; + JsonPropertyNameAttribute? nameAttribute = GetAttribute(MemberInfo); if (nameAttribute != null) { @@ -305,6 +309,7 @@ internal virtual void Initialize( Type? runtimePropertyType, ConverterStrategy runtimeClassType, MemberInfo? memberInfo, + bool isVirtual, JsonConverter converter, JsonIgnoreCondition? ignoreCondition, JsonNumberHandling? parentTypeNumberHandling, @@ -312,12 +317,12 @@ internal virtual void Initialize( { Debug.Assert(converter != null); - ClrName = memberInfo?.Name; DeclaringType = parentClassType; DeclaredPropertyType = declaredPropertyType; RuntimePropertyType = runtimePropertyType; ConverterStrategy = runtimeClassType; MemberInfo = memberInfo; + IsVirtual = isVirtual; ConverterBase = converter; Options = options; } @@ -479,6 +484,16 @@ internal JsonTypeInfo RuntimeTypeInfo internal bool IsIgnored { get; set; } + /// + /// Relevant to source generated metadata: did the property have the ? + /// + internal bool SrcGen_HasJsonInclude { get; set; } + + /// + /// Relevant to source generated metadata: is the property public? + /// + internal bool SrcGen_IsPublic { get; set; } + internal JsonNumberHandling? NumberHandling { get; set; } // Whether the property type can be null. @@ -489,5 +504,7 @@ internal JsonTypeInfo RuntimeTypeInfo internal MemberTypes MemberType { get; set; } // TODO: with some refactoring, we should be able to remove this. internal string? ClrName { get; set; } + + internal bool IsVirtual { get; set; } } } 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 8e268c2509940..dda0e7f12dd1f 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 @@ -37,6 +37,7 @@ internal override void Initialize( Type? runtimePropertyType, ConverterStrategy runtimeClassType, MemberInfo? memberInfo, + bool isVirtual, JsonConverter converter, JsonIgnoreCondition? ignoreCondition, JsonNumberHandling? parentTypeNumberHandling, @@ -48,6 +49,7 @@ internal override void Initialize( runtimePropertyType, runtimeClassType, memberInfo, + isVirtual, converter, ignoreCondition, parentTypeNumberHandling, @@ -116,13 +118,15 @@ internal override void Initialize( internal void InitializeForSourceGen( JsonSerializerOptions options, bool isProperty, + bool isPublic, Type declaringType, JsonTypeInfo typeInfo, JsonConverter converter, Func? getter, Action? setter, - JsonIgnoreCondition ignoreCondition, - JsonNumberHandling numberHandling, + JsonIgnoreCondition? ignoreCondition, + bool hasJsonInclude, + JsonNumberHandling? numberHandling, string propertyName, string? jsonPropertyName) { @@ -150,6 +154,9 @@ internal void InitializeForSourceGen( NameAsUtf8Bytes ??= Encoding.UTF8.GetBytes(NameAsString!); EscapedNameSection ??= JsonHelpers.GetEscapedPropertyNameSection(NameAsUtf8Bytes, Options.Encoder); + SrcGen_IsPublic = isPublic; + SrcGen_HasJsonInclude = hasJsonInclude; + if (ignoreCondition == JsonIgnoreCondition.Always) { IsIgnored = true; 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 7b3369a90ee67..cb2545c0cc10b 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 @@ -52,13 +52,14 @@ internal static JsonPropertyInfo AddProperty( MemberInfo memberInfo, Type memberType, Type parentClassType, + bool isVirtual, JsonNumberHandling? parentTypeNumberHandling, JsonSerializerOptions options) { JsonIgnoreCondition? ignoreCondition = JsonPropertyInfo.GetAttribute(memberInfo)?.Condition; if (ignoreCondition == JsonIgnoreCondition.Always) { - return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(memberInfo, options); + return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(memberInfo, memberType, isVirtual, options); } JsonConverter converter = GetConverter( @@ -73,6 +74,7 @@ internal static JsonPropertyInfo AddProperty( runtimePropertyType: runtimeType, memberInfo, parentClassType, + isVirtual, converter, options, parentTypeNumberHandling, @@ -84,6 +86,7 @@ internal static JsonPropertyInfo CreateProperty( Type? runtimePropertyType, MemberInfo? memberInfo, Type parentClassType, + bool isVirtual, JsonConverter converter, JsonSerializerOptions options, JsonNumberHandling? parentTypeNumberHandling = null, @@ -98,6 +101,7 @@ internal static JsonPropertyInfo CreateProperty( runtimePropertyType, runtimeClassType: converter.ConverterStrategy, memberInfo, + isVirtual, converter, ignoreCondition, parentTypeNumberHandling, @@ -108,7 +112,7 @@ internal static JsonPropertyInfo CreateProperty( /// /// Create a for a given Type. - /// See . + /// See . /// internal static JsonPropertyInfo CreatePropertyInfoForTypeInfo( Type declaredPropertyType, @@ -121,8 +125,9 @@ internal static JsonPropertyInfo CreatePropertyInfoForTypeInfo( declaredPropertyType: declaredPropertyType, runtimePropertyType: runtimePropertyType, memberInfo: null, // Not a real property so this is null. - parentClassType: JsonTypeInfo.ObjectType, // a dummy value (not used) - converter: converter, + parentClassType: ObjectType, // a dummy value (not used) + isVirtual: false, + converter, options, parentTypeNumberHandling: numberHandling); @@ -582,18 +587,34 @@ internal void InitializePropCache() } JsonPropertyInfo[] array = PropInitFunc(context); - var properties = new JsonPropertyDictionary(Options.PropertyNameCaseInsensitive, array.Length); + Dictionary? ignoredMembers = null; + JsonPropertyDictionary propertyCache = new(Options.PropertyNameCaseInsensitive, array.Length); + for (int i = 0; i < array.Length; i++) { - JsonPropertyInfo property = array[i]; - if (!properties.TryAdd(property.NameAsString, property)) + JsonPropertyInfo jsonPropertyInfo = array[i]; + bool hasJsonInclude = jsonPropertyInfo.SrcGen_HasJsonInclude; + + if (!jsonPropertyInfo.SrcGen_IsPublic) + { + if (hasJsonInclude) + { + ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(jsonPropertyInfo.ClrName!, jsonPropertyInfo.DeclaringType); + } + + continue; + } + + if (jsonPropertyInfo.MemberType == MemberTypes.Field && !hasJsonInclude && !Options.IncludeFields) { - ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, property); + continue; } + + CacheMember(jsonPropertyInfo, propertyCache, ref ignoredMembers); } - // Avoid threading issues by populating a local cache, and assigning it to the global cache after completion. - PropertyCache = properties; + // Avoid threading issues by populating a local cache and assigning it to the global cache after completion. + PropertyCache = propertyCache; } } } 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 53ed06d9c3664..ba90400e24650 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 @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; +using System.Text.Json.Reflection; namespace System.Text.Json.Serialization.Metadata { @@ -192,7 +193,7 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json CreateObject = Options.MemberAccessorStrategy.CreateConstructor(type); - Dictionary? ignoredMembers = null; + Dictionary? ignoredMembers = null; PropertyInfo[] properties = type.GetProperties(bindingFlags); @@ -208,8 +209,12 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json { foreach (PropertyInfo propertyInfo in properties) { + bool isVirtual = propertyInfo.IsVirtual(); + string propertyName = propertyInfo.Name; + // Ignore indexers and virtual properties that have overrides that were [JsonIgnore]d. - if (propertyInfo.GetIndexParameters().Length > 0 || PropertyIsOverridenAndIgnored(propertyInfo, ignoredMembers)) + if (propertyInfo.GetIndexParameters().Length > 0 || + PropertyIsOverridenAndIgnored(propertyName, propertyInfo.PropertyType, isVirtual, ignoredMembers)) { continue; } @@ -222,6 +227,7 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json currentType, propertyInfo.PropertyType, propertyInfo, + isVirtual, typeNumberHandling, ref ignoredMembers); } @@ -229,7 +235,7 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json { if (JsonPropertyInfo.GetAttribute(propertyInfo) != null) { - ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(propertyInfo, currentType); + ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(propertyName, currentType); } // Non-public properties should not be included for (de)serialization. @@ -238,7 +244,9 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json foreach (FieldInfo fieldInfo in currentType.GetFields(bindingFlags)) { - if (PropertyIsOverridenAndIgnored(fieldInfo, ignoredMembers)) + string fieldName = fieldInfo.Name; + + if (PropertyIsOverridenAndIgnored(fieldName, fieldInfo.FieldType, currentMemberIsVirtual: false, ignoredMembers)) { continue; } @@ -253,6 +261,7 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json currentType, fieldInfo.FieldType, fieldInfo, + isVirtual: false, typeNumberHandling, ref ignoredMembers); } @@ -261,7 +270,7 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json { if (hasJsonInclude) { - ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(fieldInfo, currentType); + ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(fieldName, currentType); } // Non-public fields should not be included for (de)serialization. @@ -316,8 +325,9 @@ private void CacheMember( Type declaringType, Type memberType, MemberInfo memberInfo, + bool isVirtual, JsonNumberHandling? typeNumberHandling, - ref Dictionary? ignoredMembers) + ref Dictionary? ignoredMembers) { bool hasExtensionAttribute = memberInfo.GetCustomAttribute(typeof(JsonExtensionDataAttribute)) != null; if (hasExtensionAttribute && DataExtensionProperty != null) @@ -325,7 +335,7 @@ private void CacheMember( ThrowHelper.ThrowInvalidOperationException_SerializationDuplicateTypeAttribute(Type, typeof(JsonExtensionDataAttribute)); } - JsonPropertyInfo jsonPropertyInfo = AddProperty(memberInfo, memberType, declaringType, typeNumberHandling, Options); + JsonPropertyInfo jsonPropertyInfo = AddProperty(memberInfo, memberType, declaringType, isVirtual, typeNumberHandling, Options); Debug.Assert(jsonPropertyInfo.NameAsString != null); if (hasExtensionAttribute) @@ -336,38 +346,43 @@ private void CacheMember( } else { - string memberName = memberInfo.Name; + CacheMember(jsonPropertyInfo, PropertyCache, ref ignoredMembers); + } + } - // The JsonPropertyNameAttribute or naming policy resulted in a collision. - if (!PropertyCache!.TryAdd(jsonPropertyInfo.NameAsString, jsonPropertyInfo)) - { - JsonPropertyInfo other = PropertyCache[jsonPropertyInfo.NameAsString]!; + private void CacheMember(JsonPropertyInfo jsonPropertyInfo, JsonPropertyDictionary? propertyCache, ref Dictionary? ignoredMembers) + { + string memberName = jsonPropertyInfo.ClrName!; - if (other.IsIgnored) - { - // Overwrite previously cached property since it has [JsonIgnore]. - PropertyCache[jsonPropertyInfo.NameAsString] = jsonPropertyInfo; - } - else if ( - // 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.MemberInfo!.Name != memberName && - // Was a property with the same CLR name was ignored? That property hid the current property, - // thus, if it was ignored, the current property should be ignored too. - ignoredMembers?.ContainsKey(memberName) != true) - { - // We throw if we have two public properties that have the same JSON property name, and neither have been ignored. - ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, jsonPropertyInfo); - } - // Ignore the current property. - } + // The JsonPropertyNameAttribute or naming policy resulted in a collision. + if (!propertyCache!.TryAdd(jsonPropertyInfo.NameAsString, jsonPropertyInfo)) + { + JsonPropertyInfo other = propertyCache[jsonPropertyInfo.NameAsString]!; - if (jsonPropertyInfo.IsIgnored) + if (other.IsIgnored) + { + // Overwrite previously cached property since it has [JsonIgnore]. + propertyCache[jsonPropertyInfo.NameAsString] = jsonPropertyInfo; + } + else if ( + // 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.ClrName != memberName && + // Was a property with the same CLR name was ignored? That property hid the current property, + // thus, if it was ignored, the current property should be ignored too. + ignoredMembers?.ContainsKey(memberName) != true) { - (ignoredMembers ??= new Dictionary()).Add(memberName, memberInfo); + // We throw if we have two public properties that have the same JSON property name, and neither have been ignored. + ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, jsonPropertyInfo); } + // Ignore the current property. + } + + if (jsonPropertyInfo.IsIgnored) + { + (ignoredMembers ??= new Dictionary()).Add(memberName, jsonPropertyInfo); } } @@ -476,33 +491,21 @@ static Type GetMemberType(MemberInfo memberInfo) ParameterCache = parameterCache; ParameterCount = parameters.Length; } - private static bool PropertyIsOverridenAndIgnored(MemberInfo currentMember, Dictionary? ignoredMembers) + + private static bool PropertyIsOverridenAndIgnored( + string currentMemberName, + Type currentMemberType, + bool currentMemberIsVirtual, + Dictionary? ignoredMembers) { - if (ignoredMembers == null || !ignoredMembers.TryGetValue(currentMember.Name, out MemberInfo? ignoredProperty)) + if (ignoredMembers == null || !ignoredMembers.TryGetValue(currentMemberName, out JsonPropertyInfo? ignoredMember)) { return false; } - Debug.Assert(currentMember is PropertyInfo || currentMember is FieldInfo); - PropertyInfo? currentPropertyInfo = currentMember as PropertyInfo; - Type currentMemberType = currentPropertyInfo == null - ? Unsafe.As(currentMember).FieldType - : currentPropertyInfo.PropertyType; - - Debug.Assert(ignoredProperty is PropertyInfo || ignoredProperty is FieldInfo); - PropertyInfo? ignoredPropertyInfo = ignoredProperty as PropertyInfo; - Type ignoredPropertyType = ignoredPropertyInfo == null - ? Unsafe.As(ignoredProperty).FieldType - : ignoredPropertyInfo.PropertyType; - - return currentMemberType == ignoredPropertyType && - PropertyIsVirtual(currentPropertyInfo) && - PropertyIsVirtual(ignoredPropertyInfo); - } - - private static bool PropertyIsVirtual(PropertyInfo? propertyInfo) - { - return propertyInfo != null && (propertyInfo.GetMethod?.IsVirtual == true || propertyInfo.SetMethod?.IsVirtual == true); + return currentMemberType == ignoredMember.DeclaredPropertyType && + currentMemberIsVirtual && + ignoredMember.IsVirtual; } private void ValidateAndAssignDataExtensionProperty(JsonPropertyInfo jsonPropertyInfo) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index 7beb0612a4e18..42af83d147a57 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -244,9 +244,9 @@ public static void ThrowInvalidOperationException_ExtensionDataCannotBindToCtorP [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(MemberInfo memberInfo, Type parentType) + public static void ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(string memberName, Type declaringType) { - throw new InvalidOperationException(SR.Format(SR.JsonIncludeOnNonPublicInvalid, memberInfo.Name, parentType)); + throw new InvalidOperationException(SR.Format(SR.JsonIncludeOnNonPublicInvalid, memberName, declaringType)); } [DoesNotReturn] diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSerializerWrapperForString.cs b/src/libraries/System.Text.Json/tests/Common/JsonSerializerWrapperForString.cs new file mode 100644 index 0000000000000..eb67440c0d188 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/JsonSerializerWrapperForString.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; + +namespace System.Text.Json.Serialization.Tests +{ + public abstract partial class JsonSerializerWrapperForString + { + protected internal abstract Task SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null); + + protected internal abstract Task SerializeWrapper(T value, JsonSerializerOptions options = null); + + protected internal abstract Task SerializeWrapper(object value, Type inputType, JsonSerializerContext context); + + protected internal abstract Task SerializeWrapper(T value, JsonTypeInfo jsonTypeInfo); + + protected internal abstract Task DeserializeWrapper(string json, JsonSerializerOptions options = null); + + protected internal abstract Task DeserializeWrapper(string json, Type type, JsonSerializerOptions options = null); + + protected internal abstract Task DeserializeWrapper(string json, JsonTypeInfo jsonTypeInfo); + + protected internal abstract Task DeserializeWrapper(string json, Type type, JsonSerializerContext context); + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyVisiblityTests.InitOnly.cs b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.InitOnly.cs similarity index 73% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyVisiblityTests.InitOnly.cs rename to src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.InitOnly.cs index 48fe1e34c935b..eda534737b0e9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyVisiblityTests.InitOnly.cs +++ b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.InitOnly.cs @@ -1,51 +1,52 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Threading.Tasks; using Xunit; namespace System.Text.Json.Serialization.Tests { - public static partial class PropertyVisibilityTests + public abstract partial class PropertyVisibilityTests { [Theory] [InlineData(typeof(ClassWithInitOnlyProperty))] [InlineData(typeof(StructWithInitOnlyProperty))] - public static void InitOnlyProperties_Work(Type type) + public virtual async Task InitOnlyProperties(Type type) { // Init-only property included by default. - object obj = JsonSerializer.Deserialize(@"{""MyInt"":1}", type); + object obj = await JsonSerializerWrapperForString.DeserializeWrapper(@"{""MyInt"":1}", type); Assert.Equal(1, (int)type.GetProperty("MyInt").GetValue(obj)); // Init-only properties can be serialized. - Assert.Equal(@"{""MyInt"":1}", JsonSerializer.Serialize(obj)); + Assert.Equal(@"{""MyInt"":1}", await JsonSerializerWrapperForString.SerializeWrapper(obj)); } [Theory] [InlineData(typeof(Class_PropertyWith_PrivateInitOnlySetter))] [InlineData(typeof(Class_PropertyWith_InternalInitOnlySetter))] [InlineData(typeof(Class_PropertyWith_ProtectedInitOnlySetter))] - public static void NonPublicInitOnlySetter_Without_JsonInclude_Fails(Type type) + public async Task NonPublicInitOnlySetter_Without_JsonInclude_Fails(Type type) { // Non-public init-only property setter ignored. - object obj = JsonSerializer.Deserialize(@"{""MyInt"":1}", type); + object obj = await JsonSerializerWrapperForString.DeserializeWrapper(@"{""MyInt"":1}", type); Assert.Equal(0, (int)type.GetProperty("MyInt").GetValue(obj)); // Public getter can be used for serialization. - Assert.Equal(@"{""MyInt"":0}", JsonSerializer.Serialize(obj)); + Assert.Equal(@"{""MyInt"":0}", await JsonSerializerWrapperForString.SerializeWrapper(obj, type)); } [Theory] [InlineData(typeof(Class_PropertyWith_PrivateInitOnlySetter_WithAttribute))] [InlineData(typeof(Class_PropertyWith_InternalInitOnlySetter_WithAttribute))] [InlineData(typeof(Class_PropertyWith_ProtectedInitOnlySetter_WithAttribute))] - public static void NonPublicInitOnlySetter_With_JsonInclude_Works(Type type) + public virtual async Task NonPublicInitOnlySetter_With_JsonInclude(Type type) { // Non-public init-only property setter included with [JsonInclude]. - object obj = JsonSerializer.Deserialize(@"{""MyInt"":1}", type); + object obj = await JsonSerializerWrapperForString.DeserializeWrapper(@"{""MyInt"":1}", type); Assert.Equal(1, (int)type.GetProperty("MyInt").GetValue(obj)); // Init-only properties can be serialized. - Assert.Equal(@"{""MyInt"":1}", JsonSerializer.Serialize(obj)); + Assert.Equal(@"{""MyInt"":1}", await JsonSerializerWrapperForString.SerializeWrapper(obj)); } public class ClassWithInitOnlyProperty diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyVisibilityTests.NonPublicAccessors.cs b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.NonPublicAccessors.cs similarity index 66% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyVisibilityTests.NonPublicAccessors.cs rename to src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.NonPublicAccessors.cs index 0dd3657e6f5c3..0c8dc6a941c38 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyVisibilityTests.NonPublicAccessors.cs +++ b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.NonPublicAccessors.cs @@ -2,14 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Threading.Tasks; using Xunit; namespace System.Text.Json.Serialization.Tests { - public static partial class PropertyVisibilityTests + public abstract partial class PropertyVisibilityTests { [Fact] - public static void NonPublic_AccessorsNotSupported_WithoutAttribute() + public async Task NonPublic_AccessorsNotSupported_WithoutAttribute() { string json = @"{ ""MyInt"":1, @@ -18,20 +19,20 @@ public static void NonPublic_AccessorsNotSupported_WithoutAttribute() ""MyUri"":""https://microsoft.com"" }"; - var obj = JsonSerializer.Deserialize(json); + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); Assert.Equal(0, obj.MyInt); Assert.Null(obj.MyString); Assert.Equal(2f, obj.GetMyFloat); Assert.Equal(new Uri("https://microsoft.com"), obj.MyUri); - json = JsonSerializer.Serialize(obj); + json = await JsonSerializerWrapperForString.SerializeWrapper(obj); Assert.Contains(@"""MyInt"":0", json); Assert.Contains(@"""MyString"":null", json); Assert.DoesNotContain(@"""MyFloat"":", json); Assert.DoesNotContain(@"""MyUri"":", json); } - private class MyClass_WithNonPublicAccessors + public class MyClass_WithNonPublicAccessors { public int MyInt { get; private set; } public string MyString { get; internal set; } @@ -43,7 +44,7 @@ private class MyClass_WithNonPublicAccessors } [Fact] - public static void Honor_JsonSerializablePropertyAttribute_OnProperties() + public virtual async Task Honor_JsonSerializablePropertyAttribute_OnProperties() { string json = @"{ ""MyInt"":1, @@ -52,20 +53,20 @@ public static void Honor_JsonSerializablePropertyAttribute_OnProperties() ""MyUri"":""https://microsoft.com"" }"; - var obj = JsonSerializer.Deserialize(json); + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); Assert.Equal(1, obj.MyInt); Assert.Equal("Hello", obj.MyString); Assert.Equal(2f, obj.GetMyFloat); Assert.Equal(new Uri("https://microsoft.com"), obj.MyUri); - json = JsonSerializer.Serialize(obj); + json = await JsonSerializerWrapperForString.SerializeWrapper(obj); Assert.Contains(@"""MyInt"":1", json); Assert.Contains(@"""MyString"":""Hello""", json); Assert.Contains(@"""MyFloat"":2", json); Assert.Contains(@"""MyUri"":""https://microsoft.com""", json); } - private class MyClass_WithNonPublicAccessors_WithPropertyAttributes + public class MyClass_WithNonPublicAccessors_WithPropertyAttributes { [JsonInclude] public int MyInt { get; private set; } @@ -103,19 +104,23 @@ private class MyClass_WithNonPublicAccessors_WithPropertyAttributes_And_Property } [Fact] - public static void ExtensionDataCanHaveNonPublicSetter() +#if BUILDING_SOURCE_GENERATOR_TESTS + // Need support for extension data. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task ExtensionDataCanHaveNonPublicSetter() { string json = @"{""Key"":""Value""}"; // Baseline - var obj1 = JsonSerializer.Deserialize(json); + var obj1 = await JsonSerializerWrapperForString.DeserializeWrapper(json); Assert.Null(obj1.ExtensionData); - Assert.Equal("{}", JsonSerializer.Serialize(obj1)); + Assert.Equal("{}", await JsonSerializerWrapperForString.SerializeWrapper(obj1)); // With attribute - var obj2 = JsonSerializer.Deserialize(json); + var obj2 = await JsonSerializerWrapperForString.DeserializeWrapper(json); Assert.Equal("Value", obj2.ExtensionData["Key"].GetString()); - Assert.Equal(json, JsonSerializer.Serialize(obj2)); + Assert.Equal(json, await JsonSerializerWrapperForString.SerializeWrapper(obj2)); } private class ClassWithExtensionData_NonPublicSetter @@ -138,7 +143,7 @@ private class ClassWithExtensionData_NonPublicGetter } [Fact] - public static void HonorCustomConverter() + public virtual async Task HonorCustomConverter_UsingPrivateSetter() { var options = new JsonSerializerOptions(); options.Converters.Add(new JsonStringEnumConverter()); @@ -146,17 +151,17 @@ public static void HonorCustomConverter() string json = @"{""MyEnum"":""AnotherValue"",""MyInt"":2}"; // Deserialization baseline, without enum converter, we get JsonException. - Assert.Throws(() => JsonSerializer.Deserialize(json)); + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper(json)); - var obj = JsonSerializer.Deserialize(json, options); + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); Assert.Equal(MySmallEnum.AnotherValue, obj.GetMyEnum); Assert.Equal(25, obj.MyInt); // ConverterForInt32 throws this exception. - Assert.Throws(() => JsonSerializer.Serialize(obj, options)); + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(obj, options)); } - private struct StructWithPropertiesWithConverter + public struct StructWithPropertiesWithConverter { [JsonInclude] public MySmallEnum MyEnum { private get; set; } @@ -169,23 +174,23 @@ private struct StructWithPropertiesWithConverter internal MySmallEnum GetMyEnum => MyEnum; } - private enum MySmallEnum + public enum MySmallEnum { DefaultValue = 0, AnotherValue = 1 } [Fact] - public static void HonorCaseInsensitivity() + public async Task HonorCaseInsensitivity() { var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; string json = @"{""MYSTRING"":""Hello""}"; - Assert.Null(JsonSerializer.Deserialize(json).MyString); - Assert.Equal("Hello", JsonSerializer.Deserialize(json, options).MyString); + Assert.Null((await JsonSerializerWrapperForString.DeserializeWrapper(json)).MyString); + Assert.Equal("Hello", (await JsonSerializerWrapperForString.DeserializeWrapper(json, options)).MyString); } - private struct MyStruct_WithNonPublicAccessors_WithTypeAttribute + public struct MyStruct_WithNonPublicAccessors_WithTypeAttribute { [JsonInclude] public int MyInt { get; private set; } @@ -201,30 +206,30 @@ private struct MyStruct_WithNonPublicAccessors_WithTypeAttribute } [Fact] - public static void HonorNamingPolicy() + public async Task HonorNamingPolicy() { var options = new JsonSerializerOptions { PropertyNamingPolicy = new SimpleSnakeCasePolicy() }; string json = @"{""my_string"":""Hello""}"; - Assert.Null(JsonSerializer.Deserialize(json).MyString); - Assert.Equal("Hello", JsonSerializer.Deserialize(json, options).MyString); + Assert.Null((await JsonSerializerWrapperForString.DeserializeWrapper(json)).MyString); + Assert.Equal("Hello", (await JsonSerializerWrapperForString.DeserializeWrapper(json, options)).MyString); } [Fact] - public static void HonorJsonPropertyName() + public virtual async Task HonorJsonPropertyName() { string json = @"{""prop1"":1,""prop2"":2}"; - var obj = JsonSerializer.Deserialize(json); + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); Assert.Equal(MySmallEnum.AnotherValue, obj.GetMyEnum); Assert.Equal(2, obj.MyInt); - json = JsonSerializer.Serialize(obj); + json = await JsonSerializerWrapperForString.SerializeWrapper(obj); Assert.Contains(@"""prop1"":1", json); Assert.Contains(@"""prop2"":2", json); } - private struct StructWithPropertiesWithJsonPropertyName + public struct StructWithPropertiesWithJsonPropertyName { [JsonInclude] [JsonPropertyName("prop1")] @@ -239,9 +244,13 @@ private struct StructWithPropertiesWithJsonPropertyName } [Fact] - public static void Map_JsonSerializableProperties_ToCtorArgs() +#if BUILDING_SOURCE_GENERATOR_TESTS + // Needs support for parameterized ctors. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task Map_JsonSerializableProperties_ToCtorArgs() { - var obj = JsonSerializer.Deserialize(@"{""X"":1,""Y"":2}"); + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(@"{""X"":1,""Y"":2}"); Assert.Equal(1, obj.X); Assert.Equal(2, obj.GetY); } @@ -260,24 +269,24 @@ private struct PointWith_JsonSerializableProperties } [Fact] - public static void Public_And_NonPublicPropertyAccessors_PropertyAttributes() + public virtual async Task Public_And_NonPublicPropertyAccessors_PropertyAttributes() { string json = @"{""W"":1,""X"":2,""Y"":3,""Z"":4}"; - var obj = JsonSerializer.Deserialize(json); + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); Assert.Equal(1, obj.W); Assert.Equal(2, obj.X); Assert.Equal(3, obj.Y); Assert.Equal(4, obj.GetZ); - json = JsonSerializer.Serialize(obj); + json = await JsonSerializerWrapperForString.SerializeWrapper(obj); Assert.Contains(@"""W"":1", json); Assert.Contains(@"""X"":2", json); Assert.Contains(@"""Y"":3", json); Assert.Contains(@"""Z"":4", json); } - private class ClassWithMixedPropertyAccessors_PropertyAttributes + public class ClassWithMixedPropertyAccessors_PropertyAttributes { [JsonInclude] public int W { get; set; } @@ -301,40 +310,40 @@ private class ClassWithMixedPropertyAccessors_PropertyAttributes [InlineData(typeof(ClassWithPrivate_InitOnlyProperty_WithJsonIncludeProperty))] [InlineData(typeof(ClassWithInternal_InitOnlyProperty_WithJsonIncludeProperty))] [InlineData(typeof(ClassWithProtected_InitOnlyProperty_WithJsonIncludeProperty))] - public static void NonPublicProperty_WithJsonInclude_Invalid(Type type) + public virtual async Task NonPublicProperty_WithJsonInclude_Invalid(Type type) { - InvalidOperationException ex = Assert.Throws(() => JsonSerializer.Deserialize("", type)); + InvalidOperationException ex = await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("{}", type)); string exAsStr = ex.ToString(); Assert.Contains("MyString", exAsStr); Assert.Contains(type.ToString(), exAsStr); Assert.Contains("JsonIncludeAttribute", exAsStr); - ex = Assert.Throws(() => JsonSerializer.Serialize(Activator.CreateInstance(type), type)); + ex = await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(Activator.CreateInstance(type), type)); exAsStr = ex.ToString(); Assert.Contains("MyString", exAsStr); Assert.Contains(type.ToString(), exAsStr); Assert.Contains("JsonIncludeAttribute", exAsStr); } - private class ClassWithPrivateProperty_WithJsonIncludeProperty + public class ClassWithPrivateProperty_WithJsonIncludeProperty { [JsonInclude] private string MyString { get; set; } } - private class ClassWithInternalProperty_WithJsonIncludeProperty + public class ClassWithInternalProperty_WithJsonIncludeProperty { [JsonInclude] internal string MyString { get; } } - private class ClassWithProtectedProperty_WithJsonIncludeProperty + public class ClassWithProtectedProperty_WithJsonIncludeProperty { [JsonInclude] protected string MyString { get; private set; } } - private class ClassWithPrivateField_WithJsonIncludeProperty + public class ClassWithPrivateField_WithJsonIncludeProperty { [JsonInclude] private string MyString = null; @@ -342,31 +351,31 @@ private class ClassWithPrivateField_WithJsonIncludeProperty public override string ToString() => MyString; } - private class ClassWithInternalField_WithJsonIncludeProperty + public class ClassWithInternalField_WithJsonIncludeProperty { [JsonInclude] internal string MyString = null; } - private class ClassWithProtectedField_WithJsonIncludeProperty + public class ClassWithProtectedField_WithJsonIncludeProperty { [JsonInclude] protected string MyString = null; } - private class ClassWithPrivate_InitOnlyProperty_WithJsonIncludeProperty + public class ClassWithPrivate_InitOnlyProperty_WithJsonIncludeProperty { [JsonInclude] private string MyString { get; init; } } - private class ClassWithInternal_InitOnlyProperty_WithJsonIncludeProperty + public class ClassWithInternal_InitOnlyProperty_WithJsonIncludeProperty { [JsonInclude] internal string MyString { get; init; } } - private class ClassWithProtected_InitOnlyProperty_WithJsonIncludeProperty + public class ClassWithProtected_InitOnlyProperty_WithJsonIncludeProperty { [JsonInclude] protected string MyString { get; init; } diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs new file mode 100644 index 0000000000000..32f19eb120c39 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs @@ -0,0 +1,2632 @@ +// 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.Generic; +using System.Collections.Concurrent; +using System.Numerics; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public abstract partial class PropertyVisibilityTests : SerializerTests + { + public PropertyVisibilityTests(JsonSerializerWrapperForString serializerWrapper) : base(serializerWrapper) { } + + [Fact] + public async Task Serialize_NewSlotPublicField() + { + // Serialize + var obj = new ClassWithNewSlotField(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Equal(@"{""MyString"":""NewDefaultValue""}", json); + + // Deserialize + json = @"{""MyString"":""NewValue""}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal("NewValue", ((ClassWithNewSlotField)obj).MyString); + Assert.Equal("DefaultValue", ((ClassWithInternalField)obj).MyString); + } + + [Fact] + public async Task Serialize_NewSlotPublicProperty() + { + // Serialize + var obj = new ClassWithNewSlotProperty(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Equal(@"{""MyString"":""NewDefaultValue""}", json); + + // Deserialize + json = @"{""MyString"":""NewValue""}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal("NewValue", ((ClassWithNewSlotProperty)obj).MyString); + Assert.Equal("DefaultValue", ((ClassWithInternalProperty)obj).MyString); + } + + [Fact] + public async Task Serialize_BasePublicProperty_ConflictWithDerivedPrivate() + { + // Serialize + var obj = new ClassWithNewSlotInternalProperty(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Equal(@"{""MyString"":""DefaultValue""}", json); + + // Deserialize + json = @"{""MyString"":""NewValue""}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal("NewValue", ((ClassWithPublicProperty)obj).MyString); + Assert.Equal("NewDefaultValue", ((ClassWithNewSlotInternalProperty)obj).MyString); + } + + [Fact] + public async Task Serialize_PublicProperty_ConflictWithPrivateDueAttributes() + { + // Serialize + var obj = new ClassWithPropertyNamingConflict(); + + // Newtonsoft.Json throws JsonSerializationException here because + // non-public properties are included when [JsonProperty] is placed on them. + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Equal(@"{""MyString"":""DefaultValue""}", json); + + // Deserialize + json = @"{""MyString"":""NewValue""}"; + + // Newtonsoft.Json throws JsonSerializationException here because + // non-public properties are included when [JsonProperty] is placed on them. + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal("NewValue", obj.MyString); + Assert.Equal("ConflictingValue", obj.ConflictingString); + } + + [Fact] + public async Task Serialize_PublicProperty_ConflictWithPrivateDuePolicy() + { + var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + // Serialize + var obj = new ClassWithPropertyPolicyConflict(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + + Assert.Equal(@"{""myString"":""DefaultValue""}", json); + + // Deserialize + json = @"{""myString"":""NewValue""}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + + Assert.Equal("NewValue", obj.MyString); + Assert.Equal("ConflictingValue", obj.myString); + } + + [Fact] + public async Task Serialize_NewSlotPublicProperty_ConflictWithBasePublicProperty() + { + // Serialize + var obj = new ClassWithNewSlotDecimalProperty(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Equal(@"{""MyNumeric"":1.5}", json); + + // Deserialize + json = @"{""MyNumeric"":2.5}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal(2.5M, obj.MyNumeric); + } + + [Fact] + public async Task Serialize_NewSlotPublicField_ConflictWithBasePublicProperty() + { + // Serialize + var obj = new ClassWithNewSlotDecimalField(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Equal(@"{""MyNumeric"":1.5}", json); + + // Deserialize + json = @"{""MyNumeric"":2.5}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal(2.5M, obj.MyNumeric); + } + + [Fact] + public async Task Serialize_NewSlotPublicField_SpecifiedJsonPropertyName() + { + // Serialize + var obj = new ClassWithNewSlotAttributedDecimalField(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Contains(@"""MyNewNumeric"":1.5", json); + Assert.Contains(@"""MyNumeric"":1", json); + + // Deserialize + json = @"{""MyNewNumeric"":2.5,""MyNumeric"":4}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal(4, ((ClassWithHiddenByNewSlotIntProperty)obj).MyNumeric); + Assert.Equal(2.5M, ((ClassWithNewSlotAttributedDecimalField)obj).MyNumeric); + } + + [Fact] + public async Task Serialize_NewSlotPublicProperty_SpecifiedJsonPropertyName() + { + // Serialize + var obj = new ClassWithNewSlotAttributedDecimalProperty(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Contains(@"""MyNewNumeric"":1.5", json); + Assert.Contains(@"""MyNumeric"":1", json); + + // Deserialize + json = @"{""MyNewNumeric"":2.5,""MyNumeric"":4}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal(4, ((ClassWithHiddenByNewSlotIntProperty)obj).MyNumeric); + Assert.Equal(2.5M, ((ClassWithNewSlotAttributedDecimalProperty)obj).MyNumeric); + } + + [Fact] + public async Task Ignore_NonPublicProperty() + { + // Serialize + var obj = new ClassWithInternalProperty(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Equal(@"{}", json); + + // Deserialize + json = @"{""MyString"":""NewValue""}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal("DefaultValue", obj.MyString); + } + + [Fact] + public async Task Ignore_NewSlotPublicFieldIgnored() + { + // Serialize + var obj = new ClassWithIgnoredNewSlotField(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Equal(@"{}", json); + + // Deserialize + json = @"{""MyString"":""NewValue""}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal("NewDefaultValue", ((ClassWithIgnoredNewSlotField)obj).MyString); + Assert.Equal("DefaultValue", ((ClassWithInternalField)obj).MyString); + } + + [Fact] + public async Task Ignore_NewSlotPublicPropertyIgnored() + { + // Serialize + var obj = new ClassWithIgnoredNewSlotProperty(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Equal(@"{}", json); + + // Deserialize + json = @"{""MyString"":""NewValue""}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal("NewDefaultValue", ((ClassWithIgnoredNewSlotProperty)obj).MyString); + Assert.Equal("DefaultValue", ((ClassWithInternalProperty)obj).MyString); + } + + [Fact] + public async Task Ignore_BasePublicPropertyIgnored_ConflictWithDerivedPrivate() + { + // Serialize + var obj = new ClassWithIgnoredPublicPropertyAndNewSlotPrivate(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Equal(@"{}", json); + + // Deserialize + json = @"{""MyString"":""NewValue""}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal("DefaultValue", ((ClassWithIgnoredPublicProperty)obj).MyString); + Assert.Equal("NewDefaultValue", ((ClassWithIgnoredPublicPropertyAndNewSlotPrivate)obj).MyString); + } + + [Fact] + public async Task Ignore_PublicProperty_ConflictWithPrivateDueAttributes() + { + // Serialize + var obj = new ClassWithIgnoredPropertyNamingConflictPrivate(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Equal(@"{}", json); + + // Newtonsoft.Json has the following output because non-public properties are included when [JsonProperty] is placed on them. + // {"MyString":"ConflictingValue"} + + // Deserialize + json = @"{""MyString"":""NewValue""}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal("DefaultValue", obj.MyString); + Assert.Equal("ConflictingValue", obj.ConflictingString); + + // The output for Newtonsoft.Json is: + // obj.ConflictingString = "NewValue" + // obj.MyString still equals "DefaultValue" + } + + [Fact] + public async Task Ignore_PublicProperty_ConflictWithPrivateDuePolicy() + { + var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + // Serialize + var obj = new ClassWithIgnoredPropertyPolicyConflictPrivate(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + + Assert.Equal(@"{}", json); + + // Deserialize + json = @"{""myString"":""NewValue""}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + + Assert.Equal("DefaultValue", obj.MyString); + Assert.Equal("ConflictingValue", obj.myString); + } + + [Fact] + public async Task Ignore_PublicProperty_ConflictWithPublicDueAttributes() + { + // Serialize + var obj = new ClassWithIgnoredPropertyNamingConflictPublic(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + + Assert.Equal(@"{""MyString"":""ConflictingValue""}", json); + + // Deserialize + json = @"{""MyString"":""NewValue""}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal("DefaultValue", obj.MyString); + Assert.Equal("NewValue", obj.ConflictingString); + } + + [Fact] + public async Task Ignore_PublicProperty_ConflictWithPublicDuePolicy() + { + var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + // Serialize + var obj = new ClassWithIgnoredPropertyPolicyConflictPublic(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + + Assert.Equal(@"{""myString"":""ConflictingValue""}", json); + + // Deserialize + json = @"{""myString"":""NewValue""}"; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + + Assert.Equal("DefaultValue", obj.MyString); + Assert.Equal("NewValue", obj.myString); + } + + [Fact] + public async Task Throw_PublicProperty_ConflictDueAttributes() + { + // Serialize + var obj = new ClassWithPropertyNamingConflictWhichThrows(); + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.SerializeWrapper(obj)); + + // Deserialize + string json = @"{""MyString"":""NewValue""}"; + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.DeserializeWrapper(json)); + } + + [Fact] + public async Task Throw_PublicPropertyAndField_ConflictDueAttributes() + { + // Serialize + var obj = new ClassWithPropertyFieldNamingConflictWhichThrows(); + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.SerializeWrapper(obj)); + + // Deserialize + string json = @"{""MyString"":""NewValue""}"; + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.DeserializeWrapper(json)); + } + + [Fact] + public async Task Throw_PublicProperty_ConflictDueAttributes_SingleInheritance() + { + // Serialize + var obj = new ClassInheritedWithPropertyNamingConflictWhichThrows(); + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.SerializeWrapper(obj)); + + // The output for Newtonsoft.Json is: + // {"MyString":"ConflictingValue"} + // Conflicts at different type-hierarchy levels that are not caused by + // deriving or the new keyword are allowed. Properties on more derived types win. + + // Deserialize + string json = @"{""MyString"":""NewValue""}"; + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.DeserializeWrapper(json)); + + // The output for Newtonsoft.Json is: + // obj.ConflictingString = "NewValue" + // obj.MyString still equals "DefaultValue" + } + + [Fact] + public async Task Throw_PublicPropertyAndField_ConflictDueAttributes_SingleInheritance() + { + // Serialize + var obj = new ClassInheritedWithPropertyFieldNamingConflictWhichThrows(); + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.SerializeWrapper(obj)); + + // The output for Newtonsoft.Json is: + // {"MyString":"ConflictingValue"} + // Conflicts at different type-hierarchy levels that are not caused by + // deriving or the new keyword are allowed. Properties on more derived types win. + + // Deserialize + string json = @"{""MyString"":""NewValue""}"; + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.DeserializeWrapper(json)); + + // The output for Newtonsoft.Json is: + // obj.ConflictingString = "NewValue" + // obj.MyString still equals "DefaultValue" + } + + [Fact] + public async Task Throw_PublicProperty_ConflictDueAttributes_DoubleInheritance() + { + // Serialize + var obj = new ClassTwiceInheritedWithPropertyNamingConflictWhichThrows(); + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.SerializeWrapper(obj)); + + // The output for Newtonsoft.Json is: + // {"MyString":"ConflictingValue"} + // Conflicts at different type-hierarchy levels that are not caused by + // deriving or the new keyword are allowed. Properties on more derived types win. + + // Deserialize + string json = @"{""MyString"":""NewValue""}"; + + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.DeserializeWrapper(json)); + + // The output for Newtonsoft.Json is: + // obj.ConflictingString = "NewValue" + // obj.MyString still equals "DefaultValue" + } + + [Fact] + public async Task Throw_PublicPropertyAndField_ConflictDueAttributes_DoubleInheritance() + { + // Serialize + var obj = new ClassTwiceInheritedWithPropertyFieldNamingConflictWhichThrows(); + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.SerializeWrapper(obj)); + + // The output for Newtonsoft.Json is: + // {"MyString":"ConflictingValue"} + // Conflicts at different type-hierarchy levels that are not caused by + // deriving or the new keyword are allowed. Properties on more derived types win. + + // Deserialize + string json = @"{""MyString"":""NewValue""}"; + + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.DeserializeWrapper(json)); + + // The output for Newtonsoft.Json is: + // obj.ConflictingString = "NewValue" + // obj.MyString still equals "DefaultValue" + } + + [Fact] + public async Task Throw_PublicProperty_ConflictDuePolicy() + { + var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + // Serialize + var obj = new ClassWithPropertyPolicyConflictWhichThrows(); + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.SerializeWrapper(obj, options)); + + // Deserialize + string json = @"{""MyString"":""NewValue""}"; + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.DeserializeWrapper(json, options)); + } + + [Fact] + public async Task Throw_PublicPropertyAndField_ConflictDuePolicy() + { + var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + // Serialize + var obj = new ClassWithPropertyFieldPolicyConflictWhichThrows(); + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.SerializeWrapper(obj, options)); + + // Deserialize + string json = @"{""MyString"":""NewValue""}"; + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.DeserializeWrapper(json, options)); + } + + [Fact] + public async Task Throw_PublicProperty_ConflictDuePolicy_SingleInheritance() + { + var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + // Serialize + var obj = new ClassInheritedWithPropertyPolicyConflictWhichThrows(); + + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.SerializeWrapper(obj, options)); + + // The output for Newtonsoft.Json is: + // {"myString":"ConflictingValue"} + // Conflicts at different type-hierarchy levels that are not caused by + // deriving or the new keyword are allowed. Properties on more derived types win. + + // Deserialize + string json = @"{""MyString"":""NewValue""}"; + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.DeserializeWrapper(json, options)); + + // The output for Newtonsoft.Json is: + // obj.myString = "NewValue" + // obj.MyString still equals "DefaultValue" + } + + [Fact] + public async Task Throw_PublicPropertyAndField_ConflictDuePolicy_SingleInheritance() + { + var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + // Serialize + var obj = new ClassInheritedWithPropertyFieldPolicyConflictWhichThrows(); + + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.SerializeWrapper(obj, options)); + + // The output for Newtonsoft.Json is: + // {"myString":"ConflictingValue"} + // Conflicts at different type-hierarchy levels that are not caused by + // deriving or the new keyword are allowed. Properties on more derived types win. + + // Deserialize + string json = @"{""MyString"":""NewValue""}"; + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.DeserializeWrapper(json, options)); + + // The output for Newtonsoft.Json is: + // obj.myString = "NewValue" + // obj.MyString still equals "DefaultValue" + } + + [Fact] + public async Task Throw_PublicProperty_ConflictDuePolicy_DobuleInheritance() + { + var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + // Serialize + var obj = new ClassTwiceInheritedWithPropertyPolicyConflictWhichThrows(); + + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.SerializeWrapper(obj, options)); + + // The output for Newtonsoft.Json is: + // {"myString":"ConflictingValue"} + // Conflicts at different type-hierarchy levels that are not caused by + // deriving or the new keyword are allowed. Properties on more derived types win. + + // Deserialize + string json = @"{""MyString"":""NewValue""}"; + + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.DeserializeWrapper(json, options)); + + // The output for Newtonsoft.Json is: + // obj.myString = "NewValue" + // obj.MyString still equals "DefaultValue" + } + + [Fact] + public async Task Throw_PublicPropertyAndField_ConflictDuePolicy_DobuleInheritance() + { + var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + // Serialize + var obj = new ClassTwiceInheritedWithPropertyFieldPolicyConflictWhichThrows(); + + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.SerializeWrapper(obj, options)); + + // The output for Newtonsoft.Json is: + // {"myString":"ConflictingValue"} + // Conflicts at different type-hierarchy levels that are not caused by + // deriving or the new keyword are allowed. Properties on more derived types win. + + // Deserialize + string json = @"{""MyString"":""NewValue""}"; + + await Assert.ThrowsAsync( + async () => await JsonSerializerWrapperForString.DeserializeWrapper(json, options)); + + // The output for Newtonsoft.Json is: + // obj.myString = "NewValue" + // obj.MyString still equals "DefaultValue" + } + + [Fact] + public async Task HiddenPropertiesIgnored_WhenOverridesIgnored() + { + string serialized = await JsonSerializerWrapperForString.SerializeWrapper(new DerivedClass_With_IgnoredOverride()); + Assert.Equal(@"{}", serialized); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(new DerivedClass_WithVisibleProperty_Of_DerivedClass_With_IgnoredOverride()); + Assert.Equal(@"{""MyProp"":false}", serialized); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(new DerivedClass_With_IgnoredOverride_And_ConflictingPropertyName()); + Assert.Equal(@"{""MyProp"":null}", serialized); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(new DerivedClass_With_Ignored_NewProperty()); + Assert.Equal(@"{""MyProp"":false}", serialized); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(new DerivedClass_WithConflictingNewMember()); + Assert.Equal(@"{""MyProp"":false}", serialized); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(new DerivedClass_WithConflictingNewMember_Of_DifferentType()); + Assert.Equal(@"{""MyProp"":0}", serialized); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(new DerivedClass_With_Ignored_ConflictingNewMember()); + Assert.Equal(@"{""MyProp"":false}", serialized); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(new DerivedClass_With_Ignored_ConflictingNewMember_Of_DifferentType()); + Assert.Equal(@"{""MyProp"":false}", serialized); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(new DerivedClass_With_NewProperty_And_ConflictingPropertyName()); + Assert.Equal(@"{""MyProp"":null}", serialized); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(new DerivedClass_With_Ignored_NewProperty_Of_DifferentType()); + Assert.Equal(@"{""MyProp"":false}", serialized); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(new DerivedClass_With_Ignored_NewProperty_Of_DifferentType_And_ConflictingPropertyName()); + Assert.Equal(@"{""MyProp"":null}", serialized); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(new FurtherDerivedClass_With_ConflictingPropertyName()); + Assert.Equal(@"{""MyProp"":null}", serialized); + + // Here we differ from Newtonsoft.Json, where the output would be + // {"MyProp":null} + // Conflicts at different type-hierarchy levels that are not caused by + // deriving or the new keyword are allowed. Properties on more derived types win. + // This is invalid in System.Text.Json. + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(new DerivedClass_WithConflictingPropertyName())); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(new FurtherDerivedClass_With_IgnoredOverride()); + Assert.Equal(@"{""MyProp"":null}", serialized); + } + + public class ClassWithInternalField + { + internal string MyString = "DefaultValue"; + } + + public class ClassWithNewSlotField : ClassWithInternalField + { + [JsonInclude] + public new string MyString = "NewDefaultValue"; + } + + public class ClassWithInternalProperty + { + internal string MyString { get; set; } = "DefaultValue"; + } + + public class ClassWithNewSlotProperty : ClassWithInternalProperty + { + public new string MyString { get; set; } = "NewDefaultValue"; + } + + public class ClassWithPublicProperty + { + public string MyString { get; set; } = "DefaultValue"; + } + + public class ClassWithNewSlotInternalProperty : ClassWithPublicProperty + { + internal new string MyString { get; set; } = "NewDefaultValue"; + } + + public class ClassWithPropertyNamingConflict + { + public string MyString { get; set; } = "DefaultValue"; + + [JsonPropertyName(nameof(MyString))] + internal string ConflictingString { get; set; } = "ConflictingValue"; + } + + public class ClassWithPropertyNamingConflictWhichThrows + { + public string MyString { get; set; } = "DefaultValue"; + + [JsonPropertyName(nameof(MyString))] + public string ConflictingString { get; set; } = "ConflictingValue"; + } + + public class ClassWithPropertyFieldNamingConflictWhichThrows + { + public string MyString { get; set; } = "DefaultValue"; + + [JsonInclude] + [JsonPropertyName(nameof(MyString))] + public string ConflictingString = "ConflictingValue"; + } + + public class ClassInheritedWithPropertyNamingConflictWhichThrows : ClassWithPublicProperty + { + [JsonPropertyName(nameof(MyString))] + public string ConflictingString { get; set; } = "ConflictingValue"; + } + + public class ClassInheritedWithPropertyFieldNamingConflictWhichThrows : ClassWithPublicProperty + { + [JsonInclude] + [JsonPropertyName(nameof(MyString))] + public string ConflictingString = "ConflictingValue"; + } + + public class ClassTwiceInheritedWithPropertyNamingConflictWhichThrowsDummy : ClassWithPublicProperty + { + } + + public class ClassTwiceInheritedWithPropertyNamingConflictWhichThrows : ClassTwiceInheritedWithPropertyNamingConflictWhichThrowsDummy + { + [JsonPropertyName(nameof(MyString))] + public string ConflictingString { get; set; } = "ConflictingValue"; + } + + public class ClassTwiceInheritedWithPropertyFieldNamingConflictWhichThrows : ClassTwiceInheritedWithPropertyNamingConflictWhichThrowsDummy + { + [JsonInclude] + [JsonPropertyName(nameof(MyString))] + public string ConflictingString = "ConflictingValue"; + } + + public class ClassWithPropertyPolicyConflict + { + public string MyString { get; set; } = "DefaultValue"; + + internal string myString { get; set; } = "ConflictingValue"; + } + + public class ClassWithPropertyPolicyConflictWhichThrows + { + public string MyString { get; set; } = "DefaultValue"; + + public string myString { get; set; } = "ConflictingValue"; + } + + public class ClassWithPropertyFieldPolicyConflictWhichThrows + { + public string MyString { get; set; } = "DefaultValue"; + + [JsonInclude] + public string myString = "ConflictingValue"; + } + + public class ClassInheritedWithPropertyPolicyConflictWhichThrows : ClassWithPublicProperty + { + public string myString { get; set; } = "ConflictingValue"; + } + + public class ClassInheritedWithPropertyFieldPolicyConflictWhichThrows : ClassWithPublicProperty + { + [JsonInclude] + public string myString = "ConflictingValue"; + } + + public class ClassInheritedWithPropertyPolicyConflictWhichThrowsDummy : ClassWithPublicProperty + { + } + + public class ClassTwiceInheritedWithPropertyPolicyConflictWhichThrows : ClassInheritedWithPropertyPolicyConflictWhichThrowsDummy + { + public string myString { get; set; } = "ConflictingValue"; + } + + public class ClassTwiceInheritedWithPropertyFieldPolicyConflictWhichThrows : ClassInheritedWithPropertyPolicyConflictWhichThrowsDummy + { + [JsonInclude] + public string myString { get; set; } = "ConflictingValue"; + } + + public class ClassWithIgnoredNewSlotField : ClassWithInternalField + { + [JsonIgnore] + public new string MyString = "NewDefaultValue"; + } + + public class ClassWithIgnoredNewSlotProperty : ClassWithInternalProperty + { + [JsonIgnore] + public new string MyString { get; set; } = "NewDefaultValue"; + } + + public class ClassWithIgnoredPublicProperty + { + [JsonIgnore] + public string MyString { get; set; } = "DefaultValue"; + } + + public class ClassWithIgnoredPublicPropertyAndNewSlotPrivate : ClassWithIgnoredPublicProperty + { + internal new string MyString { get; set; } = "NewDefaultValue"; + } + + public class ClassWithIgnoredPropertyNamingConflictPrivate + { + [JsonIgnore] + public string MyString { get; set; } = "DefaultValue"; + + [JsonPropertyName(nameof(MyString))] + internal string ConflictingString { get; set; } = "ConflictingValue"; + } + + public class ClassWithIgnoredPropertyPolicyConflictPrivate + { + [JsonIgnore] + public string MyString { get; set; } = "DefaultValue"; + + internal string myString { get; set; } = "ConflictingValue"; + } + + public class ClassWithIgnoredPropertyNamingConflictPublic + { + [JsonIgnore] + public string MyString { get; set; } = "DefaultValue"; + + [JsonPropertyName(nameof(MyString))] + public string ConflictingString { get; set; } = "ConflictingValue"; + } + + public class ClassWithIgnoredPropertyPolicyConflictPublic + { + [JsonIgnore] + public string MyString { get; set; } = "DefaultValue"; + + public string myString { get; set; } = "ConflictingValue"; + } + + public class ClassWithHiddenByNewSlotIntProperty + { + public int MyNumeric { get; set; } = 1; + } + + public class ClassWithNewSlotDecimalField : ClassWithHiddenByNewSlotIntProperty + { + [JsonInclude] + public new decimal MyNumeric = 1.5M; + } + + public class ClassWithNewSlotDecimalProperty : ClassWithHiddenByNewSlotIntProperty + { + public new decimal MyNumeric { get; set; } = 1.5M; + } + + public class ClassWithNewSlotAttributedDecimalField : ClassWithHiddenByNewSlotIntProperty + { + [JsonInclude] + [JsonPropertyName("MyNewNumeric")] + public new decimal MyNumeric = 1.5M; + } + + public class ClassWithNewSlotAttributedDecimalProperty : ClassWithHiddenByNewSlotIntProperty + { + [JsonPropertyName("MyNewNumeric")] + public new decimal MyNumeric { get; set; } = 1.5M; + } + + public class Class_With_VirtualProperty + { + public virtual bool MyProp { get; set; } + } + + public class DerivedClass_With_IgnoredOverride : Class_With_VirtualProperty + { + [JsonIgnore] + public override bool MyProp { get; set; } + } + + public class DerivedClass_WithVisibleProperty_Of_DerivedClass_With_IgnoredOverride : DerivedClass_With_IgnoredOverride + { + public override bool MyProp { get; set; } + } + + public class DerivedClass_With_IgnoredOverride_And_ConflictingPropertyName : Class_With_VirtualProperty + { + [JsonPropertyName("MyProp")] + public string MyString { get; set; } + + [JsonIgnore] + public override bool MyProp { get; set; } + } + + public class Class_With_Property + { + public bool MyProp { get; set; } + } + + public class DerivedClass_With_Ignored_NewProperty : Class_With_Property + { + [JsonIgnore] + public new bool MyProp { get; set; } + } + + public class DerivedClass_With_NewProperty_And_ConflictingPropertyName : Class_With_Property + { + [JsonPropertyName("MyProp")] + public string MyString { get; set; } + + [JsonIgnore] + public new bool MyProp { get; set; } + } + + public class DerivedClass_With_Ignored_NewProperty_Of_DifferentType : Class_With_Property + { + [JsonIgnore] + public new int MyProp { get; set; } + } + + public class DerivedClass_With_Ignored_NewProperty_Of_DifferentType_And_ConflictingPropertyName : Class_With_Property + { + [JsonPropertyName("MyProp")] + public string MyString { get; set; } + + [JsonIgnore] + public new int MyProp { get; set; } + } + + public class DerivedClass_WithIgnoredOverride : Class_With_VirtualProperty + { + [JsonIgnore] + public override bool MyProp { get; set; } + } + + public class DerivedClass_WithConflictingNewMember : Class_With_VirtualProperty + { + public new bool MyProp { get; set; } + } + + public class DerivedClass_WithConflictingNewMember_Of_DifferentType : Class_With_VirtualProperty + { + public new int MyProp { get; set; } + } + + public class DerivedClass_With_Ignored_ConflictingNewMember : Class_With_VirtualProperty + { + [JsonIgnore] + public new bool MyProp { get; set; } + } + + public class DerivedClass_With_Ignored_ConflictingNewMember_Of_DifferentType : Class_With_VirtualProperty + { + [JsonIgnore] + public new int MyProp { get; set; } + } + + public class FurtherDerivedClass_With_ConflictingPropertyName : DerivedClass_WithIgnoredOverride + { + [JsonPropertyName("MyProp")] + public string MyString { get; set; } + } + + public class DerivedClass_WithConflictingPropertyName : Class_With_VirtualProperty + { + [JsonPropertyName("MyProp")] + public string MyString { get; set; } + } + + public class FurtherDerivedClass_With_IgnoredOverride : DerivedClass_WithConflictingPropertyName + { + [JsonIgnore] + public override bool MyProp { get; set; } + } + + [Fact] + public async Task IgnoreReadOnlyProperties() + { + var options = new JsonSerializerOptions(); + options.IgnoreReadOnlyProperties = true; + + var obj = new ClassWithNoSetter(); + + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + + // Collections are always serialized unless they have [JsonIgnore]. + Assert.Equal(@"{""MyInts"":[1,2]}", json); + } + + [Fact] + public async Task IgnoreReadOnlyFields() + { + var options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.IgnoreReadOnlyFields = true; + + var obj = new ClassWithReadOnlyFields(); + + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + + // Collections are always serialized unless they have [JsonIgnore]. + Assert.Equal(@"{""MyInts"":[1,2]}", json); + } + + [Fact] + public async Task NoSetter() + { + var obj = new ClassWithNoSetter(); + + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Contains(@"""MyString"":""DefaultValue""", json); + Assert.Contains(@"""MyInts"":[1,2]", json); + + obj = await JsonSerializerWrapperForString.DeserializeWrapper(@"{""MyString"":""IgnoreMe"",""MyInts"":[0]}"); + Assert.Equal("DefaultValue", obj.MyString); + Assert.Equal(2, obj.MyInts.Length); + } + + [Fact] + public async Task NoGetter() + { + ClassWithNoGetter objWithNoGetter = await JsonSerializerWrapperForString.DeserializeWrapper( + @"{""MyString"":""Hello"",""MyIntArray"":[0],""MyIntList"":[0]}"); + + Assert.Equal("Hello", objWithNoGetter.GetMyString()); + + // Currently we don't support setters without getters. + Assert.Equal(0, objWithNoGetter.GetMyIntArray().Length); + Assert.Equal(0, objWithNoGetter.GetMyIntList().Count); + } + + [Fact] + public async Task PrivateGetter() + { + var obj = new ClassWithPrivateSetterAndGetter(); + obj.SetMyString("Hello"); + + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Equal(@"{}", json); + } + + [Fact] + public async Task PrivateSetter() + { + string json = @"{""MyString"":""Hello""}"; + + ClassWithPrivateSetterAndGetter objCopy = await JsonSerializerWrapperForString.DeserializeWrapper(json); + Assert.Null(objCopy.GetMyString()); + } + + [Fact] + public async Task PrivateSetterPublicGetter() + { + // https://github.com/dotnet/runtime/issues/29503 + ClassWithPublicGetterAndPrivateSetter obj + = await JsonSerializerWrapperForString.DeserializeWrapper(@"{ ""Class"": {} }"); + + Assert.NotNull(obj); + Assert.Null(obj.Class); + } + + [Fact] + public async Task MissingObjectProperty() + { + ClassWithMissingObjectProperty obj + = await JsonSerializerWrapperForString.DeserializeWrapper(@"{ ""Object"": {} }"); + + Assert.Null(obj.Collection); + } + + [Fact] + public async Task MissingCollectionProperty() + { + ClassWithMissingCollectionProperty obj + = await JsonSerializerWrapperForString.DeserializeWrapper(@"{ ""Collection"": [] }"); + + Assert.Null(obj.Object); + } + + public class ClassWithPublicGetterAndPrivateSetter + { + public NestedClass Class { get; private set; } + } + + public class NestedClass + { + } + + [Fact] + public async Task JsonIgnoreAttribute() + { + var options = new JsonSerializerOptions { IncludeFields = true }; + + // Verify default state. + var obj = new ClassWithIgnoreAttributeProperty(); + Assert.Equal(@"MyString", obj.MyString); + Assert.Equal(@"MyStringWithIgnore", obj.MyStringWithIgnore); + Assert.Equal(2, obj.MyStringsWithIgnore.Length); + Assert.Equal(1, obj.MyDictionaryWithIgnore["Key"]); + Assert.Equal(3.14M, obj.MyNumeric); + + // Verify serialize. + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + Assert.Contains(@"""MyString""", json); + Assert.DoesNotContain(@"MyStringWithIgnore", json); + Assert.DoesNotContain(@"MyStringsWithIgnore", json); + Assert.DoesNotContain(@"MyDictionaryWithIgnore", json); + Assert.DoesNotContain(@"MyNumeric", json); + + // Verify deserialize default. + obj = await JsonSerializerWrapperForString.DeserializeWrapper(@"{}", options); + Assert.Equal(@"MyString", obj.MyString); + Assert.Equal(@"MyStringWithIgnore", obj.MyStringWithIgnore); + Assert.Equal(2, obj.MyStringsWithIgnore.Length); + Assert.Equal(1, obj.MyDictionaryWithIgnore["Key"]); + Assert.Equal(3.14M, obj.MyNumeric); + + // Verify deserialize ignores the json for MyStringWithIgnore and MyStringsWithIgnore. + obj = await JsonSerializerWrapperForString.DeserializeWrapper( + @"{""MyString"":""Hello"", ""MyStringWithIgnore"":""IgnoreMe"", ""MyStringsWithIgnore"":[""IgnoreMe""], ""MyDictionaryWithIgnore"":{""Key"":9}, ""MyNumeric"": 2.71828}", options); + Assert.Contains(@"Hello", obj.MyString); + Assert.Equal(@"MyStringWithIgnore", obj.MyStringWithIgnore); + Assert.Equal(2, obj.MyStringsWithIgnore.Length); + Assert.Equal(1, obj.MyDictionaryWithIgnore["Key"]); + Assert.Equal(3.14M, obj.MyNumeric); + } + + [Fact] +#if BUILDING_SOURCE_GENERATOR_TESTS + // Needs support for more collections. + [ActiveIssue("https://github.com/dotnet/runtime/issues/53393")] +#endif + public async Task JsonIgnoreAttribute_UnsupportedCollection() + { + string json = + @"{ + ""MyConcurrentDict"":{ + ""key"":""value"" + }, + ""MyIDict"":{ + ""key"":""value"" + }, + ""MyDict"":{ + ""key"":""value"" + } + }"; + string wrapperJson = + @"{ + ""MyClass"":{ + ""MyConcurrentDict"":{ + ""key"":""value"" + }, + ""MyIDict"":{ + ""key"":""value"" + }, + ""MyDict"":{ + ""key"":""value"" + } + } + }"; + + // Unsupported collections will throw on deserialize by default. + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper(json)); + + // Using new options instance to prevent using previously cached metadata. + JsonSerializerOptions options = new JsonSerializerOptions(); + + // Unsupported collections will throw on serialize by default. + // Only when the collection contains elements. + + var dictionary = new Dictionary(); + // Uri is an unsupported dictionary key. + dictionary.Add(new Uri("http://foo"), "bar"); + + var concurrentDictionary = new ConcurrentDictionary(dictionary); + + var instance = new ClassWithUnsupportedDictionary() + { + MyConcurrentDict = concurrentDictionary, + MyIDict = dictionary + }; + + var instanceWithIgnore = new ClassWithIgnoredUnsupportedDictionary + { + MyConcurrentDict = concurrentDictionary, + MyIDict = dictionary + }; + + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(instance, options)); + + // Unsupported collections will throw on deserialize by default if they contain elements. + options = new JsonSerializerOptions(); + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper(wrapperJson, options)); + + options = new JsonSerializerOptions(); + // Unsupported collections will throw on serialize by default if they contain elements. + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(instance, options)); + + // When ignored, we can serialize and deserialize without exceptions. + options = new JsonSerializerOptions(); + + Assert.NotNull(await JsonSerializerWrapperForString.SerializeWrapper(instanceWithIgnore, options)); + + ClassWithIgnoredUnsupportedDictionary obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + Assert.Null(obj.MyDict); + + options = new JsonSerializerOptions(); + Assert.Equal("{}", await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithIgnoredUnsupportedDictionary())); + + options = new JsonSerializerOptions(); + WrapperForClassWithIgnoredUnsupportedDictionary wrapperObj = await JsonSerializerWrapperForString.DeserializeWrapper(wrapperJson, options); + Assert.Null(wrapperObj.MyClass.MyDict); + + options = new JsonSerializerOptions(); + Assert.Equal(@"{""MyClass"":{}}", await JsonSerializerWrapperForString.SerializeWrapper(new WrapperForClassWithIgnoredUnsupportedDictionary() + { + MyClass = new ClassWithIgnoredUnsupportedDictionary(), + }, options)); + } + + [Fact] + public async Task JsonIgnoreAttribute_UnsupportedBigInteger() + { + string json = @"{""MyBigInteger"":1}"; + string wrapperJson = @"{""MyClass"":{""MyBigInteger"":1}}"; + + // Unsupported types will throw by default. + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper(json)); + // Using new options instance to prevent using previously cached metadata. + JsonSerializerOptions options = new JsonSerializerOptions(); + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper(wrapperJson, options)); + + // When ignored, we can serialize and deserialize without exceptions. + options = new JsonSerializerOptions(); + ClassWithIgnoredUnsupportedBigInteger obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + Assert.Null(obj.MyBigInteger); + + options = new JsonSerializerOptions(); + Assert.Equal("{}", await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithIgnoredUnsupportedBigInteger())); + + options = new JsonSerializerOptions(); + WrapperForClassWithIgnoredUnsupportedBigInteger wrapperObj = await JsonSerializerWrapperForString.DeserializeWrapper(wrapperJson, options); + Assert.Null(wrapperObj.MyClass.MyBigInteger); + + options = new JsonSerializerOptions(); + Assert.Equal(@"{""MyClass"":{}}", await JsonSerializerWrapperForString.SerializeWrapper(new WrapperForClassWithIgnoredUnsupportedBigInteger() + { + MyClass = new ClassWithIgnoredUnsupportedBigInteger(), + }, options)); + } + + public class ObjectDictWrapper : Dictionary { } + + public class ClassWithUnsupportedDictionary + { + public ConcurrentDictionary MyConcurrentDict { get; set; } + public IDictionary MyIDict { get; set; } + public ObjectDictWrapper MyDict { get; set; } + } + + public class WrapperForClassWithUnsupportedDictionary + { + public ClassWithUnsupportedDictionary MyClass { get; set; } = new ClassWithUnsupportedDictionary(); + } + + public class ClassWithIgnoredUnsupportedDictionary + { + [JsonIgnore] + public ConcurrentDictionary MyConcurrentDict { get; set; } + [JsonIgnore] + public IDictionary MyIDict { get; set; } + [JsonIgnore] + public ObjectDictWrapper MyDict { get; set; } + } + + public class WrapperForClassWithIgnoredUnsupportedDictionary + { + public ClassWithIgnoredUnsupportedDictionary MyClass { get; set; } + } + + public class ClassWithUnsupportedBigInteger + { + public BigInteger? MyBigInteger { get; set; } + } + + public class WrapperForClassWithUnsupportedBigInteger + { + public ClassWithUnsupportedBigInteger MyClass { get; set; } = new(); + } + + public class ClassWithIgnoredUnsupportedBigInteger + { + [JsonIgnore] + public BigInteger? MyBigInteger { get; set; } + } + + public class WrapperForClassWithIgnoredUnsupportedBigInteger + { + public ClassWithIgnoredUnsupportedBigInteger MyClass { get; set; } + } + + public class ClassWithMissingObjectProperty + { + public object[] Collection { get; set; } + } + + public class ClassWithMissingCollectionProperty + { + public object Object { get; set; } + } + + public class ClassWithPrivateSetterAndGetter + { + private string MyString { get; set; } + + public string GetMyString() + { + return MyString; + } + + public void SetMyString(string value) + { + MyString = value; + } + } + + public class ClassWithReadOnlyFields + { + public ClassWithReadOnlyFields() + { + MyString = "DefaultValue"; + MyInts = new int[] { 1, 2 }; + } + + public readonly string MyString; + public readonly int[] MyInts; + } + + public class ClassWithNoSetter + { + public ClassWithNoSetter() + { + MyString = "DefaultValue"; + MyInts = new int[] { 1, 2 }; + } + + public string MyString { get; } + public int[] MyInts { get; } + } + + public class ClassWithNoGetter + { + string _myString = ""; + int[] _myIntArray = new int[] { }; + List _myIntList = new List { }; + + public string MyString + { + set + { + _myString = value; + } + } + + public int[] MyIntArray + { + set + { + _myIntArray = value; + } + } + + public List MyList + { + set + { + _myIntList = value; + } + } + + public string GetMyString() + { + return _myString; + } + + public int[] GetMyIntArray() + { + return _myIntArray; + } + + public List GetMyIntList() + { + return _myIntList; + } + } + + public class ClassWithIgnoreAttributeProperty + { + public ClassWithIgnoreAttributeProperty() + { + MyDictionaryWithIgnore = new Dictionary { { "Key", 1 } }; + MyString = "MyString"; + MyStringWithIgnore = "MyStringWithIgnore"; + MyStringsWithIgnore = new string[] { "1", "2" }; + MyNumeric = 3.14M; + } + + [JsonIgnore] + public Dictionary MyDictionaryWithIgnore { get; set; } + + [JsonIgnore] + public string MyStringWithIgnore { get; set; } + + public string MyString { get; set; } + + [JsonIgnore] + public string[] MyStringsWithIgnore { get; set; } + + [JsonIgnore] + public decimal MyNumeric; + } + + public enum MyEnum + { + Case1 = 0, + Case2 = 1, + } + + public struct StructWithOverride + { + [JsonIgnore] + public MyEnum EnumValue { get; set; } + + [JsonPropertyName("EnumValue")] + public string EnumString + { + get => EnumValue.ToString(); + set + { + if (value == "Case1") + { + EnumValue = MyEnum.Case1; + } + else if (value == "Case2") + { + EnumValue = MyEnum.Case2; + } + else + { + throw new Exception("Unknown value!"); + } + } + } + } + + [Fact] + public async Task OverrideJsonIgnorePropertyUsingJsonPropertyName() + { + const string json = @"{""EnumValue"":""Case2""}"; + + StructWithOverride obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal(MyEnum.Case2, obj.EnumValue); + Assert.Equal("Case2", obj.EnumString); + + string jsonSerialized = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Equal(json, jsonSerialized); + } + + public struct ClassWithOverrideReversed + { + // Same as ClassWithOverride except the order of the properties is different, which should cause different reflection order. + [JsonPropertyName("EnumValue")] + public string EnumString + { + get => EnumValue.ToString(); + set + { + if (value == "Case1") + { + EnumValue = MyEnum.Case1; + } + if (value == "Case2") + { + EnumValue = MyEnum.Case2; + } + else + { + throw new Exception("Unknown value!"); + } + } + } + + [JsonIgnore] + public MyEnum EnumValue { get; set; } + } + + [Fact] + public async Task OverrideJsonIgnorePropertyUsingJsonPropertyNameReversed() + { + const string json = @"{""EnumValue"":""Case2""}"; + + ClassWithOverrideReversed obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + Assert.Equal(MyEnum.Case2, obj.EnumValue); + Assert.Equal("Case2", obj.EnumString); + + string jsonSerialized = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Equal(json, jsonSerialized); + } + + [Theory] + [InlineData(typeof(ClassWithProperty_IgnoreConditionAlways))] + [InlineData(typeof(ClassWithProperty_IgnoreConditionAlways_Ctor))] +#if BUILDING_SOURCE_GENERATOR_TESTS + // Need support for parameterized ctors. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task JsonIgnoreConditionSetToAlwaysWorks(Type type) + { + string json = @"{""MyString"":""Random"",""MyDateTime"":""2020-03-23"",""MyInt"":4}"; + + object obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, type); + Assert.Equal("Random", (string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(default, (DateTime)type.GetProperty("MyDateTime").GetValue(obj)); + Assert.Equal(4, (int)type.GetProperty("MyInt").GetValue(obj)); + + string serialized = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Contains(@"""MyString"":""Random""", serialized); + Assert.Contains(@"""MyInt"":4", serialized); + Assert.DoesNotContain(@"""MyDateTime"":", serialized); + } + + public class ClassWithProperty_IgnoreConditionAlways + { + public string MyString { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public DateTime MyDateTime { get; set; } + public int MyInt { get; set; } + } + + private class ClassWithProperty_IgnoreConditionAlways_Ctor + { + public string MyString { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public DateTime MyDateTime { get; } + public int MyInt { get; } + + public ClassWithProperty_IgnoreConditionAlways_Ctor(DateTime myDateTime, int myInt) + { + MyDateTime = myDateTime; + MyInt = myInt; + } + } + + [Theory] + [MemberData(nameof(JsonIgnoreConditionWhenWritingDefault_ClassProperty_TestData))] +#if BUILDING_SOURCE_GENERATOR_TESTS + // Need support for parameterized ctors. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task JsonIgnoreConditionWhenWritingDefault_ClassProperty(Type type, JsonSerializerOptions options) + { + // Property shouldn't be ignored if it isn't null. + string json = @"{""Int1"":1,""MyString"":""Random"",""Int2"":2}"; + + object obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, type, options); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Equal("Random", (string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + string serialized = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""MyString"":""Random""", serialized); + Assert.Contains(@"""Int2"":2", serialized); + + // Property should be ignored when null. + json = @"{""Int1"":1,""MyString"":null,""Int2"":2}"; + + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, type, options); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + + if (options.IgnoreNullValues) + { + // Null values can be ignored on deserialization using IgnoreNullValues. + Assert.Equal("DefaultString", (string)type.GetProperty("MyString").GetValue(obj)); + } + else + { + Assert.Null((string)type.GetProperty("MyString").GetValue(obj)); + } + + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + // Set property to be ignored to null. + type.GetProperty("MyString").SetValue(obj, null); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""Int2"":2", serialized); + Assert.DoesNotContain(@"""MyString"":", serialized); + } + + public class ClassWithClassProperty_IgnoreConditionWhenWritingDefault + { + public int Int1 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string MyString { get; set; } = "DefaultString"; + public int Int2 { get; set; } + } + + private class ClassWithClassProperty_IgnoreConditionWhenWritingDefault_Ctor + { + public int Int1 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string MyString { get; set; } = "DefaultString"; + public int Int2 { get; set; } + + public ClassWithClassProperty_IgnoreConditionWhenWritingDefault_Ctor(string myString) + { + if (myString != null) + { + MyString = myString; + } + } + } + + public static IEnumerable JsonIgnoreConditionWhenWritingDefault_ClassProperty_TestData() + { + yield return new object[] { typeof(ClassWithClassProperty_IgnoreConditionWhenWritingDefault), new JsonSerializerOptions() }; + yield return new object[] { typeof(ClassWithClassProperty_IgnoreConditionWhenWritingDefault_Ctor), new JsonSerializerOptions { IgnoreNullValues = true } }; + } + + [Theory] + [MemberData(nameof(JsonIgnoreConditionWhenWritingDefault_StructProperty_TestData))] +#if BUILDING_SOURCE_GENERATOR_TESTS + // Need support for parameterized ctors. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task JsonIgnoreConditionWhenWritingDefault_StructProperty(Type type, JsonSerializerOptions options) + { + // Property shouldn't be ignored if it isn't null. + string json = @"{""Int1"":1,""MyInt"":3,""Int2"":2}"; + + object obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, type, options); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Equal(3, (int)type.GetProperty("MyInt").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + string serialized = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""MyInt"":3", serialized); + Assert.Contains(@"""Int2"":2", serialized); + + // Null being assigned to non-nullable types is invalid. + json = @"{""Int1"":1,""MyInt"":null,""Int2"":2}"; + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper(json, type, options)); + } + + public class ClassWithStructProperty_IgnoreConditionWhenWritingDefault + { + public int Int1 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int MyInt { get; set; } + public int Int2 { get; set; } + } + + private struct StructWithStructProperty_IgnoreConditionWhenWritingDefault_Ctor + { + public int Int1 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int MyInt { get; } + public int Int2 { get; set; } + + [JsonConstructor] + public StructWithStructProperty_IgnoreConditionWhenWritingDefault_Ctor(int myInt) + { + Int1 = 0; + MyInt = myInt; + Int2 = 0; + } + } + + public static IEnumerable JsonIgnoreConditionWhenWritingDefault_StructProperty_TestData() + { + yield return new object[] { typeof(ClassWithStructProperty_IgnoreConditionWhenWritingDefault), new JsonSerializerOptions() }; + yield return new object[] { typeof(StructWithStructProperty_IgnoreConditionWhenWritingDefault_Ctor), new JsonSerializerOptions { IgnoreNullValues = true } }; + } + + [Theory] + [MemberData(nameof(JsonIgnoreConditionNever_TestData))] +#if BUILDING_SOURCE_GENERATOR_TESTS + // Need support for parameterized ctors. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task JsonIgnoreConditionNever(Type type) + { + // Property should always be (de)serialized, even when null. + string json = @"{""Int1"":1,""MyString"":""Random"",""Int2"":2}"; + + object obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, type); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Equal("Random", (string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + string serialized = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""MyString"":""Random""", serialized); + Assert.Contains(@"""Int2"":2", serialized); + + // Property should always be (de)serialized, even when null. + json = @"{""Int1"":1,""MyString"":null,""Int2"":2}"; + + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, type); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Null((string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""MyString"":null", serialized); + Assert.Contains(@"""Int2"":2", serialized); + } + + [Theory] + [MemberData(nameof(JsonIgnoreConditionNever_TestData))] +#if BUILDING_SOURCE_GENERATOR_TESTS + // Need support for parameterized ctors. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task JsonIgnoreConditionNever_IgnoreNullValues_True(Type type) + { + // Property should always be (de)serialized. + string json = @"{""Int1"":1,""MyString"":""Random"",""Int2"":2}"; + var options = new JsonSerializerOptions { IgnoreNullValues = true }; + + object obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, type, options); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Equal("Random", (string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + string serialized = await JsonSerializerWrapperForString.SerializeWrapper(obj, type, options); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""MyString"":""Random""", serialized); + Assert.Contains(@"""Int2"":2", serialized); + + // Property should always be (de)serialized, even when null. + json = @"{""Int1"":1,""MyString"":null,""Int2"":2}"; + + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, type, options); + Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); + Assert.Null((string)type.GetProperty("MyString").GetValue(obj)); + Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); + + serialized = await JsonSerializerWrapperForString.SerializeWrapper(obj, type, options); + Assert.Contains(@"""Int1"":1", serialized); + Assert.Contains(@"""MyString"":null", serialized); + Assert.Contains(@"""Int2"":2", serialized); + } + + public class ClassWithStructProperty_IgnoreConditionNever + { + public int Int1 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string MyString { get; set; } + public int Int2 { get; set; } + } + + public class ClassWithStructProperty_IgnoreConditionNever_Ctor + { + public int Int1 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string MyString { get; } + public int Int2 { get; set; } + + public ClassWithStructProperty_IgnoreConditionNever_Ctor(string myString) + { + MyString = myString; + } + } + + public static IEnumerable JsonIgnoreConditionNever_TestData() + { + yield return new object[] { typeof(ClassWithStructProperty_IgnoreConditionNever) }; + yield return new object[] { typeof(ClassWithStructProperty_IgnoreConditionNever_Ctor) }; + } + + [Fact] + public async Task JsonIgnoreCondition_LastOneWins() + { + string json = @"{""MyString"":""Random"",""MYSTRING"":null}"; + + var options = new JsonSerializerOptions + { + IgnoreNullValues = true, + PropertyNameCaseInsensitive = true + }; + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + + Assert.Null(obj.MyString); + } + + [Fact] +#if BUILDING_SOURCE_GENERATOR_TESTS + [ActiveIssue("https://github.com/dotnet/runtime/issues/53393")] +#endif + public async Task ClassWithComplexObjectsUsingIgnoreWhenWritingDefaultAttribute() + { + string json = @"{""Class"":{""MyInt16"":18}, ""Dictionary"":null}"; + + ClassUsingIgnoreWhenWritingDefaultAttribute obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + + // Class is deserialized. + Assert.NotNull(obj.Class); + Assert.Equal(18, obj.Class.MyInt16); + + // Dictionary is deserialized as JsonIgnoreCondition.WhenWritingDefault only applies to deserialization. + Assert.Null(obj.Dictionary); + + obj = new ClassUsingIgnoreWhenWritingDefaultAttribute(); + json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Equal(@"{""Dictionary"":{""Key"":""Value""}}", json); + } + + public class ClassUsingIgnoreWhenWritingDefaultAttribute + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public SimpleTestClass Class { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Dictionary Dictionary { get; set; } = new Dictionary { ["Key"] = "Value" }; + } + + [Fact] +#if BUILDING_SOURCE_GENERATOR_TESTS + [ActiveIssue("https://github.com/dotnet/runtime/issues/53393")] +#endif + public async Task ClassWithComplexObjectUsingIgnoreNeverAttribute() + { + string json = @"{""Class"":null, ""Dictionary"":null}"; + var options = new JsonSerializerOptions { IgnoreNullValues = true }; + + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + + // Class is not deserialized because it is null in json. + Assert.NotNull(obj.Class); + Assert.Equal(18, obj.Class.MyInt16); + + // Dictionary is deserialized regardless of being null in json. + Assert.Null(obj.Dictionary); + + // Serialize when values are null. + obj = new ClassUsingIgnoreNeverAttribute(); + obj.Class = null; + obj.Dictionary = null; + + json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + + // Class is not included in json because it was null, Dictionary is included regardless of being null. + Assert.Equal(@"{""Dictionary"":null}", json); + } + + public class ClassUsingIgnoreNeverAttribute + { + public SimpleTestClass Class { get; set; } = new SimpleTestClass { MyInt16 = 18 }; + + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public Dictionary Dictionary { get; set; } = new Dictionary { ["Key"] = "Value" }; + } + + [Fact] + public async Task IgnoreConditionNever_WinsOver_IgnoreReadOnlyProperties() + { + var options = new JsonSerializerOptions { IgnoreReadOnlyProperties = true }; + + // Baseline + string json = await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithReadOnlyStringProperty("Hello"), options); + Assert.Equal("{}", json); + + // With condition to never ignore + json = await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithReadOnlyStringProperty_IgnoreNever("Hello"), options); + Assert.Equal(@"{""MyString"":""Hello""}", json); + + json = await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithReadOnlyStringProperty_IgnoreNever(null), options); + Assert.Equal(@"{""MyString"":null}", json); + } + + [Fact] + public async Task IgnoreConditionWhenWritingDefault_WinsOver_IgnoreReadOnlyProperties() + { + var options = new JsonSerializerOptions { IgnoreReadOnlyProperties = true }; + + // Baseline + string json = await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithReadOnlyStringProperty("Hello"), options); + Assert.Equal("{}", json); + + // With condition to ignore when null + json = await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithReadOnlyStringProperty_IgnoreWhenWritingDefault("Hello"), options); + Assert.Equal(@"{""MyString"":""Hello""}", json); + + json = await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithReadOnlyStringProperty_IgnoreWhenWritingDefault(null), options); + Assert.Equal(@"{}", json); + } + + [Fact] + public async Task IgnoreConditionNever_WinsOver_IgnoreReadOnlyFields() + { + var options = new JsonSerializerOptions { IgnoreReadOnlyProperties = true }; + + // Baseline + string json = await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithReadOnlyStringField("Hello"), options); + Assert.Equal("{}", json); + + // With condition to never ignore + json = await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithReadOnlyStringField_IgnoreNever("Hello"), options); + Assert.Equal(@"{""MyString"":""Hello""}", json); + + json = await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithReadOnlyStringField_IgnoreNever(null), options); + Assert.Equal(@"{""MyString"":null}", json); + } + + [Fact] + public async Task IgnoreConditionWhenWritingDefault_WinsOver_IgnoreReadOnlyFields() + { + var options = new JsonSerializerOptions { IgnoreReadOnlyProperties = true }; + + // Baseline + string json = await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithReadOnlyStringField("Hello"), options); + Assert.Equal("{}", json); + + // With condition to ignore when null + json = await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithReadOnlyStringField_IgnoreWhenWritingDefault("Hello"), options); + Assert.Equal(@"{""MyString"":""Hello""}", json); + + json = await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithReadOnlyStringField_IgnoreWhenWritingDefault(null), options); + Assert.Equal(@"{}", json); + } + + public class ClassWithReadOnlyStringProperty + { + public string MyString { get; } + + public ClassWithReadOnlyStringProperty(string myString) => MyString = myString; + } + + public class ClassWithReadOnlyStringProperty_IgnoreNever + { + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string MyString { get; } + + public ClassWithReadOnlyStringProperty_IgnoreNever(string myString) => MyString = myString; + } + + public class ClassWithReadOnlyStringProperty_IgnoreWhenWritingDefault + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string MyString { get; } + + public ClassWithReadOnlyStringProperty_IgnoreWhenWritingDefault(string myString) => MyString = myString; + } + + public class ClassWithReadOnlyStringField + { + public string MyString { get; } + + public ClassWithReadOnlyStringField(string myString) => MyString = myString; + } + + public class ClassWithReadOnlyStringField_IgnoreNever + { + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string MyString { get; } + + public ClassWithReadOnlyStringField_IgnoreNever(string myString) => MyString = myString; + } + + public class ClassWithReadOnlyStringField_IgnoreWhenWritingDefault + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string MyString { get; } + + public ClassWithReadOnlyStringField_IgnoreWhenWritingDefault(string myString) => MyString = myString; + } + + [Fact] + public async Task NonPublicMembersAreNotIncluded() + { + Assert.Equal("{}", await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithNonPublicProperties())); + + string json = @"{""MyInt"":1,""MyString"":""Hello"",""MyFloat"":2,""MyDouble"":3}"; + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + Assert.Equal(0, obj.MyInt); + Assert.Null(obj.MyString); + Assert.Equal(0, obj.GetMyFloat); + Assert.Equal(0, obj.GetMyDouble); + } + + public class ClassWithNonPublicProperties + { + internal int MyInt { get; set; } + internal string MyString { get; private set; } + internal float MyFloat { private get; set; } + private double MyDouble { get; set; } + + internal float GetMyFloat => MyFloat; + internal double GetMyDouble => MyDouble; + } + + [Fact] + public async Task IgnoreCondition_WhenWritingDefault_Globally_Works() + { + // Baseline - default values written. + string expected = @"{""MyString"":null,""MyInt"":0,""MyPoint"":{""X"":0,""Y"":0}}"; + var obj = new ClassWithProps(); + JsonTestHelper.AssertJsonEqual(expected, await JsonSerializerWrapperForString.SerializeWrapper(obj)); + + // Default values ignored when specified. + Assert.Equal("{}", await JsonSerializerWrapperForString.SerializeWrapper(obj, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault })); + } + + public class ClassWithProps + { + public string MyString { get; set; } + public int MyInt { get; set; } + public Point_2D_Struct MyPoint { get; set; } + } + + [Fact] + public async Task IgnoreCondition_WhenWritingDefault_PerProperty_Works() + { + // Default values ignored when specified. + Assert.Equal(@"{""MyInt"":0}", await JsonSerializerWrapperForString.SerializeWrapper(new ClassWithPropsAndIgnoreAttributes())); + } + + public class ClassWithPropsAndIgnoreAttributes + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string MyString { get; set; } + public int MyInt { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Point_2D_Struct MyPoint { get; set; } + } + + [Fact] + public async Task IgnoreCondition_WhenWritingDefault_DoesNotApplyToCollections() + { + var list = new List { false, true }; + + var options = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault }; + Assert.Equal("[false,true]", await JsonSerializerWrapperForString.SerializeWrapper(list, options)); + } + + [Fact] + public async Task IgnoreCondition_WhenWritingDefault_DoesNotApplyToDeserialization() + { + // Baseline - null values are ignored on deserialization when using IgnoreNullValues (for compat with initial support). + string json = @"{""MyString"":null,""MyInt"":0,""MyPoint"":{""X"":0,""Y"":0}}"; + + var options = new JsonSerializerOptions { IgnoreNullValues = true }; + ClassWithInitializedProps obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + + Assert.Equal("Default", obj.MyString); + // Value types are not ignored. + Assert.Equal(0, obj.MyInt); + Assert.Equal(0, obj.MyPoint.X); + Assert.Equal(0, obj.MyPoint.X); + + // Test - default values (both null and default for value types) are not ignored when using + // JsonIgnoreCondition.WhenWritingDefault (as the option name implies) + options = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault }; + obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + Assert.Null(obj.MyString); + Assert.Equal(0, obj.MyInt); + Assert.Equal(0, obj.MyPoint.X); + Assert.Equal(0, obj.MyPoint.X); + } + + public class ClassWithInitializedProps + { + public string MyString { get; set; } = "Default"; + public int MyInt { get; set; } = -1; + public Point_2D_Struct MyPoint { get; set; } = new Point_2D_Struct(-1, -1); + } + + [Fact] + public async Task ValueType_Properties_NotIgnoredWhen_IgnoreNullValues_Active_ClassTest() + { + var options = new JsonSerializerOptions { IgnoreNullValues = true }; + + // Deserialization. + string json = @"{""MyString"":null,""MyInt"":0,""MyBool"":null,""MyPointClass"":null,""MyPointStruct"":{""X"":0,""Y"":0}}"; + + ClassWithValueAndReferenceTypes obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + + // Null values ignored for reference types/nullable value types. + Assert.Equal("Default", obj.MyString); + Assert.NotNull(obj.MyPointClass); + Assert.True(obj.MyBool); + + // Default values not ignored for value types. + Assert.Equal(0, obj.MyInt); + Assert.Equal(0, obj.MyPointStruct.X); + Assert.Equal(0, obj.MyPointStruct.Y); + + // Serialization. + + // Make all members their default CLR value. + obj.MyString = null; + obj.MyPointClass = null; + obj.MyBool = null; + + json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + + // Null values not serialized, default values for value types serialized. + JsonTestHelper.AssertJsonEqual(@"{""MyInt"":0,""MyPointStruct"":{""X"":0,""Y"":0}}", json); + } + + [Fact] +#if BUILDING_SOURCE_GENERATOR_TESTS + // Need support for parameterized ctors. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task ValueType_Properties_NotIgnoredWhen_IgnoreNullValues_Active_LargeStructTest() + { + var options = new JsonSerializerOptions { IgnoreNullValues = true }; + + // Deserialization. + string json = @"{""MyString"":null,""MyInt"":0,""MyBool"":null,""MyPointClass"":null,""MyPointStruct"":{""X"":0,""Y"":0}}"; + + LargeStructWithValueAndReferenceTypes obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + + // Null values ignored for reference types. + + Assert.Equal("Default", obj.MyString); + // No way to specify a non-constant default before construction with ctor, so this remains null. + Assert.Null(obj.MyPointClass); + Assert.True(obj.MyBool); + + // Default values not ignored for value types. + Assert.Equal(0, obj.MyInt); + Assert.Equal(0, obj.MyPointStruct.X); + Assert.Equal(0, obj.MyPointStruct.Y); + + // Serialization. + + // Make all members their default CLR value. + obj = new LargeStructWithValueAndReferenceTypes(null, new Point_2D_Struct(0, 0), null, 0, null); + + json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + + // Null values not serialized, default values for value types serialized. + JsonTestHelper.AssertJsonEqual(@"{""MyInt"":0,""MyPointStruct"":{""X"":0,""Y"":0}}", json); + } + + [Fact] +#if BUILDING_SOURCE_GENERATOR_TESTS + // Need support for parameterized ctors. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task ValueType_Properties_NotIgnoredWhen_IgnoreNullValues_Active_SmallStructTest() + { + var options = new JsonSerializerOptions { IgnoreNullValues = true }; + + // Deserialization. + string json = @"{""MyString"":null,""MyInt"":0,""MyBool"":null,""MyPointStruct"":{""X"":0,""Y"":0}}"; + + SmallStructWithValueAndReferenceTypes obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + + // Null values ignored for reference types. + Assert.Equal("Default", obj.MyString); + Assert.True(obj.MyBool); + + // Default values not ignored for value types. + Assert.Equal(0, obj.MyInt); + Assert.Equal(0, obj.MyPointStruct.X); + Assert.Equal(0, obj.MyPointStruct.Y); + + // Serialization. + + // Make all members their default CLR value. + obj = new SmallStructWithValueAndReferenceTypes(new Point_2D_Struct(0, 0), null, 0, null); + + json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + + // Null values not serialized, default values for value types serialized. + JsonTestHelper.AssertJsonEqual(@"{""MyInt"":0,""MyPointStruct"":{""X"":0,""Y"":0}}", json); + } + + public class ClassWithValueAndReferenceTypes + { + public string MyString { get; set; } = "Default"; + public int MyInt { get; set; } = -1; + public bool? MyBool { get; set; } = true; + public PointClass MyPointClass { get; set; } = new PointClass(); + public Point_2D_Struct MyPointStruct { get; set; } = new Point_2D_Struct(1, 2); + } + + public struct LargeStructWithValueAndReferenceTypes + { + public string MyString { get; } + public int MyInt { get; set; } + public bool? MyBool { get; set; } + public PointClass MyPointClass { get; set; } + public Point_2D_Struct MyPointStruct { get; set; } + + [JsonConstructor] + public LargeStructWithValueAndReferenceTypes( + PointClass myPointClass, + Point_2D_Struct myPointStruct, + string myString = "Default", + int myInt = -1, + bool? myBool = true) + { + MyString = myString; + MyInt = myInt; + MyBool = myBool; + MyPointClass = myPointClass; + MyPointStruct = myPointStruct; + } + } + + private struct SmallStructWithValueAndReferenceTypes + { + public string MyString { get; } + public int MyInt { get; set; } + public bool? MyBool { get; set; } + public Point_2D_Struct MyPointStruct { get; set; } + + [JsonConstructor] + public SmallStructWithValueAndReferenceTypes( + Point_2D_Struct myPointStruct, + string myString = "Default", + int myInt = -1, + bool? myBool = true) + { + MyString = myString; + MyInt = myInt; + MyBool = myBool; + MyPointStruct = myPointStruct; + } + } + + public class PointClass { } + + [Fact] +#if BUILDING_SOURCE_GENERATOR_TESTS + // Need support for parameterized ctors. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task Ignore_WhenWritingNull_Globally() + { + var options = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IncludeFields = true + }; + + string json = @"{ +""MyPointClass2_IgnoredWhenWritingNull"":{}, +""MyString1_IgnoredWhenWritingNull"":""Default"", +""MyNullableBool1_IgnoredWhenWritingNull"":null, +""MyInt2"":0, +""MyPointStruct2"":{""X"":1,""Y"":2}, +""MyInt1"":1, +""MyString2_IgnoredWhenWritingNull"":null, +""MyPointClass1_IgnoredWhenWritingNull"":null, +""MyNullableBool2_IgnoredWhenWritingNull"":true, +""MyPointStruct1"":{""X"":0,""Y"":0} +}"; + + // All members should correspond to JSON contents, as ignore doesn't apply to deserialization. + ClassWithThingsToIgnore obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + Assert.NotNull(obj.MyPointClass2_IgnoredWhenWritingNull); + Assert.Equal("Default", obj.MyString1_IgnoredWhenWritingNull); + Assert.Null(obj.MyNullableBool1_IgnoredWhenWritingNull); + Assert.Equal(0, obj.MyInt2); + Assert.Equal(1, obj.MyPointStruct2.X); + Assert.Equal(2, obj.MyPointStruct2.Y); + Assert.Equal(1, obj.MyInt1); + Assert.Null(obj.MyString2_IgnoredWhenWritingNull); + Assert.Null(obj.MyPointClass1_IgnoredWhenWritingNull); + Assert.True(obj.MyNullableBool2_IgnoredWhenWritingNull); + Assert.Equal(0, obj.MyPointStruct1.X); + Assert.Equal(0, obj.MyPointStruct1.Y); + + // Ignore null as appropriate during serialization. + string expectedJson = @"{ +""MyPointClass2_IgnoredWhenWritingNull"":{}, +""MyString1_IgnoredWhenWritingNull"":""Default"", +""MyInt2"":0, +""MyPointStruct2"":{""X"":1,""Y"":2}, +""MyInt1"":1, +""MyNullableBool2_IgnoredWhenWritingNull"":true, +""MyPointStruct1"":{""X"":0,""Y"":0} +}"; + JsonTestHelper.AssertJsonEqual(expectedJson, await JsonSerializerWrapperForString.SerializeWrapper(obj, options)); + } + + public class ClassWithThingsToIgnore + { + public string MyString1_IgnoredWhenWritingNull { get; set; } + + public string MyString2_IgnoredWhenWritingNull; + + public int MyInt1; + + public int MyInt2 { get; set; } + + public bool? MyNullableBool1_IgnoredWhenWritingNull { get; set; } + + public bool? MyNullableBool2_IgnoredWhenWritingNull; + + public PointClass MyPointClass1_IgnoredWhenWritingNull; + + public PointClass MyPointClass2_IgnoredWhenWritingNull { get; set; } + + public Point_2D_Struct_WithAttribute MyPointStruct1; + + public Point_2D_Struct_WithAttribute MyPointStruct2 { get; set; } + } + + [Fact] +#if BUILDING_SOURCE_GENERATOR_TESTS + // Need support for parameterized ctors. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task Ignore_WhenWritingNull_PerProperty() + { + var options = new JsonSerializerOptions + { + IncludeFields = true + }; + + string json = @"{ +""MyPointClass2_IgnoredWhenWritingNull"":{}, +""MyString1_IgnoredWhenWritingNull"":""Default"", +""MyNullableBool1_IgnoredWhenWritingNull"":null, +""MyInt2"":0, +""MyPointStruct2"":{""X"":1,""Y"":2}, +""MyInt1"":1, +""MyString2_IgnoredWhenWritingNull"":null, +""MyPointClass1_IgnoredWhenWritingNull"":null, +""MyNullableBool2_IgnoredWhenWritingNull"":true, +""MyPointStruct1"":{""X"":0,""Y"":0} +}"; + + // All members should correspond to JSON contents, as ignore doesn't apply to deserialization. + ClassWithThingsToIgnore_PerProperty obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + Assert.NotNull(obj.MyPointClass2_IgnoredWhenWritingNull); + Assert.Equal("Default", obj.MyString1_IgnoredWhenWritingNull); + Assert.Null(obj.MyNullableBool1_IgnoredWhenWritingNull); + Assert.Equal(0, obj.MyInt2); + Assert.Equal(1, obj.MyPointStruct2.X); + Assert.Equal(2, obj.MyPointStruct2.Y); + Assert.Equal(1, obj.MyInt1); + Assert.Null(obj.MyString2_IgnoredWhenWritingNull); + Assert.Null(obj.MyPointClass1_IgnoredWhenWritingNull); + Assert.True(obj.MyNullableBool2_IgnoredWhenWritingNull); + Assert.Equal(0, obj.MyPointStruct1.X); + Assert.Equal(0, obj.MyPointStruct1.Y); + + // Ignore null as appropriate during serialization. + string expectedJson = @"{ +""MyPointClass2_IgnoredWhenWritingNull"":{}, +""MyString1_IgnoredWhenWritingNull"":""Default"", +""MyInt2"":0, +""MyPointStruct2"":{""X"":1,""Y"":2}, +""MyInt1"":1, +""MyNullableBool2_IgnoredWhenWritingNull"":true, +""MyPointStruct1"":{""X"":0,""Y"":0} +}"; + JsonTestHelper.AssertJsonEqual(expectedJson, await JsonSerializerWrapperForString.SerializeWrapper(obj, options)); + } + + public class ClassWithThingsToIgnore_PerProperty + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string MyString1_IgnoredWhenWritingNull { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string MyString2_IgnoredWhenWritingNull; + + public int MyInt1; + + public int MyInt2 { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? MyNullableBool1_IgnoredWhenWritingNull { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? MyNullableBool2_IgnoredWhenWritingNull; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public PointClass MyPointClass1_IgnoredWhenWritingNull; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public PointClass MyPointClass2_IgnoredWhenWritingNull { get; set; } + + public Point_2D_Struct_WithAttribute MyPointStruct1; + + public Point_2D_Struct_WithAttribute MyPointStruct2 { get; set; } + } + + [Theory] + [InlineData(typeof(ClassWithBadIgnoreAttribute))] + [InlineData(typeof(StructWithBadIgnoreAttribute))] + public virtual async Task JsonIgnoreCondition_WhenWritingNull_OnValueType_Fail(Type type) + { + InvalidOperationException ex = await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("{}", type)); + string exAsStr = ex.ToString(); + Assert.Contains("JsonIgnoreCondition.WhenWritingNull", exAsStr); + Assert.Contains("MyBadMember", exAsStr); + Assert.Contains(type.ToString(), exAsStr); + Assert.Contains("JsonIgnoreCondition.WhenWritingDefault", exAsStr); + + ex = await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(Activator.CreateInstance(type), type)); + exAsStr = ex.ToString(); + Assert.Contains("JsonIgnoreCondition.WhenWritingNull", exAsStr); + Assert.Contains("MyBadMember", exAsStr); + Assert.Contains(type.ToString(), exAsStr); + Assert.Contains("JsonIgnoreCondition.WhenWritingDefault", exAsStr); + } + + [Theory] + [InlineData(typeof(ClassWithBadIgnoreAttribute))] + [InlineData(typeof(StructWithBadIgnoreAttribute))] + public virtual async Task JsonIgnoreCondition_WhenWritingNull_OnValueType_Fail_EmptyJson(Type type) + { + InvalidOperationException ex = await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("", type)); + string exAsStr = ex.ToString(); + Assert.Contains("JsonIgnoreCondition.WhenWritingNull", exAsStr); + Assert.Contains("MyBadMember", exAsStr); + Assert.Contains(type.ToString(), exAsStr); + Assert.Contains("JsonIgnoreCondition.WhenWritingDefault", exAsStr); + + ex = await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(Activator.CreateInstance(type))); + exAsStr = ex.ToString(); + Assert.Contains("JsonIgnoreCondition.WhenWritingNull", exAsStr); + Assert.Contains("MyBadMember", exAsStr); + Assert.Contains(type.ToString(), exAsStr); + Assert.Contains("JsonIgnoreCondition.WhenWritingDefault", exAsStr); + } + + public class ClassWithBadIgnoreAttribute + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int MyBadMember { get; set; } + } + + public struct StructWithBadIgnoreAttribute + { + [JsonInclude] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Point_2D_Struct MyBadMember { get; set; } + } + + public interface IUseCustomConverter { } + + [JsonConverter(typeof(MyCustomConverter))] + public struct MyValueTypeWithProperties : IUseCustomConverter + { + public int PrimitiveValue { get; set; } + public object RefValue { get; set; } + } + + public class MyCustomConverter : JsonConverter + { + public override bool CanConvert(Type typeToConvert) + { + return typeof(IUseCustomConverter).IsAssignableFrom(typeToConvert); + } + + public override IUseCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + throw new NotImplementedException(); + + public override void Write(Utf8JsonWriter writer, IUseCustomConverter value, JsonSerializerOptions options) + { + MyValueTypeWithProperties obj = (MyValueTypeWithProperties)value; + writer.WriteNumberValue(obj.PrimitiveValue + 100); + // Ignore obj.RefValue + } + } + + public class MyClassWithValueType + { + public MyClassWithValueType() { } + + public MyValueTypeWithProperties Value { get; set; } + } + + [Fact] +#if BUILDING_SOURCE_GENERATOR_TESTS + // Needs bug fixes to custom converter handling. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task JsonIgnoreCondition_WhenWritingDefault_OnValueTypeWithCustomConverter() + { + var obj = new MyClassWithValueType(); + + // Baseline without custom options. + Assert.True(EqualityComparer.Default.Equals(default, obj.Value)); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Equal("{\"Value\":100}", json); + + var options = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault + }; + + // Verify ignored. + Assert.True(EqualityComparer.Default.Equals(default, obj.Value)); + json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + Assert.Equal("{}", json); + + // Change a primitive value so it's no longer a default value. + obj.Value = new MyValueTypeWithProperties { PrimitiveValue = 1 }; + Assert.False(EqualityComparer.Default.Equals(default, obj.Value)); + json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + Assert.Equal("{\"Value\":101}", json); + + // Change reference value so it's no longer a default value. + obj.Value = new MyValueTypeWithProperties { RefValue = 1 }; + Assert.False(EqualityComparer.Default.Equals(default, obj.Value)); + json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + Assert.Equal("{\"Value\":100}", json); + } + + [Fact] + public async Task JsonIgnoreCondition_ConverterCalledOnDeserialize() + { + // Verify converter is called. + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("{}")); + + var options = new JsonSerializerOptions + { + IgnoreNullValues = true + }; + + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("{}", options)); + } + + [Fact] +#if BUILDING_SOURCE_GENERATOR_TESTS + // Needs bug fixes to custom converter handling. + [ActiveIssue("https://github.com/dotnet/runtime/issues/45448")] +#endif + public async Task JsonIgnoreCondition_WhenWritingNull_OnValueTypeWithCustomConverter() + { + string json; + var obj = new MyClassWithValueType(); + + // Baseline without custom options. + Assert.True(EqualityComparer.Default.Equals(default, obj.Value)); + json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Equal("{\"Value\":100}", json); + + var options = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + // Verify not ignored; MyValueTypeWithProperties is not null. + Assert.True(EqualityComparer.Default.Equals(default, obj.Value)); + json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + Assert.Equal("{\"Value\":100}", json); + } + + [Fact] + public async Task JsonIgnoreCondition_WhenWritingDefault_OnRootTypes() + { + string json; + int i = 0; + object obj = null; + + // Baseline without custom options. + json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Equal("null", json); + + json = await JsonSerializerWrapperForString.SerializeWrapper(i); + Assert.Equal("0", json); + + var options = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault + }; + + // We don't ignore when applied to root types; only properties. + + json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + Assert.Equal("null", json); + + json = await JsonSerializerWrapperForString.SerializeWrapper(i, options); + Assert.Equal("0", json); + } + + public struct MyValueTypeWithBoxedPrimitive + { + public object BoxedPrimitive { get; set; } + } + + [Fact] + public async Task JsonIgnoreCondition_WhenWritingDefault_OnBoxedPrimitive() + { + string json; + + MyValueTypeWithBoxedPrimitive obj = new MyValueTypeWithBoxedPrimitive { BoxedPrimitive = 0 }; + + // Baseline without custom options. + json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Equal("{\"BoxedPrimitive\":0}", json); + + var options = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault + }; + + // No check if the boxed object's value type is a default value (0 in this case). + json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + Assert.Equal("{\"BoxedPrimitive\":0}", json); + + obj = new MyValueTypeWithBoxedPrimitive(); + json = await JsonSerializerWrapperForString.SerializeWrapper(obj, options); + Assert.Equal("{}", json); + } + + public class MyClassWithValueTypeInterfaceProperty + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IInterface MyProp { get; set; } + + public interface IInterface { } + public struct MyStruct : IInterface { } + } + + [Fact] + public async Task JsonIgnoreCondition_WhenWritingDefault_OnInterface() + { + // MyProp should be ignored due to [JsonIgnore]. + var obj = new MyClassWithValueTypeInterfaceProperty(); + string json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Equal("{}", json); + + // No check if the interface property's value type is a default value. + obj = new MyClassWithValueTypeInterfaceProperty { MyProp = new MyClassWithValueTypeInterfaceProperty.MyStruct() }; + json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Equal("{\"MyProp\":{}}", json); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Common/SerializerTests.cs b/src/libraries/System.Text.Json/tests/Common/SerializerTests.cs new file mode 100644 index 0000000000000..c7198b1e30691 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/SerializerTests.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization.Tests +{ + public abstract class SerializerTests + { + protected JsonSerializerWrapperForString JsonSerializerWrapperForString { get; } + + protected SerializerTests(JsonSerializerWrapperForString serializerWrapper) => JsonSerializerWrapperForString = serializerWrapper; + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.ConcurrentCollections.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.ConcurrentCollections.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.ConcurrentCollections.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.ConcurrentCollections.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.Constructor.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.Constructor.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.GenericCollections.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.GenericCollections.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.GenericCollections.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.GenericCollections.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.ImmutableCollections.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.ImmutableCollections.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.ImmutableCollections.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.ImmutableCollections.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.NonGenericCollections.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.NonGenericCollections.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.NonGenericCollections.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.NonGenericCollections.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.Polymorphic.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Polymorphic.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.Polymorphic.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Polymorphic.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestClass.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClass.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestClass.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClass.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestClassWithFields.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithFields.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestClassWithFields.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithFields.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestClassWithNullables.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithNullables.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestClassWithNullables.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithNullables.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestClassWithObject.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithObject.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestClassWithObject.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithObject.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestClassWithObjectArrays.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithObjectArrays.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestClassWithObjectArrays.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithObjectArrays.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestClassWithSimpleObject.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithSimpleObject.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestClassWithSimpleObject.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithSimpleObject.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestStruct.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestStruct.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestStruct.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestStruct.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestStructWithFields.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestStructWithFields.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.SimpleTestStructWithFields.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestStructWithFields.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.ValueTypedMember.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.ValueTypedMember.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.ValueTypedMember.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.ValueTypedMember.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.cs similarity index 100% rename from src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TestClasses/TestClasses.cs rename to src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.cs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSerializerWrapperForString_SourceGen.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSerializerWrapperForString_SourceGen.cs new file mode 100644 index 0000000000000..dfb968371a224 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSerializerWrapperForString_SourceGen.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json.Serialization.Tests; +using System.Threading.Tasks; + +namespace System.Text.Json.SourceGeneration.Tests +{ + internal sealed class JsonSerializerWrapperForString_SourceGen : JsonSerializerWrapperForString + { + private readonly JsonSerializerContext _defaultContext; + private readonly Func _customContextCreator; + + public JsonSerializerWrapperForString_SourceGen(JsonSerializerContext defaultContext, Func customContextCreator) + { + _defaultContext = defaultContext ?? throw new ArgumentNullException(nameof(defaultContext)); + _customContextCreator = customContextCreator ?? throw new ArgumentNullException(nameof(defaultContext)); + } + + protected internal override Task SerializeWrapper(object value, Type type, JsonSerializerOptions? options = null) + { + if (options != null) + { + return Task.FromResult(Serialize(value, type, options)); + } + + return Task.FromResult(JsonSerializer.Serialize(value, type, _defaultContext)); + } + + private string Serialize(object value, Type type, JsonSerializerOptions options) + { + JsonSerializerContext context = _customContextCreator(new JsonSerializerOptions(options)); + return JsonSerializer.Serialize(value, type, context); + } + + protected internal override Task SerializeWrapper(T value, JsonSerializerOptions? options = null) + { + if (options != null) + { + return Task.FromResult(Serialize(value, options)); + } + + JsonTypeInfo typeInfo = (JsonTypeInfo)_defaultContext.GetTypeInfo(typeof(T)); + return Task.FromResult(JsonSerializer.Serialize(value, typeInfo)); + } + + private string Serialize(T value, JsonSerializerOptions options) + { + JsonSerializerContext context = _customContextCreator(new JsonSerializerOptions(options)); + JsonTypeInfo typeInfo = (JsonTypeInfo)context.GetTypeInfo(typeof(T)); + return JsonSerializer.Serialize(value, typeInfo); + } + + protected internal override Task SerializeWrapper(object value, Type inputType, JsonSerializerContext context) + => throw new NotImplementedException(); + + protected internal override Task SerializeWrapper(T value, JsonTypeInfo jsonTypeInfo) + => throw new NotImplementedException(); + + protected internal override Task DeserializeWrapper(string json, JsonSerializerOptions? options = null) + { + if (options != null) + { + return Task.FromResult(Deserialize(json, options)); + } + + JsonTypeInfo typeInfo = (JsonTypeInfo)_defaultContext.GetTypeInfo(typeof(T)); + return Task.FromResult(JsonSerializer.Deserialize(json, typeInfo)); + } + + private T Deserialize(string json, JsonSerializerOptions options) + { + JsonSerializerContext context = _customContextCreator(new JsonSerializerOptions(options)); + JsonTypeInfo typeInfo = (JsonTypeInfo)context.GetTypeInfo(typeof(T)); + return JsonSerializer.Deserialize(json, typeInfo); + } + + protected internal override Task DeserializeWrapper(string json, Type type, JsonSerializerOptions? options = null) + { + if (options != null) + { + return Task.FromResult(Deserialize(json, type, options)); + } + + return Task.FromResult(JsonSerializer.Deserialize(json, type, _defaultContext)); + } + + private object Deserialize(string json, Type type, JsonSerializerOptions options) + { + JsonSerializerContext context = _customContextCreator(new JsonSerializerOptions(options)); + return JsonSerializer.Deserialize(json, type, context); + } + + protected internal override Task DeserializeWrapper(string json, JsonTypeInfo jsonTypeInfo) + => throw new NotImplementedException(); + + protected internal override Task DeserializeWrapper(string json, Type type, JsonSerializerContext context) + => throw new NotImplementedException(); + } +} 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 new file mode 100644 index 0000000000000..6ebb90f030c6b --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs @@ -0,0 +1,424 @@ +// 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.Generic; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Tests; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.SourceGeneration.Tests +{ + public partial class PropertyVisibilityTests_Metadata : PropertyVisibilityTests + { + public PropertyVisibilityTests_Metadata() + : this(new JsonSerializerWrapperForString_SourceGen(PropertyVisibilityTestsContext_Metadata.Default, (options) => new PropertyVisibilityTestsContext_Metadata(options))) + { + } + + protected PropertyVisibilityTests_Metadata(JsonSerializerWrapperForString serializerWrapper) + : base(serializerWrapper) + { + } + + [Theory] + [InlineData(typeof(ClassWithBadIgnoreAttribute))] + [InlineData(typeof(StructWithBadIgnoreAttribute))] + public override async Task JsonIgnoreCondition_WhenWritingNull_OnValueType_Fail_EmptyJson(Type type) + { + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("", type)); + + InvalidOperationException ioe = await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(Activator.CreateInstance(type), type)); + string exAsStr = ioe.ToString(); + Assert.Contains("JsonIgnoreCondition.WhenWritingNull", exAsStr); + Assert.Contains("MyBadMember", exAsStr); + Assert.Contains(type.ToString(), exAsStr); + Assert.Contains("JsonIgnoreCondition.WhenWritingDefault", exAsStr); + } + + [Fact] + public override async Task Honor_JsonSerializablePropertyAttribute_OnProperties() + { + string json = @"{ + ""MyInt"":1, + ""MyString"":""Hello"", + ""MyFloat"":2, + ""MyUri"":""https://microsoft.com"" + }"; + + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + Assert.Equal(0, obj.MyInt); // Source gen can't use private setter + Assert.Equal("Hello", obj.MyString); + Assert.Equal(2f, obj.GetMyFloat); + Assert.Equal(new Uri("https://microsoft.com"), obj.MyUri); + + json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Contains(@"""MyInt"":0", json); + Assert.Contains(@"""MyString"":""Hello""", json); + Assert.DoesNotContain(@"""MyFloat"":", json); // Source gen can't use private setter + Assert.Contains(@"""MyUri"":""https://microsoft.com""", json); + } + + [Theory] + [InlineData(typeof(ClassWithInitOnlyProperty))] + [InlineData(typeof(StructWithInitOnlyProperty))] + public override async Task InitOnlyProperties(Type type) + { + // Init-only setters cannot be referenced as get/set helpers in generated code. + object obj = await JsonSerializerWrapperForString.DeserializeWrapper(@"{""MyInt"":1}", type); + Assert.Equal(0, (int)type.GetProperty("MyInt").GetValue(obj)); + + // Init-only properties can be serialized. + Assert.Equal(@"{""MyInt"":0}", await JsonSerializerWrapperForString.SerializeWrapper(obj, type)); + } + + [Theory] + [InlineData(typeof(Class_PropertyWith_PrivateInitOnlySetter_WithAttribute))] + [InlineData(typeof(Class_PropertyWith_InternalInitOnlySetter_WithAttribute))] + [InlineData(typeof(Class_PropertyWith_ProtectedInitOnlySetter_WithAttribute))] + public override async Task NonPublicInitOnlySetter_With_JsonInclude(Type type) + { + // Init-only setters cannot be referenced as get/set helpers in generated code. + object obj = await JsonSerializerWrapperForString.DeserializeWrapper(@"{""MyInt"":1}", type); + Assert.Equal(0, (int)type.GetProperty("MyInt").GetValue(obj)); + + // Init-only properties can be serialized. + Assert.Equal(@"{""MyInt"":0}", await JsonSerializerWrapperForString.SerializeWrapper(obj, type)); + } + + [Fact] + public override async Task HonorCustomConverter_UsingPrivateSetter() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + + string json = @"{""MyEnum"":""AnotherValue"",""MyInt"":2}"; + + // Deserialization baseline, without enum converter, we get JsonException. + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper(json)); + + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(json, options); + Assert.Equal(MySmallEnum.AnotherValue, obj.GetMyEnum); + Assert.Equal(0, obj.MyInt); // Private setter can't be used with source-gen. + + // ConverterForInt32 throws this exception. + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(obj, options)); + } + + [Fact] + public override async Task Public_And_NonPublicPropertyAccessors_PropertyAttributes() + { + string json = @"{""W"":1,""X"":2,""Y"":3,""Z"":4}"; + + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + Assert.Equal(1, obj.W); + Assert.Equal(2, obj.X); + Assert.Equal(3, obj.Y); + Assert.Equal(4, obj.GetZ); + + json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.Contains(@"""W"":1", json); + Assert.Contains(@"""X"":2", json); + Assert.Contains(@"""Y"":3", json); + Assert.DoesNotContain(@"""Z"":", json); // Private setter cannot be used with source gen. + } + + [Fact] + public override async Task HonorJsonPropertyName() + { + string json = @"{""prop1"":1,""prop2"":2}"; + + var obj = await JsonSerializerWrapperForString.DeserializeWrapper(json); + Assert.Equal(MySmallEnum.AnotherValue, obj.GetMyEnum); + Assert.Equal(0, obj.MyInt); // Private setter cannot be used with source gen. + + json = await JsonSerializerWrapperForString.SerializeWrapper(obj); + Assert.DoesNotContain(@"""prop1"":", json); // Private getter cannot be used with source gen. + Assert.Contains(@"""prop2"":0", json); + } + + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(ClassWithNewSlotField))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(object))] + [JsonSerializable(typeof(ClassWithInternalField))] + [JsonSerializable(typeof(ClassWithNewSlotDecimalField))] + [JsonSerializable(typeof(ClassWithNewSlotAttributedDecimalField))] + [JsonSerializable(typeof(ClassWithIgnoredPropertyPolicyConflictPrivate))] + [JsonSerializable(typeof(ClassWithMissingCollectionProperty))] + [JsonSerializable(typeof(ClassWithClassProperty_IgnoreConditionWhenWritingDefault))] + [JsonSerializable(typeof(ClassWithNoSetter))] + [JsonSerializable(typeof(ClassWithInternalProperty))] + [JsonSerializable(typeof(ClassWithPropertyNamingConflict))] + [JsonSerializable(typeof(ClassWithStructProperty_IgnoreConditionWhenWritingDefault))] + [JsonSerializable(typeof(ClassWithMissingObjectProperty))] + [JsonSerializable(typeof(ClassWithInitOnlyProperty))] + [JsonSerializable(typeof(StructWithInitOnlyProperty))] + [JsonSerializable(typeof(MyClassWithValueTypeInterfaceProperty))] + [JsonSerializable(typeof(ClassWithNonPublicProperties))] + [JsonSerializable(typeof(ClassWithProperty_IgnoreConditionAlways))] + [JsonSerializable(typeof(ClassWithBadIgnoreAttribute))] + [JsonSerializable(typeof(StructWithBadIgnoreAttribute))] + [JsonSerializable(typeof(Class_PropertyWith_InternalInitOnlySetter))] + [JsonSerializable(typeof(Class_PropertyWith_ProtectedInitOnlySetter))] + [JsonSerializable(typeof(ClassWithIgnoredPublicPropertyAndNewSlotPrivate))] + [JsonSerializable(typeof(ClassWithIgnoredPropertyPolicyConflictPublic))] + [JsonSerializable(typeof(ClassWithIgnoredPropertyNamingConflictPrivate))] + [JsonSerializable(typeof(ClassWithIgnoredNewSlotProperty))] + [JsonSerializable(typeof(ClassWithPublicGetterAndPrivateSetter))] + [JsonSerializable(typeof(ClassWithInitializedProps))] + [JsonSerializable(typeof(ClassWithNewSlotInternalProperty))] + [JsonSerializable(typeof(ClassWithPropertyPolicyConflict))] + [JsonSerializable(typeof(ClassWithPrivateSetterAndGetter))] + [JsonSerializable(typeof(ClassWithIgnoreAttributeProperty))] + [JsonSerializable(typeof(ClassWithIgnoredNewSlotField))] + [JsonSerializable(typeof(MyStruct_WithNonPublicAccessors_WithTypeAttribute))] + [JsonSerializable(typeof(ClassWithReadOnlyFields))] + [JsonSerializable(typeof(MyValueTypeWithBoxedPrimitive))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(ClassWithNoGetter))] + [JsonSerializable(typeof(ClassWithPropsAndIgnoreAttributes))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(MyValueTypeWithProperties))] + [JsonSerializable(typeof(ClassInheritedWithPropertyPolicyConflictWhichThrows))] + [JsonSerializable(typeof(ClassInheritedWithPropertyFieldPolicyConflictWhichThrows))] + [JsonSerializable(typeof(ClassWithPropertyFieldPolicyConflictWhichThrows))] + [JsonSerializable(typeof(ClassWithOverrideReversed))] + [JsonSerializable(typeof(ClassWithReadOnlyStringProperty))] + [JsonSerializable(typeof(ClassWithReadOnlyStringProperty_IgnoreNever))] + [JsonSerializable(typeof(ClassWithProps))] + [JsonSerializable(typeof(ClassWithStructProperty_IgnoreConditionNever))] + [JsonSerializable(typeof(ClassWithStructProperty_IgnoreConditionNever_Ctor))] + [JsonSerializable(typeof(ClassWithPropertyFieldNamingConflictWhichThrows))] + [JsonSerializable(typeof(ClassWithBadIgnoreAttribute))] + [JsonSerializable(typeof(StructWithBadIgnoreAttribute))] + [JsonSerializable(typeof(ClassWithPropertyNamingConflictWhichThrows))] + [JsonSerializable(typeof(ClassWithReadOnlyStringField))] + [JsonSerializable(typeof(ClassWithReadOnlyStringField_IgnoreWhenWritingDefault))] + [JsonSerializable(typeof(ClassWithReadOnlyStringField_IgnoreNever))] + [JsonSerializable(typeof(ClassInheritedWithPropertyFieldNamingConflictWhichThrows))] + [JsonSerializable(typeof(ClassTwiceInheritedWithPropertyFieldNamingConflictWhichThrows))] + [JsonSerializable(typeof(ClassWithPrivateProperty_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithInternalProperty_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithProtectedProperty_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithPrivateField_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithInternalField_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithProtectedField_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithPrivate_InitOnlyProperty_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithInternal_InitOnlyProperty_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithProtected_InitOnlyProperty_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithPublicProperty))] + [JsonSerializable(typeof(ClassInheritedWithPropertyNamingConflictWhichThrows))] + [JsonSerializable(typeof(StructWithOverride))] + [JsonSerializable(typeof(ClassTwiceInheritedWithPropertyFieldPolicyConflictWhichThrows))] + [JsonSerializable(typeof(ClassTwiceInheritedWithPropertyNamingConflictWhichThrows))] + [JsonSerializable(typeof(MyClass_WithNonPublicAccessors_WithPropertyAttributes))] + [JsonSerializable(typeof(Class_PropertyWith_PrivateInitOnlySetter))] + [JsonSerializable(typeof(Class_PropertyWith_InternalInitOnlySetter))] + [JsonSerializable(typeof(Class_PropertyWith_ProtectedInitOnlySetter))] + [JsonSerializable(typeof(Class_PropertyWith_PrivateInitOnlySetter_WithAttribute))] + [JsonSerializable(typeof(Class_PropertyWith_InternalInitOnlySetter_WithAttribute))] + [JsonSerializable(typeof(Class_PropertyWith_ProtectedInitOnlySetter_WithAttribute))] + [JsonSerializable(typeof(DerivedClass_With_IgnoredOverride))] + [JsonSerializable(typeof(DerivedClass_WithVisibleProperty_Of_DerivedClass_With_IgnoredOverride))] + [JsonSerializable(typeof(DerivedClass_With_IgnoredOverride_And_ConflictingPropertyName))] + [JsonSerializable(typeof(DerivedClass_With_Ignored_NewProperty))] + [JsonSerializable(typeof(DerivedClass_WithConflictingNewMember))] + [JsonSerializable(typeof(DerivedClass_WithConflictingNewMember_Of_DifferentType))] + [JsonSerializable(typeof(DerivedClass_With_Ignored_ConflictingNewMember))] + [JsonSerializable(typeof(DerivedClass_With_Ignored_ConflictingNewMember_Of_DifferentType))] + [JsonSerializable(typeof(DerivedClass_With_NewProperty_And_ConflictingPropertyName))] + [JsonSerializable(typeof(DerivedClass_With_Ignored_NewProperty_Of_DifferentType))] + [JsonSerializable(typeof(DerivedClass_With_Ignored_NewProperty_Of_DifferentType_And_ConflictingPropertyName))] + [JsonSerializable(typeof(FurtherDerivedClass_With_ConflictingPropertyName))] + [JsonSerializable(typeof(DerivedClass_WithConflictingPropertyName))] + [JsonSerializable(typeof(FurtherDerivedClass_With_IgnoredOverride))] + [JsonSerializable(typeof(ClassWithIgnoredPropertyNamingConflictPublic))] + [JsonSerializable(typeof(MyClassWithValueType))] + [JsonSerializable(typeof(StructWithPropertiesWithConverter))] + [JsonSerializable(typeof(ClassWithNewSlotProperty))] + [JsonSerializable(typeof(ClassWithNewSlotAttributedDecimalProperty))] + [JsonSerializable(typeof(ClassWithNewSlotDecimalProperty))] + [JsonSerializable(typeof(LargeStructWithValueAndReferenceTypes))] + [JsonSerializable(typeof(ClassWithUnsupportedBigInteger))] + [JsonSerializable(typeof(WrapperForClassWithUnsupportedBigInteger))] + [JsonSerializable(typeof(ClassWithIgnoredUnsupportedBigInteger))] + [JsonSerializable(typeof(WrapperForClassWithIgnoredUnsupportedBigInteger))] + [JsonSerializable(typeof(ClassWithThingsToIgnore))] + [JsonSerializable(typeof(ClassWithMixedPropertyAccessors_PropertyAttributes))] + [JsonSerializable(typeof(ClassWithPropertyPolicyConflictWhichThrows))] + [JsonSerializable(typeof(ClassTwiceInheritedWithPropertyPolicyConflictWhichThrows))] + [JsonSerializable(typeof(MyClass_WithNonPublicAccessors))] + [JsonSerializable(typeof(ClassWithThingsToIgnore_PerProperty))] + [JsonSerializable(typeof(StructWithPropertiesWithJsonPropertyName))] + [JsonSerializable(typeof(ClassWithValueAndReferenceTypes))] + [JsonSerializable(typeof(ClassWithReadOnlyStringProperty_IgnoreWhenWritingDefault))] + internal sealed partial class PropertyVisibilityTestsContext_Metadata : JsonSerializerContext + { + } + } + + public partial class PropertyVisibilityTests_Default : PropertyVisibilityTests_Metadata + //public partial class PropertyVisibilityTests_Default : PropertyVisibilityTests + { + public PropertyVisibilityTests_Default() + : base(new JsonSerializerWrapperForString_SourceGen(PropertyVisibilityTestsContext_Default.Default, (options) => new PropertyVisibilityTestsContext_Default(options))) + { + } + + [Theory] + [InlineData(typeof(ClassWithPrivateProperty_WithJsonIncludeProperty))] + [InlineData(typeof(ClassWithInternalProperty_WithJsonIncludeProperty))] + [InlineData(typeof(ClassWithProtectedProperty_WithJsonIncludeProperty))] + [InlineData(typeof(ClassWithPrivateField_WithJsonIncludeProperty))] + [InlineData(typeof(ClassWithInternalField_WithJsonIncludeProperty))] + [InlineData(typeof(ClassWithProtectedField_WithJsonIncludeProperty))] + [InlineData(typeof(ClassWithPrivate_InitOnlyProperty_WithJsonIncludeProperty))] + [InlineData(typeof(ClassWithInternal_InitOnlyProperty_WithJsonIncludeProperty))] + [InlineData(typeof(ClassWithProtected_InitOnlyProperty_WithJsonIncludeProperty))] + public override async Task NonPublicProperty_WithJsonInclude_Invalid(Type type) + { + // Exception messages direct users to use JsonSourceGenerationMode.Metadata to see a more detailed error. + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("{}", type)); + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(Activator.CreateInstance(type), type)); + } + + [Theory] + [InlineData(typeof(ClassWithBadIgnoreAttribute))] + [InlineData(typeof(StructWithBadIgnoreAttribute))] + public override async Task JsonIgnoreCondition_WhenWritingNull_OnValueType_Fail(Type type) + { + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("{}", type)); + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(Activator.CreateInstance(type), type)); + } + + [Theory] + [InlineData(typeof(ClassWithBadIgnoreAttribute))] + [InlineData(typeof(StructWithBadIgnoreAttribute))] + public override async Task JsonIgnoreCondition_WhenWritingNull_OnValueType_Fail_EmptyJson(Type type) + { + // Since this code goes down fast-path, there's no warm up and we hit the reader exception about having no tokens. + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.DeserializeWrapper("", type)); + await Assert.ThrowsAsync(async () => await JsonSerializerWrapperForString.SerializeWrapper(Activator.CreateInstance(type), type)); + } + + [JsonSerializable(typeof(ClassWithNewSlotField))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(object))] + [JsonSerializable(typeof(ClassWithInternalField))] + [JsonSerializable(typeof(ClassWithNewSlotDecimalField))] + [JsonSerializable(typeof(ClassWithNewSlotAttributedDecimalField))] + [JsonSerializable(typeof(ClassWithIgnoredPropertyPolicyConflictPrivate))] + [JsonSerializable(typeof(ClassWithMissingCollectionProperty))] + [JsonSerializable(typeof(ClassWithClassProperty_IgnoreConditionWhenWritingDefault))] + [JsonSerializable(typeof(ClassWithNoSetter))] + [JsonSerializable(typeof(ClassWithInternalProperty))] + [JsonSerializable(typeof(ClassWithPropertyNamingConflict))] + [JsonSerializable(typeof(ClassWithStructProperty_IgnoreConditionWhenWritingDefault))] + [JsonSerializable(typeof(ClassWithMissingObjectProperty))] + [JsonSerializable(typeof(ClassWithInitOnlyProperty))] + [JsonSerializable(typeof(StructWithInitOnlyProperty))] + [JsonSerializable(typeof(MyClassWithValueTypeInterfaceProperty))] + [JsonSerializable(typeof(ClassWithNonPublicProperties))] + [JsonSerializable(typeof(ClassWithProperty_IgnoreConditionAlways))] + [JsonSerializable(typeof(ClassWithBadIgnoreAttribute))] + [JsonSerializable(typeof(StructWithBadIgnoreAttribute))] + [JsonSerializable(typeof(Class_PropertyWith_InternalInitOnlySetter))] + [JsonSerializable(typeof(Class_PropertyWith_ProtectedInitOnlySetter))] + [JsonSerializable(typeof(ClassWithIgnoredPublicPropertyAndNewSlotPrivate))] + [JsonSerializable(typeof(ClassWithIgnoredPropertyPolicyConflictPublic))] + [JsonSerializable(typeof(ClassWithIgnoredPropertyNamingConflictPrivate))] + [JsonSerializable(typeof(ClassWithIgnoredNewSlotProperty))] + [JsonSerializable(typeof(ClassWithPublicGetterAndPrivateSetter))] + [JsonSerializable(typeof(ClassWithInitializedProps))] + [JsonSerializable(typeof(ClassWithNewSlotInternalProperty))] + [JsonSerializable(typeof(ClassWithPropertyPolicyConflict))] + [JsonSerializable(typeof(ClassWithPrivateSetterAndGetter))] + [JsonSerializable(typeof(ClassWithIgnoreAttributeProperty))] + [JsonSerializable(typeof(ClassWithIgnoredNewSlotField))] + [JsonSerializable(typeof(MyStruct_WithNonPublicAccessors_WithTypeAttribute))] + [JsonSerializable(typeof(ClassWithReadOnlyFields))] + [JsonSerializable(typeof(MyValueTypeWithBoxedPrimitive))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(ClassWithNoGetter))] + [JsonSerializable(typeof(ClassWithPropsAndIgnoreAttributes))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(MyValueTypeWithProperties))] + [JsonSerializable(typeof(ClassInheritedWithPropertyPolicyConflictWhichThrows))] + [JsonSerializable(typeof(ClassInheritedWithPropertyFieldPolicyConflictWhichThrows))] + [JsonSerializable(typeof(ClassWithPropertyFieldPolicyConflictWhichThrows))] + [JsonSerializable(typeof(ClassWithOverrideReversed))] + [JsonSerializable(typeof(ClassWithReadOnlyStringProperty))] + [JsonSerializable(typeof(ClassWithReadOnlyStringProperty_IgnoreNever))] + [JsonSerializable(typeof(ClassWithProps))] + [JsonSerializable(typeof(ClassWithStructProperty_IgnoreConditionNever))] + [JsonSerializable(typeof(ClassWithStructProperty_IgnoreConditionNever_Ctor))] + [JsonSerializable(typeof(ClassWithPropertyFieldNamingConflictWhichThrows))] + [JsonSerializable(typeof(ClassWithBadIgnoreAttribute))] + [JsonSerializable(typeof(StructWithBadIgnoreAttribute))] + [JsonSerializable(typeof(ClassWithPropertyNamingConflictWhichThrows))] + [JsonSerializable(typeof(ClassWithReadOnlyStringField))] + [JsonSerializable(typeof(ClassWithReadOnlyStringField_IgnoreWhenWritingDefault))] + [JsonSerializable(typeof(ClassWithReadOnlyStringField_IgnoreNever))] + [JsonSerializable(typeof(ClassInheritedWithPropertyFieldNamingConflictWhichThrows))] + [JsonSerializable(typeof(ClassTwiceInheritedWithPropertyFieldNamingConflictWhichThrows))] + [JsonSerializable(typeof(ClassWithPrivateProperty_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithInternalProperty_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithProtectedProperty_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithPrivateField_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithInternalField_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithProtectedField_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithPrivate_InitOnlyProperty_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithInternal_InitOnlyProperty_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithProtected_InitOnlyProperty_WithJsonIncludeProperty))] + [JsonSerializable(typeof(ClassWithPublicProperty))] + [JsonSerializable(typeof(ClassInheritedWithPropertyNamingConflictWhichThrows))] + [JsonSerializable(typeof(StructWithOverride))] + [JsonSerializable(typeof(ClassTwiceInheritedWithPropertyFieldPolicyConflictWhichThrows))] + [JsonSerializable(typeof(ClassTwiceInheritedWithPropertyNamingConflictWhichThrows))] + [JsonSerializable(typeof(MyClass_WithNonPublicAccessors_WithPropertyAttributes))] + [JsonSerializable(typeof(Class_PropertyWith_PrivateInitOnlySetter))] + [JsonSerializable(typeof(Class_PropertyWith_InternalInitOnlySetter))] + [JsonSerializable(typeof(Class_PropertyWith_ProtectedInitOnlySetter))] + [JsonSerializable(typeof(Class_PropertyWith_PrivateInitOnlySetter_WithAttribute))] + [JsonSerializable(typeof(Class_PropertyWith_InternalInitOnlySetter_WithAttribute))] + [JsonSerializable(typeof(Class_PropertyWith_ProtectedInitOnlySetter_WithAttribute))] + [JsonSerializable(typeof(DerivedClass_With_IgnoredOverride))] + [JsonSerializable(typeof(DerivedClass_WithVisibleProperty_Of_DerivedClass_With_IgnoredOverride))] + [JsonSerializable(typeof(DerivedClass_With_IgnoredOverride_And_ConflictingPropertyName))] + [JsonSerializable(typeof(DerivedClass_With_Ignored_NewProperty))] + [JsonSerializable(typeof(DerivedClass_WithConflictingNewMember))] + [JsonSerializable(typeof(DerivedClass_WithConflictingNewMember_Of_DifferentType))] + [JsonSerializable(typeof(DerivedClass_With_Ignored_ConflictingNewMember))] + [JsonSerializable(typeof(DerivedClass_With_Ignored_ConflictingNewMember_Of_DifferentType))] + [JsonSerializable(typeof(DerivedClass_With_NewProperty_And_ConflictingPropertyName))] + [JsonSerializable(typeof(DerivedClass_With_Ignored_NewProperty_Of_DifferentType))] + [JsonSerializable(typeof(DerivedClass_With_Ignored_NewProperty_Of_DifferentType_And_ConflictingPropertyName))] + [JsonSerializable(typeof(FurtherDerivedClass_With_ConflictingPropertyName))] + [JsonSerializable(typeof(DerivedClass_WithConflictingPropertyName))] + [JsonSerializable(typeof(FurtherDerivedClass_With_IgnoredOverride))] + [JsonSerializable(typeof(ClassWithIgnoredPropertyNamingConflictPublic))] + [JsonSerializable(typeof(MyClassWithValueType))] + [JsonSerializable(typeof(StructWithPropertiesWithConverter))] + [JsonSerializable(typeof(ClassWithNewSlotProperty))] + [JsonSerializable(typeof(ClassWithNewSlotAttributedDecimalProperty))] + [JsonSerializable(typeof(ClassWithNewSlotDecimalProperty))] + [JsonSerializable(typeof(LargeStructWithValueAndReferenceTypes))] + [JsonSerializable(typeof(ClassWithUnsupportedBigInteger))] + [JsonSerializable(typeof(WrapperForClassWithUnsupportedBigInteger))] + [JsonSerializable(typeof(ClassWithIgnoredUnsupportedBigInteger))] + [JsonSerializable(typeof(WrapperForClassWithIgnoredUnsupportedBigInteger))] + [JsonSerializable(typeof(ClassWithThingsToIgnore))] + [JsonSerializable(typeof(ClassWithMixedPropertyAccessors_PropertyAttributes))] + [JsonSerializable(typeof(ClassWithPropertyPolicyConflictWhichThrows))] + [JsonSerializable(typeof(ClassTwiceInheritedWithPropertyPolicyConflictWhichThrows))] + [JsonSerializable(typeof(MyClass_WithNonPublicAccessors))] + [JsonSerializable(typeof(ClassWithThingsToIgnore_PerProperty))] + [JsonSerializable(typeof(StructWithPropertiesWithJsonPropertyName))] + [JsonSerializable(typeof(ClassWithValueAndReferenceTypes))] + [JsonSerializable(typeof(ClassWithReadOnlyStringProperty_IgnoreWhenWritingDefault))] + internal sealed partial class PropertyVisibilityTestsContext_Default : JsonSerializerContext + { + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj index a88c018a1e75d..2520aa52a8b17 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj @@ -2,6 +2,12 @@ $(NetCoreAppCurrent);$(NetFrameworkCurrent) true + + $(NoWarn);SYSLIB0020 + + + + $(DefineConstants);BUILDING_SOURCE_GENERATOR_TESTS @@ -9,15 +15,38 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorTests.cs index 654b413102023..d654c01bd67d9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/JsonSourceGeneratorTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Reflection; using Microsoft.CodeAnalysis; using Xunit; @@ -395,8 +396,10 @@ private void CheckCompilationDiagnosticsErrors(ImmutableArray diagno private void CheckFieldsPropertiesMethods(Type type, string[] expectedFields, string[] expectedProperties, string[] expectedMethods) { - string[] receivedFields = type.GetFields().Select(field => field.Name).OrderBy(s => s).ToArray(); - string[] receivedProperties = type.GetProperties().Select(property => property.Name).OrderBy(s => s).ToArray(); + BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance; + + string[] receivedFields = type.GetFields(bindingFlags).Select(field => field.Name).OrderBy(s => s).ToArray(); + string[] receivedProperties = type.GetProperties(bindingFlags).Select(property => property.Name).OrderBy(s => s).ToArray(); string[] receivedMethods = type.GetMethods().Select(method => method.Name).OrderBy(s => s).ToArray(); Assert.Equal(expectedFields, receivedFields); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/TypeWrapperTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/TypeWrapperTests.cs index afcd88a5867e8..24a86137bc175 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/TypeWrapperTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.UnitTests/TypeWrapperTests.cs @@ -158,8 +158,10 @@ public void MySecondMethod() { } receivedMethodsWithAttributeNames ); + BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance; + // Check for FieldInfoWrapper attribute usage. - (string, string[])[] receivedFieldsWithAttributeNames = foundType.GetFields().Select(field => (field.Name, field.GetCustomAttributesData().Cast().Select(attributeData => attributeData.AttributeType.Name).ToArray())).Where(x => x.Item2.Any()).ToArray(); + (string, string[])[] receivedFieldsWithAttributeNames = foundType.GetFields(bindingFlags).Select(field => (field.Name, field.GetCustomAttributesData().Cast().Select(attributeData => attributeData.AttributeType.Name).ToArray())).Where(x => x.Item2.Any()).ToArray(); Assert.Equal( new (string, string[])[] { ("PublicDouble", new string[] { "JsonIncludeAttribute" }), @@ -169,7 +171,7 @@ public void MySecondMethod() { } ); // Check for PropertyInfoWrapper attribute usage. - (string, string[])[] receivedPropertyWithAttributeNames = foundType.GetProperties().Select(property => (property.Name, property.GetCustomAttributesData().Cast().Select(attributeData => attributeData.AttributeType.Name).ToArray())).Where(x => x.Item2.Any()).ToArray(); + (string, string[])[] receivedPropertyWithAttributeNames = foundType.GetProperties(bindingFlags).Select(property => (property.Name, property.GetCustomAttributesData().Cast().Select(attributeData => attributeData.AttributeType.Name).ToArray())).Where(x => x.Item2.Any()).ToArray(); Assert.Equal( new (string, string[])[] { ("PublicPropertyInt", new string[] { "JsonPropertyNameAttribute" }), diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapperForString.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapperForString.cs index b01738c7a9622..6b691004df94b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapperForString.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapperForString.cs @@ -11,7 +11,7 @@ namespace System.Text.Json.Serialization.Tests /// /// Base class for wrapping string-based JsonSerializer methods which allows tests to run under different configurations. /// - public abstract class JsonSerializerWrapperForString + public abstract partial class JsonSerializerWrapperForString { private static readonly JsonSerializerOptions _optionsWithSmallBuffer = new JsonSerializerOptions { DefaultBufferSize = 1 }; @@ -22,22 +22,6 @@ public abstract class JsonSerializerWrapperForString public static JsonSerializerWrapperForString SyncStreamSerializer => new SyncStreamSerializerWrapper(); public static JsonSerializerWrapperForString ReaderWriterSerializer => new ReaderWriterSerializerWrapper(); - protected internal abstract Task SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null); - - protected internal abstract Task SerializeWrapper(T value, JsonSerializerOptions options = null); - - protected internal abstract Task SerializeWrapper(object value, Type inputType, JsonSerializerContext context); - - protected internal abstract Task SerializeWrapper(T value, JsonTypeInfo jsonTypeInfo); - - protected internal abstract Task DeserializeWrapper(string json, JsonSerializerOptions options = null); - - protected internal abstract Task DeserializeWrapper(string json, Type type, JsonSerializerOptions options = null); - - protected internal abstract Task DeserializeWrapper(string json, JsonTypeInfo jsonTypeInfo); - - protected internal abstract Task DeserializeWrapper(string json, Type type, JsonSerializerContext context); - private class SpanSerializerWrapper : JsonSerializerWrapperForString { protected internal override Task SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapperForString_Dynamic.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapperForString_Dynamic.cs new file mode 100644 index 0000000000000..aaee9f03d81cc --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapperForString_Dynamic.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; + +namespace System.Text.Json.Serialization.Tests +{ + internal sealed class JsonSerializerWrapperForString_Dynamic + : JsonSerializerWrapperForString + { + protected internal override Task DeserializeWrapper(string json, JsonSerializerOptions options = null) + => Task.FromResult(JsonSerializer.Deserialize(json, options)); + + protected internal override Task DeserializeWrapper(string json, Type type, JsonSerializerOptions options = null) + => Task.FromResult(JsonSerializer.Deserialize(json, type, options)); + + protected internal override Task DeserializeWrapper(string json, JsonTypeInfo jsonTypeInfo) => throw new NotImplementedException(); + + protected internal override Task DeserializeWrapper(string json, Type type, JsonSerializerContext context) => throw new NotImplementedException(); + + protected internal override Task SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null) + => Task.FromResult(JsonSerializer.Serialize(value, inputType, options)); + + protected internal override Task SerializeWrapper(T value, JsonSerializerOptions options = null) + => Task.FromResult(JsonSerializer.Serialize(value, options)); + + protected internal override Task SerializeWrapper(object value, Type inputType, JsonSerializerContext context) => throw new NotImplementedException(); + + protected internal override Task SerializeWrapper(T value, JsonTypeInfo jsonTypeInfo) => throw new NotImplementedException(); + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/HighLowTemps.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/HighLowTemps.cs index 39899a5b91e57..c61356029789c 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/HighLowTemps.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/HighLowTemps.cs @@ -47,6 +47,8 @@ private static JsonPropertyInfo[] HighLowTempsPropInitFunc(JsonSerializerContext properties[0] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: true, + isPublic: true, + isVirtual: false, declaringType: typeof(HighLowTemps), propertyTypeInfo: jsonContext.Int32, converter: null, @@ -54,12 +56,15 @@ private static JsonPropertyInfo[] HighLowTempsPropInitFunc(JsonSerializerContext setter: static (obj, value) => { ((HighLowTemps)obj).High = value; }, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: nameof(Serialization.HighLowTemps.High), jsonPropertyName: null); properties[1] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: true, + isPublic: true, + isVirtual: false, declaringType: typeof(HighLowTemps), propertyTypeInfo: jsonContext.Int32, converter: null, @@ -67,6 +72,7 @@ private static JsonPropertyInfo[] HighLowTempsPropInitFunc(JsonSerializerContext setter: static (obj, value) => { ((HighLowTemps)obj).Low = value; }, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: nameof(Serialization.HighLowTemps.Low), jsonPropertyName: null); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/WeatherForecastWithPOCOs.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/WeatherForecastWithPOCOs.cs index f75200bf8b550..7d3f84451000d 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/WeatherForecastWithPOCOs.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonContext/WeatherForecastWithPOCOs.cs @@ -47,6 +47,8 @@ private static JsonPropertyInfo[] WeatherForecastWithPOCOsPropInitFunc(JsonSeria properties[0] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: true, + isPublic: true, + isVirtual: false, declaringType: typeof(WeatherForecastWithPOCOs), propertyTypeInfo: jsonContext.DateTimeOffset, converter: null, @@ -54,12 +56,15 @@ private static JsonPropertyInfo[] WeatherForecastWithPOCOsPropInitFunc(JsonSeria setter: static (obj, value) => { ((WeatherForecastWithPOCOs)obj).Date = value; }, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: nameof(Serialization.WeatherForecastWithPOCOs.Date), jsonPropertyName: null); properties[1] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: true, + isPublic: true, + isVirtual: false, declaringType: typeof(WeatherForecastWithPOCOs), propertyTypeInfo: jsonContext.Int32, converter: null, @@ -67,12 +72,15 @@ private static JsonPropertyInfo[] WeatherForecastWithPOCOsPropInitFunc(JsonSeria setter: static (obj, value) => { ((WeatherForecastWithPOCOs)obj).TemperatureCelsius = value; }, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: nameof(Serialization.WeatherForecastWithPOCOs.TemperatureCelsius), jsonPropertyName: null); properties[2] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: true, + isPublic: true, + isVirtual: false, declaringType: typeof(WeatherForecastWithPOCOs), propertyTypeInfo: jsonContext.String, converter: null, @@ -80,12 +88,15 @@ private static JsonPropertyInfo[] WeatherForecastWithPOCOsPropInitFunc(JsonSeria setter: static (obj, value) => { ((WeatherForecastWithPOCOs)obj).Summary = value; }, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: nameof(Serialization.WeatherForecastWithPOCOs.Summary), jsonPropertyName: null); properties[3] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: true, + isPublic: true, + isVirtual: false, declaringType: typeof(WeatherForecastWithPOCOs), propertyTypeInfo: jsonContext.ListSystemDateTimeOffset, converter: null, @@ -93,12 +104,15 @@ private static JsonPropertyInfo[] WeatherForecastWithPOCOsPropInitFunc(JsonSeria setter: static (obj, value) => { ((WeatherForecastWithPOCOs)obj).DatesAvailable = value; }, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: nameof(Serialization.WeatherForecastWithPOCOs.DatesAvailable), jsonPropertyName: null); properties[4] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: true, + isPublic: true, + isVirtual: false, declaringType: typeof(WeatherForecastWithPOCOs), propertyTypeInfo: jsonContext.Dictionary, converter: null, @@ -106,12 +120,15 @@ private static JsonPropertyInfo[] WeatherForecastWithPOCOsPropInitFunc(JsonSeria setter: static (obj, value) => { ((WeatherForecastWithPOCOs)obj).TemperatureRanges = value; }, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: nameof(Serialization.WeatherForecastWithPOCOs.TemperatureRanges), jsonPropertyName: null); properties[5] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: true, + isPublic: true, + isVirtual: false, declaringType: typeof(WeatherForecastWithPOCOs), propertyTypeInfo: jsonContext.StringArray, converter: null, @@ -119,12 +136,15 @@ private static JsonPropertyInfo[] WeatherForecastWithPOCOsPropInitFunc(JsonSeria setter: static (obj, value) => { ((WeatherForecastWithPOCOs)obj).SummaryWords = value; }, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: nameof(Serialization.WeatherForecastWithPOCOs.SummaryWords), jsonPropertyName: null); properties[6] = JsonMetadataServices.CreatePropertyInfo( options, isProperty: false, + isPublic: true, + isVirtual: false, declaringType: typeof(WeatherForecastWithPOCOs), propertyTypeInfo: jsonContext.String, converter: null, @@ -132,6 +152,7 @@ private static JsonPropertyInfo[] WeatherForecastWithPOCOsPropInitFunc(JsonSeria setter: static (obj, value) => { ((WeatherForecastWithPOCOs)obj).SummaryField = value; }, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: nameof(Serialization.WeatherForecastWithPOCOs.SummaryField), jsonPropertyName: null); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonMetadataServices.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonMetadataServices.cs index 227aab7bf3abb..a583b3a121b81 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonMetadataServices.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonMetadataServices.cs @@ -21,6 +21,8 @@ public void CreatePropertyInfo() ArgumentNullException ane = Assert.Throws(() => JsonMetadataServices.CreatePropertyInfo( options: null, isProperty: true, + isPublic: false, + isVirtual: false, declaringType: typeof(Point), propertyTypeInfo: JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.Int32Converter), converter: null, @@ -28,6 +30,7 @@ public void CreatePropertyInfo() setter: null, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: "MyInt", jsonPropertyName: null)); Assert.Contains("options", ane.ToString()); @@ -36,6 +39,8 @@ public void CreatePropertyInfo() ane = Assert.Throws(() => JsonMetadataServices.CreatePropertyInfo( options: options, isProperty: true, + isPublic: false, + isVirtual: false, declaringType: null, propertyTypeInfo: JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.Int32Converter), converter: null, @@ -43,6 +48,7 @@ public void CreatePropertyInfo() setter: null, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: "MyInt", jsonPropertyName: null)); Assert.Contains("declaringType", ane.ToString()); @@ -51,6 +57,8 @@ public void CreatePropertyInfo() ane = Assert.Throws(() => JsonMetadataServices.CreatePropertyInfo( options: options, isProperty: true, + isPublic: false, + isVirtual: false, declaringType: typeof(Point), propertyTypeInfo: null, converter: null, @@ -58,6 +66,7 @@ public void CreatePropertyInfo() setter: null, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: "MyInt", jsonPropertyName: null)); Assert.Contains("propertyTypeInfo", ane.ToString()); @@ -66,6 +75,8 @@ public void CreatePropertyInfo() ane = Assert.Throws(() => JsonMetadataServices.CreatePropertyInfo( options: options, isProperty: true, + isPublic: false, + isVirtual: false, declaringType: typeof(Point), propertyTypeInfo: JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.Int32Converter), converter: null, @@ -73,6 +84,7 @@ public void CreatePropertyInfo() setter: null, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: null, jsonPropertyName: null)); Assert.Contains("propertyName", ane.ToString()); @@ -81,6 +93,8 @@ public void CreatePropertyInfo() InvalidOperationException ioe = Assert.Throws(() => JsonMetadataServices.CreatePropertyInfo( options: options, isProperty: true, + isPublic: false, + isVirtual: false, declaringType: typeof(Point), // Converter invalid because you'd need to create with JsonMetadataServices.CreatePropertyInfo instead. propertyTypeInfo: JsonMetadataServices.CreateValueInfo(options, new DerivedClassConverter()), @@ -89,12 +103,31 @@ public void CreatePropertyInfo() setter: null, ignoreCondition: default, numberHandling: default, + hasJsonInclude: false, propertyName: "MyProp", jsonPropertyName: null)); string ioeAsStr = ioe.ToString(); Assert.Contains("Point.MyProp", ioeAsStr); Assert.Contains("MyClass", ioeAsStr); + // Fields cannot be virtual. + ioe = Assert.Throws(() => JsonMetadataServices.CreatePropertyInfo( + options: options, + isProperty: false, + isPublic: false, + isVirtual: true, + declaringType: typeof(Point), + propertyTypeInfo: JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.Int32Converter), + converter: null, + getter: null, + setter: null, + ignoreCondition: default, + numberHandling: default, + hasJsonInclude: false, + propertyName: "X", + jsonPropertyName: null)); + Assert.Contains("field", ioe.ToString()); + // Source generator tests verify that generated metadata is actually valid. } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonSerializer.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonSerializer.cs index b4db06777a311..5e02a3b5d6b1f 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonSerializer.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.JsonSerializer.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using Xunit; diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyVisibilityTests.cs index f164592f4fb30..85a36e92b9b1e 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PropertyVisibilityTests.cs @@ -1,2555 +1,12 @@ // 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.Generic; -using System.Collections.Concurrent; -using System.Numerics; using Xunit; namespace System.Text.Json.Serialization.Tests { - public static partial class PropertyVisibilityTests + public sealed partial class PropertyVisibilityTestsDynamic : PropertyVisibilityTests { - [Fact] - public static void Serialize_NewSlotPublicField() - { - // Serialize - var obj = new ClassWithNewSlotField(); - string json = JsonSerializer.Serialize(obj); - - Assert.Equal(@"{""MyString"":""NewDefaultValue""}", json); - - // Deserialize - json = @"{""MyString"":""NewValue""}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal("NewValue", ((ClassWithNewSlotField)obj).MyString); - Assert.Equal("DefaultValue", ((ClassWithInternalField)obj).MyString); - } - - [Fact] - public static void Serialize_NewSlotPublicProperty() - { - // Serialize - var obj = new ClassWithNewSlotProperty(); - string json = JsonSerializer.Serialize(obj); - - Assert.Equal(@"{""MyString"":""NewDefaultValue""}", json); - - // Deserialize - json = @"{""MyString"":""NewValue""}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal("NewValue", ((ClassWithNewSlotProperty)obj).MyString); - Assert.Equal("DefaultValue", ((ClassWithInternalProperty)obj).MyString); - } - - [Fact] - public static void Serialize_BasePublicProperty_ConflictWithDerivedPrivate() - { - // Serialize - var obj = new ClassWithNewSlotInternalProperty(); - string json = JsonSerializer.Serialize(obj); - - Assert.Equal(@"{""MyString"":""DefaultValue""}", json); - - // Deserialize - json = @"{""MyString"":""NewValue""}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal("NewValue", ((ClassWithPublicProperty)obj).MyString); - Assert.Equal("NewDefaultValue", ((ClassWithNewSlotInternalProperty)obj).MyString); - } - - [Fact] - public static void Serialize_PublicProperty_ConflictWithPrivateDueAttributes() - { - // Serialize - var obj = new ClassWithPropertyNamingConflict(); - - // Newtonsoft.Json throws JsonSerializationException here because - // non-public properties are included when [JsonProperty] is placed on them. - string json = JsonSerializer.Serialize(obj); - - Assert.Equal(@"{""MyString"":""DefaultValue""}", json); - - // Deserialize - json = @"{""MyString"":""NewValue""}"; - - // Newtonsoft.Json throws JsonSerializationException here because - // non-public properties are included when [JsonProperty] is placed on them. - obj = JsonSerializer.Deserialize(json); - - Assert.Equal("NewValue", obj.MyString); - Assert.Equal("ConflictingValue", obj.ConflictingString); - } - - [Fact] - public static void Serialize_PublicProperty_ConflictWithPrivateDuePolicy() - { - var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - // Serialize - var obj = new ClassWithPropertyPolicyConflict(); - string json = JsonSerializer.Serialize(obj, options); - - Assert.Equal(@"{""myString"":""DefaultValue""}", json); - - // Deserialize - json = @"{""myString"":""NewValue""}"; - obj = JsonSerializer.Deserialize(json, options); - - Assert.Equal("NewValue", obj.MyString); - Assert.Equal("ConflictingValue", obj.myString); - } - - [Fact] - public static void Serialize_NewSlotPublicProperty_ConflictWithBasePublicProperty() - { - // Serialize - var obj = new ClassWithNewSlotDecimalProperty(); - string json = JsonSerializer.Serialize(obj); - - Assert.Equal(@"{""MyNumeric"":1.5}", json); - - // Deserialize - json = @"{""MyNumeric"":2.5}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal(2.5M, obj.MyNumeric); - } - - [Fact] - public static void Serialize_NewSlotPublicField_ConflictWithBasePublicProperty() - { - // Serialize - var obj = new ClassWithNewSlotDecimalField(); - string json = JsonSerializer.Serialize(obj); - - Assert.Equal(@"{""MyNumeric"":1.5}", json); - - // Deserialize - json = @"{""MyNumeric"":2.5}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal(2.5M, obj.MyNumeric); - } - - [Fact] - public static void Serialize_NewSlotPublicField_SpecifiedJsonPropertyName() - { - // Serialize - var obj = new ClassWithNewSlotAttributedDecimalField(); - string json = JsonSerializer.Serialize(obj); - - Assert.Contains(@"""MyNewNumeric"":1.5", json); - Assert.Contains(@"""MyNumeric"":1", json); - - // Deserialize - json = @"{""MyNewNumeric"":2.5,""MyNumeric"":4}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal(4, ((ClassWithHiddenByNewSlotIntProperty)obj).MyNumeric); - Assert.Equal(2.5M, ((ClassWithNewSlotAttributedDecimalField)obj).MyNumeric); - } - - [Fact] - public static void Serialize_NewSlotPublicProperty_SpecifiedJsonPropertyName() - { - // Serialize - var obj = new ClassWithNewSlotAttributedDecimalProperty(); - string json = JsonSerializer.Serialize(obj); - - Assert.Contains(@"""MyNewNumeric"":1.5", json); - Assert.Contains(@"""MyNumeric"":1", json); - - // Deserialize - json = @"{""MyNewNumeric"":2.5,""MyNumeric"":4}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal(4, ((ClassWithHiddenByNewSlotIntProperty)obj).MyNumeric); - Assert.Equal(2.5M, ((ClassWithNewSlotAttributedDecimalProperty)obj).MyNumeric); - } - - [Fact] - public static void Ignore_NonPublicProperty() - { - // Serialize - var obj = new ClassWithInternalProperty(); - string json = JsonSerializer.Serialize(obj); - - Assert.Equal(@"{}", json); - - // Deserialize - json = @"{""MyString"":""NewValue""}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal("DefaultValue", obj.MyString); - } - - [Fact] - public static void Ignore_NewSlotPublicFieldIgnored() - { - // Serialize - var obj = new ClassWithIgnoredNewSlotField(); - string json = JsonSerializer.Serialize(obj); - - Assert.Equal(@"{}", json); - - // Deserialize - json = @"{""MyString"":""NewValue""}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal("NewDefaultValue", ((ClassWithIgnoredNewSlotField)obj).MyString); - Assert.Equal("DefaultValue", ((ClassWithInternalField)obj).MyString); - } - - [Fact] - public static void Ignore_NewSlotPublicPropertyIgnored() - { - // Serialize - var obj = new ClassWithIgnoredNewSlotProperty(); - string json = JsonSerializer.Serialize(obj); - - Assert.Equal(@"{}", json); - - // Deserialize - json = @"{""MyString"":""NewValue""}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal("NewDefaultValue", ((ClassWithIgnoredNewSlotProperty)obj).MyString); - Assert.Equal("DefaultValue", ((ClassWithInternalProperty)obj).MyString); - } - - [Fact] - public static void Ignore_BasePublicPropertyIgnored_ConflictWithDerivedPrivate() - { - // Serialize - var obj = new ClassWithIgnoredPublicPropertyAndNewSlotPrivate(); - string json = JsonSerializer.Serialize(obj); - - Assert.Equal(@"{}", json); - - // Deserialize - json = @"{""MyString"":""NewValue""}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal("DefaultValue", ((ClassWithIgnoredPublicProperty)obj).MyString); - Assert.Equal("NewDefaultValue", ((ClassWithIgnoredPublicPropertyAndNewSlotPrivate)obj).MyString); - } - - [Fact] - public static void Ignore_PublicProperty_ConflictWithPrivateDueAttributes() - { - // Serialize - var obj = new ClassWithIgnoredPropertyNamingConflictPrivate(); - string json = JsonSerializer.Serialize(obj); - - Assert.Equal(@"{}", json); - - // Newtonsoft.Json has the following output because non-public properties are included when [JsonProperty] is placed on them. - // {"MyString":"ConflictingValue"} - - // Deserialize - json = @"{""MyString"":""NewValue""}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal("DefaultValue", obj.MyString); - Assert.Equal("ConflictingValue", obj.ConflictingString); - - // The output for Newtonsoft.Json is: - // obj.ConflictingString = "NewValue" - // obj.MyString still equals "DefaultValue" - } - - [Fact] - public static void Ignore_PublicProperty_ConflictWithPrivateDuePolicy() - { - var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - // Serialize - var obj = new ClassWithIgnoredPropertyPolicyConflictPrivate(); - string json = JsonSerializer.Serialize(obj, options); - - Assert.Equal(@"{}", json); - - // Deserialize - json = @"{""myString"":""NewValue""}"; - obj = JsonSerializer.Deserialize(json, options); - - Assert.Equal("DefaultValue", obj.MyString); - Assert.Equal("ConflictingValue", obj.myString); - } - - [Fact] - public static void Ignore_PublicProperty_ConflictWithPublicDueAttributes() - { - // Serialize - var obj = new ClassWithIgnoredPropertyNamingConflictPublic(); - string json = JsonSerializer.Serialize(obj); - - Assert.Equal(@"{""MyString"":""ConflictingValue""}", json); - - // Deserialize - json = @"{""MyString"":""NewValue""}"; - obj = JsonSerializer.Deserialize(json); - - Assert.Equal("DefaultValue", obj.MyString); - Assert.Equal("NewValue", obj.ConflictingString); - } - - [Fact] - public static void Ignore_PublicProperty_ConflictWithPublicDuePolicy() - { - var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - // Serialize - var obj = new ClassWithIgnoredPropertyPolicyConflictPublic(); - string json = JsonSerializer.Serialize(obj, options); - - Assert.Equal(@"{""myString"":""ConflictingValue""}", json); - - // Deserialize - json = @"{""myString"":""NewValue""}"; - obj = JsonSerializer.Deserialize(json, options); - - Assert.Equal("DefaultValue", obj.MyString); - Assert.Equal("NewValue", obj.myString); - } - - [Fact] - public static void Throw_PublicProperty_ConflictDueAttributes() - { - // Serialize - var obj = new ClassWithPropertyNamingConflictWhichThrows(); - Assert.Throws( - () => JsonSerializer.Serialize(obj)); - - // Deserialize - string json = @"{""MyString"":""NewValue""}"; - Assert.Throws( - () => JsonSerializer.Deserialize(json)); - } - - [Fact] - public static void Throw_PublicPropertyAndField_ConflictDueAttributes() - { - // Serialize - var obj = new ClassWithPropertyFieldNamingConflictWhichThrows(); - Assert.Throws( - () => JsonSerializer.Serialize(obj)); - - // Deserialize - string json = @"{""MyString"":""NewValue""}"; - Assert.Throws( - () => JsonSerializer.Deserialize(json)); - } - - [Fact] - public static void Throw_PublicProperty_ConflictDueAttributes_SingleInheritance() - { - // Serialize - var obj = new ClassInheritedWithPropertyNamingConflictWhichThrows(); - Assert.Throws( - () => JsonSerializer.Serialize(obj)); - - // The output for Newtonsoft.Json is: - // {"MyString":"ConflictingValue"} - // Conflicts at different type-hierarchy levels that are not caused by - // deriving or the new keyword are allowed. Properties on more derived types win. - - // Deserialize - string json = @"{""MyString"":""NewValue""}"; - Assert.Throws( - () => JsonSerializer.Deserialize(json)); - - // The output for Newtonsoft.Json is: - // obj.ConflictingString = "NewValue" - // obj.MyString still equals "DefaultValue" - } - - [Fact] - public static void Throw_PublicPropertyAndField_ConflictDueAttributes_SingleInheritance() - { - // Serialize - var obj = new ClassInheritedWithPropertyFieldNamingConflictWhichThrows(); - Assert.Throws( - () => JsonSerializer.Serialize(obj)); - - // The output for Newtonsoft.Json is: - // {"MyString":"ConflictingValue"} - // Conflicts at different type-hierarchy levels that are not caused by - // deriving or the new keyword are allowed. Properties on more derived types win. - - // Deserialize - string json = @"{""MyString"":""NewValue""}"; - Assert.Throws( - () => JsonSerializer.Deserialize(json)); - - // The output for Newtonsoft.Json is: - // obj.ConflictingString = "NewValue" - // obj.MyString still equals "DefaultValue" - } - - [Fact] - public static void Throw_PublicProperty_ConflictDueAttributes_DoubleInheritance() - { - // Serialize - var obj = new ClassTwiceInheritedWithPropertyNamingConflictWhichThrows(); - Assert.Throws( - () => JsonSerializer.Serialize(obj)); - - // The output for Newtonsoft.Json is: - // {"MyString":"ConflictingValue"} - // Conflicts at different type-hierarchy levels that are not caused by - // deriving or the new keyword are allowed. Properties on more derived types win. - - // Deserialize - string json = @"{""MyString"":""NewValue""}"; - - Assert.Throws( - () => JsonSerializer.Deserialize(json)); - - // The output for Newtonsoft.Json is: - // obj.ConflictingString = "NewValue" - // obj.MyString still equals "DefaultValue" - } - - [Fact] - public static void Throw_PublicPropertyAndField_ConflictDueAttributes_DoubleInheritance() - { - // Serialize - var obj = new ClassTwiceInheritedWithPropertyFieldNamingConflictWhichThrows(); - Assert.Throws( - () => JsonSerializer.Serialize(obj)); - - // The output for Newtonsoft.Json is: - // {"MyString":"ConflictingValue"} - // Conflicts at different type-hierarchy levels that are not caused by - // deriving or the new keyword are allowed. Properties on more derived types win. - - // Deserialize - string json = @"{""MyString"":""NewValue""}"; - - Assert.Throws( - () => JsonSerializer.Deserialize(json)); - - // The output for Newtonsoft.Json is: - // obj.ConflictingString = "NewValue" - // obj.MyString still equals "DefaultValue" - } - - [Fact] - public static void Throw_PublicProperty_ConflictDuePolicy() - { - var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - // Serialize - var obj = new ClassWithPropertyPolicyConflictWhichThrows(); - Assert.Throws( - () => JsonSerializer.Serialize(obj, options)); - - // Deserialize - string json = @"{""MyString"":""NewValue""}"; - Assert.Throws( - () => JsonSerializer.Deserialize(json, options)); - } - - [Fact] - public static void Throw_PublicPropertyAndField_ConflictDuePolicy() - { - var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - // Serialize - var obj = new ClassWithPropertyFieldPolicyConflictWhichThrows(); - Assert.Throws( - () => JsonSerializer.Serialize(obj, options)); - - // Deserialize - string json = @"{""MyString"":""NewValue""}"; - Assert.Throws( - () => JsonSerializer.Deserialize(json, options)); - } - - [Fact] - public static void Throw_PublicProperty_ConflictDuePolicy_SingleInheritance() - { - var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - // Serialize - var obj = new ClassInheritedWithPropertyPolicyConflictWhichThrows(); - - Assert.Throws( - () => JsonSerializer.Serialize(obj, options)); - - // The output for Newtonsoft.Json is: - // {"myString":"ConflictingValue"} - // Conflicts at different type-hierarchy levels that are not caused by - // deriving or the new keyword are allowed. Properties on more derived types win. - - // Deserialize - string json = @"{""MyString"":""NewValue""}"; - Assert.Throws( - () => JsonSerializer.Deserialize(json, options)); - - // The output for Newtonsoft.Json is: - // obj.myString = "NewValue" - // obj.MyString still equals "DefaultValue" - } - - [Fact] - public static void Throw_PublicPropertyAndField_ConflictDuePolicy_SingleInheritance() - { - var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - // Serialize - var obj = new ClassInheritedWithPropertyFieldPolicyConflictWhichThrows(); - - Assert.Throws( - () => JsonSerializer.Serialize(obj, options)); - - // The output for Newtonsoft.Json is: - // {"myString":"ConflictingValue"} - // Conflicts at different type-hierarchy levels that are not caused by - // deriving or the new keyword are allowed. Properties on more derived types win. - - // Deserialize - string json = @"{""MyString"":""NewValue""}"; - Assert.Throws( - () => JsonSerializer.Deserialize(json, options)); - - // The output for Newtonsoft.Json is: - // obj.myString = "NewValue" - // obj.MyString still equals "DefaultValue" - } - - [Fact] - public static void Throw_PublicProperty_ConflictDuePolicy_DobuleInheritance() - { - var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - // Serialize - var obj = new ClassTwiceInheritedWithPropertyPolicyConflictWhichThrows(); - - Assert.Throws( - () => JsonSerializer.Serialize(obj, options)); - - // The output for Newtonsoft.Json is: - // {"myString":"ConflictingValue"} - // Conflicts at different type-hierarchy levels that are not caused by - // deriving or the new keyword are allowed. Properties on more derived types win. - - // Deserialize - string json = @"{""MyString"":""NewValue""}"; - - Assert.Throws( - () => JsonSerializer.Deserialize(json, options)); - - // The output for Newtonsoft.Json is: - // obj.myString = "NewValue" - // obj.MyString still equals "DefaultValue" - } - - [Fact] - public static void Throw_PublicPropertyAndField_ConflictDuePolicy_DobuleInheritance() - { - var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - // Serialize - var obj = new ClassTwiceInheritedWithPropertyFieldPolicyConflictWhichThrows(); - - Assert.Throws( - () => JsonSerializer.Serialize(obj, options)); - - // The output for Newtonsoft.Json is: - // {"myString":"ConflictingValue"} - // Conflicts at different type-hierarchy levels that are not caused by - // deriving or the new keyword are allowed. Properties on more derived types win. - - // Deserialize - string json = @"{""MyString"":""NewValue""}"; - - Assert.Throws( - () => JsonSerializer.Deserialize(json, options)); - - // The output for Newtonsoft.Json is: - // obj.myString = "NewValue" - // obj.MyString still equals "DefaultValue" - } - - [Fact] - public static void HiddenPropertiesIgnored_WhenOverridesIgnored() - { - string serialized = JsonSerializer.Serialize(new DerivedClass_With_IgnoredOverride()); - Assert.Equal(@"{}", serialized); - - serialized = JsonSerializer.Serialize(new DerivedClass_WithVisibleProperty_Of_DerivedClass_With_IgnoredOverride()); - Assert.Equal(@"{""MyProp"":false}", serialized); - - serialized = JsonSerializer.Serialize(new DerivedClass_With_IgnoredOverride_And_ConflictingPropertyName()); - Assert.Equal(@"{""MyProp"":null}", serialized); - - serialized = JsonSerializer.Serialize(new DerivedClass_With_Ignored_NewProperty()); - Assert.Equal(@"{""MyProp"":false}", serialized); - - serialized = JsonSerializer.Serialize(new DerivedClass_WithConflictingNewMember()); - Assert.Equal(@"{""MyProp"":false}", serialized); - - serialized = JsonSerializer.Serialize(new DerivedClass_WithConflictingNewMember_Of_DifferentType()); - Assert.Equal(@"{""MyProp"":0}", serialized); - - serialized = JsonSerializer.Serialize(new DerivedClass_With_Ignored_ConflictingNewMember()); - Assert.Equal(@"{""MyProp"":false}", serialized); - - serialized = JsonSerializer.Serialize(new DerivedClass_With_Ignored_ConflictingNewMember_Of_DifferentType()); - Assert.Equal(@"{""MyProp"":false}", serialized); - - serialized = JsonSerializer.Serialize(new DerivedClass_With_NewProperty_And_ConflictingPropertyName()); - Assert.Equal(@"{""MyProp"":null}", serialized); - - serialized = JsonSerializer.Serialize(new DerivedClass_With_Ignored_NewProperty_Of_DifferentType()); - Assert.Equal(@"{""MyProp"":false}", serialized); - - serialized = JsonSerializer.Serialize(new DerivedClass_With_Ignored_NewProperty_Of_DifferentType_And_ConflictingPropertyName()); - Assert.Equal(@"{""MyProp"":null}", serialized); - - serialized = JsonSerializer.Serialize(new FurtherDerivedClass_With_ConflictingPropertyName()); - Assert.Equal(@"{""MyProp"":null}", serialized); - - // Here we differ from Newtonsoft.Json, where the output would be - // {"MyProp":null} - // Conflicts at different type-hierarchy levels that are not caused by - // deriving or the new keyword are allowed. Properties on more derived types win. - // This is invalid in System.Text.Json. - Assert.Throws(() => JsonSerializer.Serialize(new DerivedClass_WithConflictingPropertyName())); - - serialized = JsonSerializer.Serialize(new FurtherDerivedClass_With_IgnoredOverride()); - Assert.Equal(@"{""MyProp"":null}", serialized); - } - - public class ClassWithInternalField - { - internal string MyString = "DefaultValue"; - } - - public class ClassWithNewSlotField : ClassWithInternalField - { - [JsonInclude] - public new string MyString = "NewDefaultValue"; - } - - public class ClassWithInternalProperty - { - internal string MyString { get; set; } = "DefaultValue"; - } - - public class ClassWithNewSlotProperty : ClassWithInternalProperty - { - public new string MyString { get; set; } = "NewDefaultValue"; - } - - public class ClassWithPublicProperty - { - public string MyString { get; set; } = "DefaultValue"; - } - - public class ClassWithNewSlotInternalProperty : ClassWithPublicProperty - { - internal new string MyString { get; set; } = "NewDefaultValue"; - } - - public class ClassWithPropertyNamingConflict - { - public string MyString { get; set; } = "DefaultValue"; - - [JsonPropertyName(nameof(MyString))] - internal string ConflictingString { get; set; } = "ConflictingValue"; - } - - public class ClassWithPropertyNamingConflictWhichThrows - { - public string MyString { get; set; } = "DefaultValue"; - - [JsonPropertyName(nameof(MyString))] - public string ConflictingString { get; set; } = "ConflictingValue"; - } - - public class ClassWithPropertyFieldNamingConflictWhichThrows - { - public string MyString { get; set; } = "DefaultValue"; - - [JsonInclude] - [JsonPropertyName(nameof(MyString))] - public string ConflictingString = "ConflictingValue"; - } - - public class ClassInheritedWithPropertyNamingConflictWhichThrows : ClassWithPublicProperty - { - [JsonPropertyName(nameof(MyString))] - public string ConflictingString { get; set; } = "ConflictingValue"; - } - - public class ClassInheritedWithPropertyFieldNamingConflictWhichThrows : ClassWithPublicProperty - { - [JsonInclude] - [JsonPropertyName(nameof(MyString))] - public string ConflictingString = "ConflictingValue"; - } - - public class ClassTwiceInheritedWithPropertyNamingConflictWhichThrowsDummy : ClassWithPublicProperty - { - } - - public class ClassTwiceInheritedWithPropertyNamingConflictWhichThrows : ClassTwiceInheritedWithPropertyNamingConflictWhichThrowsDummy - { - [JsonPropertyName(nameof(MyString))] - public string ConflictingString { get; set; } = "ConflictingValue"; - } - - public class ClassTwiceInheritedWithPropertyFieldNamingConflictWhichThrows : ClassTwiceInheritedWithPropertyNamingConflictWhichThrowsDummy - { - [JsonInclude] - [JsonPropertyName(nameof(MyString))] - public string ConflictingString = "ConflictingValue"; - } - - public class ClassWithPropertyPolicyConflict - { - public string MyString { get; set; } = "DefaultValue"; - - internal string myString { get; set; } = "ConflictingValue"; - } - - public class ClassWithPropertyPolicyConflictWhichThrows - { - public string MyString { get; set; } = "DefaultValue"; - - public string myString { get; set; } = "ConflictingValue"; - } - - public class ClassWithPropertyFieldPolicyConflictWhichThrows - { - public string MyString { get; set; } = "DefaultValue"; - - [JsonInclude] - public string myString = "ConflictingValue"; - } - - public class ClassInheritedWithPropertyPolicyConflictWhichThrows : ClassWithPublicProperty - { - public string myString { get; set; } = "ConflictingValue"; - } - - public class ClassInheritedWithPropertyFieldPolicyConflictWhichThrows : ClassWithPublicProperty - { - [JsonInclude] - public string myString = "ConflictingValue"; - } - - public class ClassInheritedWithPropertyPolicyConflictWhichThrowsDummy : ClassWithPublicProperty - { - } - - public class ClassTwiceInheritedWithPropertyPolicyConflictWhichThrows : ClassInheritedWithPropertyPolicyConflictWhichThrowsDummy - { - public string myString { get; set; } = "ConflictingValue"; - } - - public class ClassTwiceInheritedWithPropertyFieldPolicyConflictWhichThrows : ClassInheritedWithPropertyPolicyConflictWhichThrowsDummy - { - [JsonInclude] - public string myString { get; set; } = "ConflictingValue"; - } - - public class ClassWithIgnoredNewSlotField : ClassWithInternalField - { - [JsonIgnore] - public new string MyString = "NewDefaultValue"; - } - - public class ClassWithIgnoredNewSlotProperty : ClassWithInternalProperty - { - [JsonIgnore] - public new string MyString { get; set; } = "NewDefaultValue"; - } - - public class ClassWithIgnoredPublicProperty - { - [JsonIgnore] - public string MyString { get; set; } = "DefaultValue"; - } - - public class ClassWithIgnoredPublicPropertyAndNewSlotPrivate : ClassWithIgnoredPublicProperty - { - internal new string MyString { get; set; } = "NewDefaultValue"; - } - - public class ClassWithIgnoredPropertyNamingConflictPrivate - { - [JsonIgnore] - public string MyString { get; set; } = "DefaultValue"; - - [JsonPropertyName(nameof(MyString))] - internal string ConflictingString { get; set; } = "ConflictingValue"; - } - - public class ClassWithIgnoredPropertyPolicyConflictPrivate - { - [JsonIgnore] - public string MyString { get; set; } = "DefaultValue"; - - internal string myString { get; set; } = "ConflictingValue"; - } - - public class ClassWithIgnoredPropertyNamingConflictPublic - { - [JsonIgnore] - public string MyString { get; set; } = "DefaultValue"; - - [JsonPropertyName(nameof(MyString))] - public string ConflictingString { get; set; } = "ConflictingValue"; - } - - public class ClassWithIgnoredPropertyPolicyConflictPublic - { - [JsonIgnore] - public string MyString { get; set; } = "DefaultValue"; - - public string myString { get; set; } = "ConflictingValue"; - } - - public class ClassWithHiddenByNewSlotIntProperty - { - public int MyNumeric { get; set; } = 1; - } - - public class ClassWithNewSlotDecimalField : ClassWithHiddenByNewSlotIntProperty - { - [JsonInclude] - public new decimal MyNumeric = 1.5M; - } - - public class ClassWithNewSlotDecimalProperty : ClassWithHiddenByNewSlotIntProperty - { - public new decimal MyNumeric { get; set; } = 1.5M; - } - - public class ClassWithNewSlotAttributedDecimalField : ClassWithHiddenByNewSlotIntProperty - { - [JsonInclude] - [JsonPropertyName("MyNewNumeric")] - public new decimal MyNumeric = 1.5M; - } - - public class ClassWithNewSlotAttributedDecimalProperty : ClassWithHiddenByNewSlotIntProperty - { - [JsonPropertyName("MyNewNumeric")] - public new decimal MyNumeric { get; set; } = 1.5M; - } - - private class Class_With_VirtualProperty - { - public virtual bool MyProp { get; set; } - } - - private class DerivedClass_With_IgnoredOverride : Class_With_VirtualProperty - { - [JsonIgnore] - public override bool MyProp { get; set; } - } - - private class DerivedClass_WithVisibleProperty_Of_DerivedClass_With_IgnoredOverride : DerivedClass_With_IgnoredOverride - { - public override bool MyProp { get; set; } - } - - private class DerivedClass_With_IgnoredOverride_And_ConflictingPropertyName : Class_With_VirtualProperty - { - [JsonPropertyName("MyProp")] - public string MyString { get; set; } - - [JsonIgnore] - public override bool MyProp { get; set; } - } - - private class Class_With_Property - { - public bool MyProp { get; set; } - } - - private class DerivedClass_With_Ignored_NewProperty : Class_With_Property - { - [JsonIgnore] - public new bool MyProp { get; set; } - } - - private class DerivedClass_With_NewProperty_And_ConflictingPropertyName : Class_With_Property - { - [JsonPropertyName("MyProp")] - public string MyString { get; set; } - - [JsonIgnore] - public new bool MyProp { get; set; } - } - - private class DerivedClass_With_Ignored_NewProperty_Of_DifferentType : Class_With_Property - { - [JsonIgnore] - public new int MyProp { get; set; } - } - - private class DerivedClass_With_Ignored_NewProperty_Of_DifferentType_And_ConflictingPropertyName : Class_With_Property - { - [JsonPropertyName("MyProp")] - public string MyString { get; set; } - - [JsonIgnore] - public new int MyProp { get; set; } - } - - private class DerivedClass_WithIgnoredOverride : Class_With_VirtualProperty - { - [JsonIgnore] - public override bool MyProp { get; set; } - } - - private class DerivedClass_WithConflictingNewMember : Class_With_VirtualProperty - { - public new bool MyProp { get; set; } - } - - private class DerivedClass_WithConflictingNewMember_Of_DifferentType : Class_With_VirtualProperty - { - public new int MyProp { get; set; } - } - - private class DerivedClass_With_Ignored_ConflictingNewMember : Class_With_VirtualProperty - { - [JsonIgnore] - public new bool MyProp { get; set; } - } - - private class DerivedClass_With_Ignored_ConflictingNewMember_Of_DifferentType : Class_With_VirtualProperty - { - [JsonIgnore] - public new int MyProp { get; set; } - } - - private class FurtherDerivedClass_With_ConflictingPropertyName : DerivedClass_WithIgnoredOverride - { - [JsonPropertyName("MyProp")] - public string MyString { get; set; } - } - - private class DerivedClass_WithConflictingPropertyName : Class_With_VirtualProperty - { - [JsonPropertyName("MyProp")] - public string MyString { get; set; } - } - - private class FurtherDerivedClass_With_IgnoredOverride : DerivedClass_WithConflictingPropertyName - { - [JsonIgnore] - public override bool MyProp { get; set; } - } - - [Fact] - public static void IgnoreReadOnlyProperties() - { - var options = new JsonSerializerOptions(); - options.IgnoreReadOnlyProperties = true; - - var obj = new ClassWithNoSetter(); - - string json = JsonSerializer.Serialize(obj, options); - - // Collections are always serialized unless they have [JsonIgnore]. - Assert.Equal(@"{""MyInts"":[1,2]}", json); - } - - [Fact] - public static void IgnoreReadOnlyFields() - { - var options = new JsonSerializerOptions(); - options.IncludeFields = true; - options.IgnoreReadOnlyFields = true; - - var obj = new ClassWithReadOnlyFields(); - - string json = JsonSerializer.Serialize(obj, options); - - // Collections are always serialized unless they have [JsonIgnore]. - Assert.Equal(@"{""MyInts"":[1,2]}", json); - } - - [Fact] - public static void NoSetter() - { - var obj = new ClassWithNoSetter(); - - string json = JsonSerializer.Serialize(obj); - Assert.Contains(@"""MyString"":""DefaultValue""", json); - Assert.Contains(@"""MyInts"":[1,2]", json); - - obj = JsonSerializer.Deserialize(@"{""MyString"":""IgnoreMe"",""MyInts"":[0]}"); - Assert.Equal("DefaultValue", obj.MyString); - Assert.Equal(2, obj.MyInts.Length); - } - - [Fact] - public static void NoGetter() - { - ClassWithNoGetter objWithNoGetter = JsonSerializer.Deserialize( - @"{""MyString"":""Hello"",""MyIntArray"":[0],""MyIntList"":[0]}"); - - Assert.Equal("Hello", objWithNoGetter.GetMyString()); - - // Currently we don't support setters without getters. - Assert.Equal(0, objWithNoGetter.GetMyIntArray().Length); - Assert.Equal(0, objWithNoGetter.GetMyIntList().Count); - } - - [Fact] - public static void PrivateGetter() - { - var obj = new ClassWithPrivateSetterAndGetter(); - obj.SetMyString("Hello"); - - string json = JsonSerializer.Serialize(obj); - Assert.Equal(@"{}", json); - } - - [Fact] - public static void PrivateSetter() - { - string json = @"{""MyString"":""Hello""}"; - - ClassWithPrivateSetterAndGetter objCopy = JsonSerializer.Deserialize(json); - Assert.Null(objCopy.GetMyString()); - } - - [Fact] - public static void PrivateSetterPublicGetter() - { - // https://github.com/dotnet/runtime/issues/29503 - ClassWithPublicGetterAndPrivateSetter obj - = JsonSerializer.Deserialize(@"{ ""Class"": {} }"); - - Assert.NotNull(obj); - Assert.Null(obj.Class); - } - - [Fact] - public static void MissingObjectProperty() - { - ClassWithMissingObjectProperty obj - = JsonSerializer.Deserialize(@"{ ""Object"": {} }"); - - Assert.Null(obj.Collection); - } - - [Fact] - public static void MissingCollectionProperty() - { - ClassWithMissingCollectionProperty obj - = JsonSerializer.Deserialize(@"{ ""Collection"": [] }"); - - Assert.Null(obj.Object); - } - - private class ClassWithPublicGetterAndPrivateSetter - { - public NestedClass Class { get; private set; } - } - - private class NestedClass - { - } - - [Fact] - public static void JsonIgnoreAttribute() - { - var options = new JsonSerializerOptions { IncludeFields = true }; - - // Verify default state. - var obj = new ClassWithIgnoreAttributeProperty(); - Assert.Equal(@"MyString", obj.MyString); - Assert.Equal(@"MyStringWithIgnore", obj.MyStringWithIgnore); - Assert.Equal(2, obj.MyStringsWithIgnore.Length); - Assert.Equal(1, obj.MyDictionaryWithIgnore["Key"]); - Assert.Equal(3.14M, obj.MyNumeric); - - // Verify serialize. - string json = JsonSerializer.Serialize(obj, options); - Assert.Contains(@"""MyString""", json); - Assert.DoesNotContain(@"MyStringWithIgnore", json); - Assert.DoesNotContain(@"MyStringsWithIgnore", json); - Assert.DoesNotContain(@"MyDictionaryWithIgnore", json); - Assert.DoesNotContain(@"MyNumeric", json); - - // Verify deserialize default. - obj = JsonSerializer.Deserialize(@"{}", options); - Assert.Equal(@"MyString", obj.MyString); - Assert.Equal(@"MyStringWithIgnore", obj.MyStringWithIgnore); - Assert.Equal(2, obj.MyStringsWithIgnore.Length); - Assert.Equal(1, obj.MyDictionaryWithIgnore["Key"]); - Assert.Equal(3.14M, obj.MyNumeric); - - // Verify deserialize ignores the json for MyStringWithIgnore and MyStringsWithIgnore. - obj = JsonSerializer.Deserialize( - @"{""MyString"":""Hello"", ""MyStringWithIgnore"":""IgnoreMe"", ""MyStringsWithIgnore"":[""IgnoreMe""], ""MyDictionaryWithIgnore"":{""Key"":9}, ""MyNumeric"": 2.71828}", options); - Assert.Contains(@"Hello", obj.MyString); - Assert.Equal(@"MyStringWithIgnore", obj.MyStringWithIgnore); - Assert.Equal(2, obj.MyStringsWithIgnore.Length); - Assert.Equal(1, obj.MyDictionaryWithIgnore["Key"]); - Assert.Equal(3.14M, obj.MyNumeric); - } - - [Fact] - public static void JsonIgnoreAttribute_UnsupportedCollection() - { - string json = - @"{ - ""MyConcurrentDict"":{ - ""key"":""value"" - }, - ""MyIDict"":{ - ""key"":""value"" - }, - ""MyDict"":{ - ""key"":""value"" - } - }"; - string wrapperJson = - @"{ - ""MyClass"":{ - ""MyConcurrentDict"":{ - ""key"":""value"" - }, - ""MyIDict"":{ - ""key"":""value"" - }, - ""MyDict"":{ - ""key"":""value"" - } - } - }"; - - // Unsupported collections will throw on deserialize by default. - Assert.Throws(() => JsonSerializer.Deserialize(json)); - - // Using new options instance to prevent using previously cached metadata. - JsonSerializerOptions options = new JsonSerializerOptions(); - - // Unsupported collections will throw on serialize by default. - // Only when the collection contains elements. - - var dictionary = new Dictionary(); - // Uri is an unsupported dictionary key. - dictionary.Add(new Uri("http://foo"), "bar"); - - var concurrentDictionary = new ConcurrentDictionary(dictionary); - - var instance = new ClassWithUnsupportedDictionary() - { - MyConcurrentDict = concurrentDictionary, - MyIDict = dictionary - }; - - var instanceWithIgnore = new ClassWithIgnoredUnsupportedDictionary - { - MyConcurrentDict = concurrentDictionary, - MyIDict = dictionary - }; - - Assert.Throws(() => JsonSerializer.Serialize(instance, options)); - - // Unsupported collections will throw on deserialize by default if they contain elements. - options = new JsonSerializerOptions(); - Assert.Throws(() => JsonSerializer.Deserialize(wrapperJson, options)); - - options = new JsonSerializerOptions(); - // Unsupported collections will throw on serialize by default if they contain elements. - Assert.Throws(() => JsonSerializer.Serialize(instance, options)); - - // When ignored, we can serialize and deserialize without exceptions. - options = new JsonSerializerOptions(); - - Assert.NotNull(JsonSerializer.Serialize(instanceWithIgnore, options)); - - ClassWithIgnoredUnsupportedDictionary obj = JsonSerializer.Deserialize(json, options); - Assert.Null(obj.MyDict); - - options = new JsonSerializerOptions(); - Assert.Equal("{}", JsonSerializer.Serialize(new ClassWithIgnoredUnsupportedDictionary())); - - options = new JsonSerializerOptions(); - WrapperForClassWithIgnoredUnsupportedDictionary wrapperObj = JsonSerializer.Deserialize(wrapperJson, options); - Assert.Null(wrapperObj.MyClass.MyDict); - - options = new JsonSerializerOptions(); - Assert.Equal(@"{""MyClass"":{}}", JsonSerializer.Serialize(new WrapperForClassWithIgnoredUnsupportedDictionary() - { - MyClass = new ClassWithIgnoredUnsupportedDictionary(), - }, options)); - } - - [Fact] - public static void JsonIgnoreAttribute_UnsupportedBigInteger() - { - string json = @"{""MyBigInteger"":1}"; - string wrapperJson = @"{""MyClass"":{""MyBigInteger"":1}}"; - - // Unsupported types will throw by default. - Assert.Throws(() => JsonSerializer.Deserialize(json)); - // Using new options instance to prevent using previously cached metadata. - JsonSerializerOptions options = new JsonSerializerOptions(); - Assert.Throws(() => JsonSerializer.Deserialize(wrapperJson, options)); - - // When ignored, we can serialize and deserialize without exceptions. - options = new JsonSerializerOptions(); - ClassWithIgnoredUnsupportedBigInteger obj = JsonSerializer.Deserialize(json, options); - Assert.Null(obj.MyBigInteger); - - options = new JsonSerializerOptions(); - Assert.Equal("{}", JsonSerializer.Serialize(new ClassWithIgnoredUnsupportedBigInteger())); - - options = new JsonSerializerOptions(); - WrapperForClassWithIgnoredUnsupportedBigInteger wrapperObj = JsonSerializer.Deserialize(wrapperJson, options); - Assert.Null(wrapperObj.MyClass.MyBigInteger); - - options = new JsonSerializerOptions(); - Assert.Equal(@"{""MyClass"":{}}", JsonSerializer.Serialize(new WrapperForClassWithIgnoredUnsupportedBigInteger() - { - MyClass = new ClassWithIgnoredUnsupportedBigInteger(), - }, options)); - } - - public class ObjectDictWrapper : Dictionary { } - - public class ClassWithUnsupportedDictionary - { - public ConcurrentDictionary MyConcurrentDict { get; set; } - public IDictionary MyIDict { get; set; } - public ObjectDictWrapper MyDict { get; set; } - } - - public class WrapperForClassWithUnsupportedDictionary - { - public ClassWithUnsupportedDictionary MyClass { get; set; } = new ClassWithUnsupportedDictionary(); - } - - public class ClassWithIgnoredUnsupportedDictionary - { - [JsonIgnore] - public ConcurrentDictionary MyConcurrentDict { get; set; } - [JsonIgnore] - public IDictionary MyIDict { get; set; } - [JsonIgnore] - public ObjectDictWrapper MyDict { get; set; } - } - - public class WrapperForClassWithIgnoredUnsupportedDictionary - { - public ClassWithIgnoredUnsupportedDictionary MyClass { get; set; } - } - - public class ClassWithUnsupportedBigInteger - { - public BigInteger? MyBigInteger { get; set; } - } - - public class WrapperForClassWithUnsupportedBigInteger - { - public ClassWithUnsupportedBigInteger MyClass { get; set; } = new ClassWithUnsupportedBigInteger(); - } - - public class ClassWithIgnoredUnsupportedBigInteger - { - [JsonIgnore] - public BigInteger? MyBigInteger { get; set; } - } - - public class WrapperForClassWithIgnoredUnsupportedBigInteger - { - public ClassWithIgnoredUnsupportedBigInteger MyClass { get; set; } - } - - public class ClassWithMissingObjectProperty - { - public object[] Collection { get; set; } - } - - public class ClassWithMissingCollectionProperty - { - public object Object { get; set; } - } - - public class ClassWithPrivateSetterAndGetter - { - private string MyString { get; set; } - - public string GetMyString() - { - return MyString; - } - - public void SetMyString(string value) - { - MyString = value; - } - } - - public class ClassWithReadOnlyFields - { - public ClassWithReadOnlyFields() - { - MyString = "DefaultValue"; - MyInts = new int[] { 1, 2 }; - } - - public readonly string MyString; - public readonly int[] MyInts; - } - - public class ClassWithNoSetter - { - public ClassWithNoSetter() - { - MyString = "DefaultValue"; - MyInts = new int[] { 1, 2 }; - } - - public string MyString { get; } - public int[] MyInts { get; } - } - - public class ClassWithNoGetter - { - string _myString = ""; - int[] _myIntArray = new int[] { }; - List _myIntList = new List { }; - - public string MyString - { - set - { - _myString = value; - } - } - - public int[] MyIntArray - { - set - { - _myIntArray = value; - } - } - - public List MyList - { - set - { - _myIntList = value; - } - } - - public string GetMyString() - { - return _myString; - } - - public int[] GetMyIntArray() - { - return _myIntArray; - } - - public List GetMyIntList() - { - return _myIntList; - } - } - - public class ClassWithIgnoreAttributeProperty - { - public ClassWithIgnoreAttributeProperty() - { - MyDictionaryWithIgnore = new Dictionary { { "Key", 1 } }; - MyString = "MyString"; - MyStringWithIgnore = "MyStringWithIgnore"; - MyStringsWithIgnore = new string[] { "1", "2" }; - MyNumeric = 3.14M; - } - - [JsonIgnore] - public Dictionary MyDictionaryWithIgnore { get; set; } - - [JsonIgnore] - public string MyStringWithIgnore { get; set; } - - public string MyString { get; set; } - - [JsonIgnore] - public string[] MyStringsWithIgnore { get; set; } - - [JsonIgnore] - public decimal MyNumeric; - } - - private enum MyEnum - { - Case1 = 0, - Case2 = 1, - } - - private struct StructWithOverride - { - [JsonIgnore] - public MyEnum EnumValue { get; set; } - - [JsonPropertyName("EnumValue")] - public string EnumString - { - get => EnumValue.ToString(); - set - { - if (value == "Case1") - { - EnumValue = MyEnum.Case1; - } - else if (value == "Case2") - { - EnumValue = MyEnum.Case2; - } - else - { - throw new Exception("Unknown value!"); - } - } - } - } - - [Fact] - public static void OverrideJsonIgnorePropertyUsingJsonPropertyName() - { - const string json = @"{""EnumValue"":""Case2""}"; - - StructWithOverride obj = JsonSerializer.Deserialize(json); - - Assert.Equal(MyEnum.Case2, obj.EnumValue); - Assert.Equal("Case2", obj.EnumString); - - string jsonSerialized = JsonSerializer.Serialize(obj); - Assert.Equal(json, jsonSerialized); - } - - private struct ClassWithOverrideReversed - { - // Same as ClassWithOverride except the order of the properties is different, which should cause different reflection order. - [JsonPropertyName("EnumValue")] - public string EnumString - { - get => EnumValue.ToString(); - set - { - if (value == "Case1") - { - EnumValue = MyEnum.Case1; - } - if (value == "Case2") - { - EnumValue = MyEnum.Case2; - } - else - { - throw new Exception("Unknown value!"); - } - } - } - - [JsonIgnore] - public MyEnum EnumValue { get; set; } - } - - [Fact] - public static void OverrideJsonIgnorePropertyUsingJsonPropertyNameReversed() - { - const string json = @"{""EnumValue"":""Case2""}"; - - ClassWithOverrideReversed obj = JsonSerializer.Deserialize(json); - - Assert.Equal(MyEnum.Case2, obj.EnumValue); - Assert.Equal("Case2", obj.EnumString); - - string jsonSerialized = JsonSerializer.Serialize(obj); - Assert.Equal(json, jsonSerialized); - } - - [Theory] - [InlineData(typeof(ClassWithProperty_IgnoreConditionAlways))] - [InlineData(typeof(ClassWithProperty_IgnoreConditionAlways_Ctor))] - public static void JsonIgnoreConditionSetToAlwaysWorks(Type type) - { - string json = @"{""MyString"":""Random"",""MyDateTime"":""2020-03-23"",""MyInt"":4}"; - - object obj = JsonSerializer.Deserialize(json, type); - Assert.Equal("Random", (string)type.GetProperty("MyString").GetValue(obj)); - Assert.Equal(default, (DateTime)type.GetProperty("MyDateTime").GetValue(obj)); - Assert.Equal(4, (int)type.GetProperty("MyInt").GetValue(obj)); - - string serialized = JsonSerializer.Serialize(obj); - Assert.Contains(@"""MyString"":""Random""", serialized); - Assert.Contains(@"""MyInt"":4", serialized); - Assert.DoesNotContain(@"""MyDateTime"":", serialized); - } - - private class ClassWithProperty_IgnoreConditionAlways - { - public string MyString { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.Always)] - public DateTime MyDateTime { get; set; } - public int MyInt { get; set; } - } - - private class ClassWithProperty_IgnoreConditionAlways_Ctor - { - public string MyString { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.Always)] - public DateTime MyDateTime { get; } - public int MyInt { get; } - - public ClassWithProperty_IgnoreConditionAlways_Ctor(DateTime myDateTime, int myInt) - { - MyDateTime = myDateTime; - MyInt = myInt; - } - } - - [Theory] - [MemberData(nameof(JsonIgnoreConditionWhenWritingDefault_ClassProperty_TestData))] - public static void JsonIgnoreConditionWhenWritingDefault_ClassProperty(Type type, JsonSerializerOptions options) - { - // Property shouldn't be ignored if it isn't null. - string json = @"{""Int1"":1,""MyString"":""Random"",""Int2"":2}"; - - object obj = JsonSerializer.Deserialize(json, type, options); - Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); - Assert.Equal("Random", (string)type.GetProperty("MyString").GetValue(obj)); - Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); - - string serialized = JsonSerializer.Serialize(obj, options); - Assert.Contains(@"""Int1"":1", serialized); - Assert.Contains(@"""MyString"":""Random""", serialized); - Assert.Contains(@"""Int2"":2", serialized); - - // Property should be ignored when null. - json = @"{""Int1"":1,""MyString"":null,""Int2"":2}"; - - obj = JsonSerializer.Deserialize(json, type, options); - Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); - - if (options.IgnoreNullValues) - { - // Null values can be ignored on deserialization using IgnoreNullValues. - Assert.Equal("DefaultString", (string)type.GetProperty("MyString").GetValue(obj)); - } - else - { - Assert.Null((string)type.GetProperty("MyString").GetValue(obj)); - } - - Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); - - // Set property to be ignored to null. - type.GetProperty("MyString").SetValue(obj, null); - - serialized = JsonSerializer.Serialize(obj, options); - Assert.Contains(@"""Int1"":1", serialized); - Assert.Contains(@"""Int2"":2", serialized); - Assert.DoesNotContain(@"""MyString"":", serialized); - } - - private class ClassWithClassProperty_IgnoreConditionWhenWritingDefault - { - public int Int1 { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string MyString { get; set; } = "DefaultString"; - public int Int2 { get; set; } - } - - private class ClassWithClassProperty_IgnoreConditionWhenWritingDefault_Ctor - { - public int Int1 { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string MyString { get; set; } = "DefaultString"; - public int Int2 { get; set; } - - public ClassWithClassProperty_IgnoreConditionWhenWritingDefault_Ctor(string myString) - { - if (myString != null) - { - MyString = myString; - } - } - } - - public static IEnumerable JsonIgnoreConditionWhenWritingDefault_ClassProperty_TestData() - { - yield return new object[] { typeof(ClassWithClassProperty_IgnoreConditionWhenWritingDefault), new JsonSerializerOptions() }; - yield return new object[] { typeof(ClassWithClassProperty_IgnoreConditionWhenWritingDefault_Ctor), new JsonSerializerOptions { IgnoreNullValues = true } }; - } - - [Theory] - [MemberData(nameof(JsonIgnoreConditionWhenWritingDefault_StructProperty_TestData))] - public static void JsonIgnoreConditionWhenWritingDefault_StructProperty(Type type, JsonSerializerOptions options) - { - // Property shouldn't be ignored if it isn't null. - string json = @"{""Int1"":1,""MyInt"":3,""Int2"":2}"; - - object obj = JsonSerializer.Deserialize(json, type, options); - Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); - Assert.Equal(3, (int)type.GetProperty("MyInt").GetValue(obj)); - Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); - - string serialized = JsonSerializer.Serialize(obj, options); - Assert.Contains(@"""Int1"":1", serialized); - Assert.Contains(@"""MyInt"":3", serialized); - Assert.Contains(@"""Int2"":2", serialized); - - // Null being assigned to non-nullable types is invalid. - json = @"{""Int1"":1,""MyInt"":null,""Int2"":2}"; - Assert.Throws(() => JsonSerializer.Deserialize(json, type, options)); - } - - private class ClassWithStructProperty_IgnoreConditionWhenWritingDefault - { - public int Int1 { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public int MyInt { get; set; } - public int Int2 { get; set; } - } - - private struct StructWithStructProperty_IgnoreConditionWhenWritingDefault_Ctor - { - public int Int1 { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public int MyInt { get; } - public int Int2 { get; set; } - - [JsonConstructor] - public StructWithStructProperty_IgnoreConditionWhenWritingDefault_Ctor(int myInt) - { - Int1 = 0; - MyInt = myInt; - Int2 = 0; - } - } - - public static IEnumerable JsonIgnoreConditionWhenWritingDefault_StructProperty_TestData() - { - yield return new object[] { typeof(ClassWithStructProperty_IgnoreConditionWhenWritingDefault), new JsonSerializerOptions() }; - yield return new object[] { typeof(StructWithStructProperty_IgnoreConditionWhenWritingDefault_Ctor), new JsonSerializerOptions { IgnoreNullValues = true } }; - } - - [Theory] - [MemberData(nameof(JsonIgnoreConditionNever_TestData))] - public static void JsonIgnoreConditionNever(Type type) - { - // Property should always be (de)serialized, even when null. - string json = @"{""Int1"":1,""MyString"":""Random"",""Int2"":2}"; - - object obj = JsonSerializer.Deserialize(json, type); - Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); - Assert.Equal("Random", (string)type.GetProperty("MyString").GetValue(obj)); - Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); - - string serialized = JsonSerializer.Serialize(obj); - Assert.Contains(@"""Int1"":1", serialized); - Assert.Contains(@"""MyString"":""Random""", serialized); - Assert.Contains(@"""Int2"":2", serialized); - - // Property should always be (de)serialized, even when null. - json = @"{""Int1"":1,""MyString"":null,""Int2"":2}"; - - obj = JsonSerializer.Deserialize(json, type); - Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); - Assert.Null((string)type.GetProperty("MyString").GetValue(obj)); - Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); - - serialized = JsonSerializer.Serialize(obj); - Assert.Contains(@"""Int1"":1", serialized); - Assert.Contains(@"""MyString"":null", serialized); - Assert.Contains(@"""Int2"":2", serialized); - } - - [Theory] - [MemberData(nameof(JsonIgnoreConditionNever_TestData))] - public static void JsonIgnoreConditionNever_IgnoreNullValues_True(Type type) - { - // Property should always be (de)serialized. - string json = @"{""Int1"":1,""MyString"":""Random"",""Int2"":2}"; - var options = new JsonSerializerOptions { IgnoreNullValues = true }; - - object obj = JsonSerializer.Deserialize(json, type, options); - Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); - Assert.Equal("Random", (string)type.GetProperty("MyString").GetValue(obj)); - Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); - - string serialized = JsonSerializer.Serialize(obj, options); - Assert.Contains(@"""Int1"":1", serialized); - Assert.Contains(@"""MyString"":""Random""", serialized); - Assert.Contains(@"""Int2"":2", serialized); - - // Property should always be (de)serialized, even when null. - json = @"{""Int1"":1,""MyString"":null,""Int2"":2}"; - - obj = JsonSerializer.Deserialize(json, type, options); - Assert.Equal(1, (int)type.GetProperty("Int1").GetValue(obj)); - Assert.Null((string)type.GetProperty("MyString").GetValue(obj)); - Assert.Equal(2, (int)type.GetProperty("Int2").GetValue(obj)); - - serialized = JsonSerializer.Serialize(obj, options); - Assert.Contains(@"""Int1"":1", serialized); - Assert.Contains(@"""MyString"":null", serialized); - Assert.Contains(@"""Int2"":2", serialized); - } - - private class ClassWithStructProperty_IgnoreConditionNever - { - public int Int1 { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string MyString { get; set; } - public int Int2 { get; set; } - } - - private class ClassWithStructProperty_IgnoreConditionNever_Ctor - { - public int Int1 { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string MyString { get; } - public int Int2 { get; set; } - - public ClassWithStructProperty_IgnoreConditionNever_Ctor(string myString) - { - MyString = myString; - } - } - - public static IEnumerable JsonIgnoreConditionNever_TestData() - { - yield return new object[] { typeof(ClassWithStructProperty_IgnoreConditionNever) }; - yield return new object[] { typeof(ClassWithStructProperty_IgnoreConditionNever_Ctor) }; - } - - [Fact] - public static void JsonIgnoreCondition_LastOneWins() - { - string json = @"{""MyString"":""Random"",""MYSTRING"":null}"; - - var options = new JsonSerializerOptions - { - IgnoreNullValues = true, - PropertyNameCaseInsensitive = true - }; - var obj = JsonSerializer.Deserialize(json, options); - - Assert.Null(obj.MyString); - } - - [Fact] - public static void ClassWithComplexObjectsUsingIgnoreWhenWritingDefaultAttribute() - { - string json = @"{""Class"":{""MyInt16"":18}, ""Dictionary"":null}"; - - ClassUsingIgnoreWhenWritingDefaultAttribute obj = JsonSerializer.Deserialize(json); - - // Class is deserialized. - Assert.NotNull(obj.Class); - Assert.Equal(18, obj.Class.MyInt16); - - // Dictionary is deserialized as JsonIgnoreCondition.WhenWritingDefault only applies to deserialization. - Assert.Null(obj.Dictionary); - - obj = new ClassUsingIgnoreWhenWritingDefaultAttribute(); - json = JsonSerializer.Serialize(obj); - Assert.Equal(@"{""Dictionary"":{""Key"":""Value""}}", json); - } - - public class ClassUsingIgnoreWhenWritingDefaultAttribute - { - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public SimpleTestClass Class { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public Dictionary Dictionary { get; set; } = new Dictionary { ["Key"] = "Value" }; - } - - [Fact] - public static void ClassWithComplexObjectUsingIgnoreNeverAttribute() - { - string json = @"{""Class"":null, ""Dictionary"":null}"; - var options = new JsonSerializerOptions { IgnoreNullValues = true }; - - var obj = JsonSerializer.Deserialize(json, options); - - // Class is not deserialized because it is null in json. - Assert.NotNull(obj.Class); - Assert.Equal(18, obj.Class.MyInt16); - - // Dictionary is deserialized regardless of being null in json. - Assert.Null(obj.Dictionary); - - // Serialize when values are null. - obj = new ClassUsingIgnoreNeverAttribute(); - obj.Class = null; - obj.Dictionary = null; - - json = JsonSerializer.Serialize(obj, options); - - // Class is not included in json because it was null, Dictionary is included regardless of being null. - Assert.Equal(@"{""Dictionary"":null}", json); - } - - public class ClassUsingIgnoreNeverAttribute - { - public SimpleTestClass Class { get; set; } = new SimpleTestClass { MyInt16 = 18 }; - - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public Dictionary Dictionary { get; set; } = new Dictionary { ["Key"] = "Value" }; - } - - [Fact] - public static void IgnoreConditionNever_WinsOver_IgnoreReadOnlyProperties() - { - var options = new JsonSerializerOptions { IgnoreReadOnlyProperties = true }; - - // Baseline - string json = JsonSerializer.Serialize(new ClassWithReadOnlyStringProperty("Hello"), options); - Assert.Equal("{}", json); - - // With condition to never ignore - json = JsonSerializer.Serialize(new ClassWithReadOnlyStringProperty_IgnoreNever("Hello"), options); - Assert.Equal(@"{""MyString"":""Hello""}", json); - - json = JsonSerializer.Serialize(new ClassWithReadOnlyStringProperty_IgnoreNever(null), options); - Assert.Equal(@"{""MyString"":null}", json); - } - - [Fact] - public static void IgnoreConditionWhenWritingDefault_WinsOver_IgnoreReadOnlyProperties() - { - var options = new JsonSerializerOptions { IgnoreReadOnlyProperties = true }; - - // Baseline - string json = JsonSerializer.Serialize(new ClassWithReadOnlyStringProperty("Hello"), options); - Assert.Equal("{}", json); - - // With condition to ignore when null - json = JsonSerializer.Serialize(new ClassWithReadOnlyStringProperty_IgnoreWhenWritingDefault("Hello"), options); - Assert.Equal(@"{""MyString"":""Hello""}", json); - - json = JsonSerializer.Serialize(new ClassWithReadOnlyStringProperty_IgnoreWhenWritingDefault(null), options); - Assert.Equal(@"{}", json); - } - - [Fact] - public static void IgnoreConditionNever_WinsOver_IgnoreReadOnlyFields() - { - var options = new JsonSerializerOptions { IgnoreReadOnlyProperties = true }; - - // Baseline - string json = JsonSerializer.Serialize(new ClassWithReadOnlyStringField("Hello"), options); - Assert.Equal("{}", json); - - // With condition to never ignore - json = JsonSerializer.Serialize(new ClassWithReadOnlyStringField_IgnoreNever("Hello"), options); - Assert.Equal(@"{""MyString"":""Hello""}", json); - - json = JsonSerializer.Serialize(new ClassWithReadOnlyStringField_IgnoreNever(null), options); - Assert.Equal(@"{""MyString"":null}", json); - } - - [Fact] - public static void IgnoreConditionWhenWritingDefault_WinsOver_IgnoreReadOnlyFields() - { - var options = new JsonSerializerOptions { IgnoreReadOnlyProperties = true }; - - // Baseline - string json = JsonSerializer.Serialize(new ClassWithReadOnlyStringField("Hello"), options); - Assert.Equal("{}", json); - - // With condition to ignore when null - json = JsonSerializer.Serialize(new ClassWithReadOnlyStringField_IgnoreWhenWritingDefault("Hello"), options); - Assert.Equal(@"{""MyString"":""Hello""}", json); - - json = JsonSerializer.Serialize(new ClassWithReadOnlyStringField_IgnoreWhenWritingDefault(null), options); - Assert.Equal(@"{}", json); - } - - private class ClassWithReadOnlyStringProperty - { - public string MyString { get; } - - public ClassWithReadOnlyStringProperty(string myString) => MyString = myString; - } - - private class ClassWithReadOnlyStringProperty_IgnoreNever - { - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string MyString { get; } - - public ClassWithReadOnlyStringProperty_IgnoreNever(string myString) => MyString = myString; - } - - private class ClassWithReadOnlyStringProperty_IgnoreWhenWritingDefault - { - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string MyString { get; } - - public ClassWithReadOnlyStringProperty_IgnoreWhenWritingDefault(string myString) => MyString = myString; - } - - private class ClassWithReadOnlyStringField - { - public string MyString { get; } - - public ClassWithReadOnlyStringField(string myString) => MyString = myString; - } - - private class ClassWithReadOnlyStringField_IgnoreNever - { - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string MyString { get; } - - public ClassWithReadOnlyStringField_IgnoreNever(string myString) => MyString = myString; - } - - private class ClassWithReadOnlyStringField_IgnoreWhenWritingDefault - { - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string MyString { get; } - - public ClassWithReadOnlyStringField_IgnoreWhenWritingDefault(string myString) => MyString = myString; - } - - [Fact] - public static void NonPublicMembersAreNotIncluded() - { - Assert.Equal("{}", JsonSerializer.Serialize(new ClassWithNonPublicProperties())); - - string json = @"{""MyInt"":1,""MyString"":""Hello"",""MyFloat"":2,""MyDouble"":3}"; - var obj = JsonSerializer.Deserialize(json); - Assert.Equal(0, obj.MyInt); - Assert.Null(obj.MyString); - Assert.Equal(0, obj.GetMyFloat); - Assert.Equal(0, obj.GetMyDouble); - } - - private class ClassWithNonPublicProperties - { - internal int MyInt { get; set; } - internal string MyString { get; private set; } - internal float MyFloat { private get; set; } - private double MyDouble { get; set; } - - internal float GetMyFloat => MyFloat; - internal double GetMyDouble => MyDouble; - } - - [Fact] - public static void IgnoreCondition_WhenWritingDefault_Globally_Works() - { - // Baseline - default values written. - string expected = @"{""MyString"":null,""MyInt"":0,""MyPoint"":{""X"":0,""Y"":0}}"; - var obj = new ClassWithProps(); - JsonTestHelper.AssertJsonEqual(expected, JsonSerializer.Serialize(obj)); - - // Default values ignored when specified. - Assert.Equal("{}", JsonSerializer.Serialize(obj, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault })); - } - - private class ClassWithProps - { - public string MyString { get; set; } - public int MyInt { get; set; } - public Point_2D_Struct MyPoint { get; set; } - } - - [Fact] - public static void IgnoreCondition_WhenWritingDefault_PerProperty_Works() - { - // Default values ignored when specified. - Assert.Equal(@"{""MyInt"":0}", JsonSerializer.Serialize(new ClassWithPropsAndIgnoreAttributes())); - } - - private class ClassWithPropsAndIgnoreAttributes - { - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string MyString { get; set; } - public int MyInt { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public Point_2D_Struct MyPoint { get; set; } - } - - [Fact] - public static void IgnoreCondition_WhenWritingDefault_DoesNotApplyToCollections() - { - var list = new List { false, true }; - - var options = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault }; - Assert.Equal("[false,true]", JsonSerializer.Serialize(list, options)); - } - - [Fact] - public static void IgnoreCondition_WhenWritingDefault_DoesNotApplyToDeserialization() - { - // Baseline - null values are ignored on deserialization when using IgnoreNullValues (for compat with initial support). - string json = @"{""MyString"":null,""MyInt"":0,""MyPoint"":{""X"":0,""Y"":0}}"; - - var options = new JsonSerializerOptions { IgnoreNullValues = true }; - ClassWithInitializedProps obj = JsonSerializer.Deserialize(json, options); - - Assert.Equal("Default", obj.MyString); - // Value types are not ignored. - Assert.Equal(0, obj.MyInt); - Assert.Equal(0, obj.MyPoint.X); - Assert.Equal(0, obj.MyPoint.X); - - // Test - default values (both null and default for value types) are not ignored when using - // JsonIgnoreCondition.WhenWritingDefault (as the option name implies) - options = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault }; - obj = JsonSerializer.Deserialize(json, options); - Assert.Null(obj.MyString); - Assert.Equal(0, obj.MyInt); - Assert.Equal(0, obj.MyPoint.X); - Assert.Equal(0, obj.MyPoint.X); - } - - private class ClassWithInitializedProps - { - public string MyString { get; set; } = "Default"; - public int MyInt { get; set; } = -1; - public Point_2D_Struct MyPoint { get; set; } = new Point_2D_Struct(-1, -1); - } - - [Fact] - public static void ValueType_Properties_NotIgnoredWhen_IgnoreNullValues_Active_ClassTest() - { - var options = new JsonSerializerOptions { IgnoreNullValues = true }; - - // Deserialization. - string json = @"{""MyString"":null,""MyInt"":0,""MyBool"":null,""MyPointClass"":null,""MyPointStruct"":{""X"":0,""Y"":0}}"; - - ClassWithValueAndReferenceTypes obj = JsonSerializer.Deserialize(json, options); - - // Null values ignored for reference types/nullable value types. - Assert.Equal("Default", obj.MyString); - Assert.NotNull(obj.MyPointClass); - Assert.True(obj.MyBool); - - // Default values not ignored for value types. - Assert.Equal(0, obj.MyInt); - Assert.Equal(0, obj.MyPointStruct.X); - Assert.Equal(0, obj.MyPointStruct.Y); - - // Serialization. - - // Make all members their default CLR value. - obj.MyString = null; - obj.MyPointClass = null; - obj.MyBool = null; - - json = JsonSerializer.Serialize(obj, options); - - // Null values not serialized, default values for value types serialized. - JsonTestHelper.AssertJsonEqual(@"{""MyInt"":0,""MyPointStruct"":{""X"":0,""Y"":0}}", json); - } - - [Fact] - public static void ValueType_Properties_NotIgnoredWhen_IgnoreNullValues_Active_LargeStructTest() - { - var options = new JsonSerializerOptions { IgnoreNullValues = true }; - - // Deserialization. - string json = @"{""MyString"":null,""MyInt"":0,""MyBool"":null,""MyPointClass"":null,""MyPointStruct"":{""X"":0,""Y"":0}}"; - - LargeStructWithValueAndReferenceTypes obj = JsonSerializer.Deserialize(json, options); - - // Null values ignored for reference types. - - Assert.Equal("Default", obj.MyString); - // No way to specify a non-constant default before construction with ctor, so this remains null. - Assert.Null(obj.MyPointClass); - Assert.True(obj.MyBool); - - // Default values not ignored for value types. - Assert.Equal(0, obj.MyInt); - Assert.Equal(0, obj.MyPointStruct.X); - Assert.Equal(0, obj.MyPointStruct.Y); - - // Serialization. - - // Make all members their default CLR value. - obj = new LargeStructWithValueAndReferenceTypes(null, new Point_2D_Struct(0, 0), null, 0, null); - - json = JsonSerializer.Serialize(obj, options); - - // Null values not serialized, default values for value types serialized. - JsonTestHelper.AssertJsonEqual(@"{""MyInt"":0,""MyPointStruct"":{""X"":0,""Y"":0}}", json); - } - - [Fact] - public static void ValueType_Properties_NotIgnoredWhen_IgnoreNullValues_Active_SmallStructTest() - { - var options = new JsonSerializerOptions { IgnoreNullValues = true }; - - // Deserialization. - string json = @"{""MyString"":null,""MyInt"":0,""MyBool"":null,""MyPointStruct"":{""X"":0,""Y"":0}}"; - - SmallStructWithValueAndReferenceTypes obj = JsonSerializer.Deserialize(json, options); - - // Null values ignored for reference types. - Assert.Equal("Default", obj.MyString); - Assert.True(obj.MyBool); - - // Default values not ignored for value types. - Assert.Equal(0, obj.MyInt); - Assert.Equal(0, obj.MyPointStruct.X); - Assert.Equal(0, obj.MyPointStruct.Y); - - // Serialization. - - // Make all members their default CLR value. - obj = new SmallStructWithValueAndReferenceTypes(new Point_2D_Struct(0, 0), null, 0, null); - - json = JsonSerializer.Serialize(obj, options); - - // Null values not serialized, default values for value types serialized. - JsonTestHelper.AssertJsonEqual(@"{""MyInt"":0,""MyPointStruct"":{""X"":0,""Y"":0}}", json); - } - - private class ClassWithValueAndReferenceTypes - { - public string MyString { get; set; } = "Default"; - public int MyInt { get; set; } = -1; - public bool? MyBool { get; set; } = true; - public PointClass MyPointClass { get; set; } = new PointClass(); - public Point_2D_Struct MyPointStruct { get; set; } = new Point_2D_Struct(1, 2); - } - - private struct LargeStructWithValueAndReferenceTypes - { - public string MyString { get; } - public int MyInt { get; set; } - public bool? MyBool { get; set; } - public PointClass MyPointClass { get; set; } - public Point_2D_Struct MyPointStruct { get; set; } - - [JsonConstructor] - public LargeStructWithValueAndReferenceTypes( - PointClass myPointClass, - Point_2D_Struct myPointStruct, - string myString = "Default", - int myInt = -1, - bool? myBool = true) - { - MyString = myString; - MyInt = myInt; - MyBool = myBool; - MyPointClass = myPointClass; - MyPointStruct = myPointStruct; - } - } - - private struct SmallStructWithValueAndReferenceTypes - { - public string MyString { get; } - public int MyInt { get; set; } - public bool? MyBool { get; set; } - public Point_2D_Struct MyPointStruct { get; set; } - - [JsonConstructor] - public SmallStructWithValueAndReferenceTypes( - Point_2D_Struct myPointStruct, - string myString = "Default", - int myInt = -1, - bool? myBool = true) - { - MyString = myString; - MyInt = myInt; - MyBool = myBool; - MyPointStruct = myPointStruct; - } - } - - public class PointClass { } - - [Fact] - public static void Ignore_WhenWritingNull_Globally() - { - var options = new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - IncludeFields = true - }; - - string json = @"{ -""MyPointClass2_IgnoredWhenWritingNull"":{}, -""MyString1_IgnoredWhenWritingNull"":""Default"", -""MyNullableBool1_IgnoredWhenWritingNull"":null, -""MyInt2"":0, -""MyPointStruct2"":{""X"":1,""Y"":2}, -""MyInt1"":1, -""MyString2_IgnoredWhenWritingNull"":null, -""MyPointClass1_IgnoredWhenWritingNull"":null, -""MyNullableBool2_IgnoredWhenWritingNull"":true, -""MyPointStruct1"":{""X"":0,""Y"":0} -}"; - - // All members should correspond to JSON contents, as ignore doesn't apply to deserialization. - ClassWithThingsToIgnore obj = JsonSerializer.Deserialize(json, options); - Assert.NotNull(obj.MyPointClass2_IgnoredWhenWritingNull); - Assert.Equal("Default", obj.MyString1_IgnoredWhenWritingNull); - Assert.Null(obj.MyNullableBool1_IgnoredWhenWritingNull); - Assert.Equal(0, obj.MyInt2); - Assert.Equal(1, obj.MyPointStruct2.X); - Assert.Equal(2, obj.MyPointStruct2.Y); - Assert.Equal(1, obj.MyInt1); - Assert.Null(obj.MyString2_IgnoredWhenWritingNull); - Assert.Null(obj.MyPointClass1_IgnoredWhenWritingNull); - Assert.True(obj.MyNullableBool2_IgnoredWhenWritingNull); - Assert.Equal(0, obj.MyPointStruct1.X); - Assert.Equal(0, obj.MyPointStruct1.Y); - - // Ignore null as appropriate during serialization. - string expectedJson = @"{ -""MyPointClass2_IgnoredWhenWritingNull"":{}, -""MyString1_IgnoredWhenWritingNull"":""Default"", -""MyInt2"":0, -""MyPointStruct2"":{""X"":1,""Y"":2}, -""MyInt1"":1, -""MyNullableBool2_IgnoredWhenWritingNull"":true, -""MyPointStruct1"":{""X"":0,""Y"":0} -}"; - JsonTestHelper.AssertJsonEqual(expectedJson, JsonSerializer.Serialize(obj, options)); - } - - public class ClassWithThingsToIgnore - { - public string MyString1_IgnoredWhenWritingNull { get; set; } - - public string MyString2_IgnoredWhenWritingNull; - - public int MyInt1; - - public int MyInt2 { get; set; } - - public bool? MyNullableBool1_IgnoredWhenWritingNull { get; set; } - - public bool? MyNullableBool2_IgnoredWhenWritingNull; - - public PointClass MyPointClass1_IgnoredWhenWritingNull; - - public PointClass MyPointClass2_IgnoredWhenWritingNull { get; set; } - - public Point_2D_Struct_WithAttribute MyPointStruct1; - - public Point_2D_Struct_WithAttribute MyPointStruct2 { get; set; } - } - - [Fact] - public static void Ignore_WhenWritingNull_PerProperty() - { - var options = new JsonSerializerOptions - { - IncludeFields = true - }; - - string json = @"{ -""MyPointClass2_IgnoredWhenWritingNull"":{}, -""MyString1_IgnoredWhenWritingNull"":""Default"", -""MyNullableBool1_IgnoredWhenWritingNull"":null, -""MyInt2"":0, -""MyPointStruct2"":{""X"":1,""Y"":2}, -""MyInt1"":1, -""MyString2_IgnoredWhenWritingNull"":null, -""MyPointClass1_IgnoredWhenWritingNull"":null, -""MyNullableBool2_IgnoredWhenWritingNull"":true, -""MyPointStruct1"":{""X"":0,""Y"":0} -}"; - - // All members should correspond to JSON contents, as ignore doesn't apply to deserialization. - ClassWithThingsToIgnore_PerProperty obj = JsonSerializer.Deserialize(json, options); - Assert.NotNull(obj.MyPointClass2_IgnoredWhenWritingNull); - Assert.Equal("Default", obj.MyString1_IgnoredWhenWritingNull); - Assert.Null(obj.MyNullableBool1_IgnoredWhenWritingNull); - Assert.Equal(0, obj.MyInt2); - Assert.Equal(1, obj.MyPointStruct2.X); - Assert.Equal(2, obj.MyPointStruct2.Y); - Assert.Equal(1, obj.MyInt1); - Assert.Null(obj.MyString2_IgnoredWhenWritingNull); - Assert.Null(obj.MyPointClass1_IgnoredWhenWritingNull); - Assert.True(obj.MyNullableBool2_IgnoredWhenWritingNull); - Assert.Equal(0, obj.MyPointStruct1.X); - Assert.Equal(0, obj.MyPointStruct1.Y); - - // Ignore null as appropriate during serialization. - string expectedJson = @"{ -""MyPointClass2_IgnoredWhenWritingNull"":{}, -""MyString1_IgnoredWhenWritingNull"":""Default"", -""MyInt2"":0, -""MyPointStruct2"":{""X"":1,""Y"":2}, -""MyInt1"":1, -""MyNullableBool2_IgnoredWhenWritingNull"":true, -""MyPointStruct1"":{""X"":0,""Y"":0} -}"; - JsonTestHelper.AssertJsonEqual(expectedJson, JsonSerializer.Serialize(obj, options)); - } - - public class ClassWithThingsToIgnore_PerProperty - { - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string MyString1_IgnoredWhenWritingNull { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string MyString2_IgnoredWhenWritingNull; - - public int MyInt1; - - public int MyInt2 { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? MyNullableBool1_IgnoredWhenWritingNull { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? MyNullableBool2_IgnoredWhenWritingNull; - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public PointClass MyPointClass1_IgnoredWhenWritingNull; - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public PointClass MyPointClass2_IgnoredWhenWritingNull { get; set; } - - public Point_2D_Struct_WithAttribute MyPointStruct1; - - public Point_2D_Struct_WithAttribute MyPointStruct2 { get; set; } - } - - [Theory] - [InlineData(typeof(ClassWithBadIgnoreAttribute))] - [InlineData(typeof(StructWithBadIgnoreAttribute))] - public static void JsonIgnoreCondition_WhenWritingNull_OnValueType_Fail(Type type) - { - InvalidOperationException ex = Assert.Throws(() => JsonSerializer.Deserialize("", type)); - string exAsStr = ex.ToString(); - Assert.Contains("JsonIgnoreCondition.WhenWritingNull", exAsStr); - Assert.Contains("MyBadMember", exAsStr); - Assert.Contains(type.ToString(), exAsStr); - Assert.Contains("JsonIgnoreCondition.WhenWritingDefault", exAsStr); - - ex = Assert.Throws(() => JsonSerializer.Serialize(Activator.CreateInstance(type))); - exAsStr = ex.ToString(); - Assert.Contains("JsonIgnoreCondition.WhenWritingNull", exAsStr); - Assert.Contains("MyBadMember", exAsStr); - Assert.Contains(type.ToString(), exAsStr); - Assert.Contains("JsonIgnoreCondition.WhenWritingDefault", exAsStr); - } - - private class ClassWithBadIgnoreAttribute - { - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public int MyBadMember { get; set; } - } - - private struct StructWithBadIgnoreAttribute - { - [JsonInclude] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public Point_2D_Struct MyBadMember { get; set; } - } - - private interface IUseCustomConverter { } - - [JsonConverter(typeof(MyCustomConverter))] - private struct MyValueTypeWithProperties : IUseCustomConverter - { - public int PrimitiveValue { get; set; } - public object RefValue { get; set; } - } - - private class MyCustomConverter : JsonConverter - { - public override bool CanConvert(Type typeToConvert) - { - return typeof(IUseCustomConverter).IsAssignableFrom(typeToConvert); - } - - public override IUseCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - throw new NotImplementedException(); - - public override void Write(Utf8JsonWriter writer, IUseCustomConverter value, JsonSerializerOptions options) - { - MyValueTypeWithProperties obj = (MyValueTypeWithProperties)value; - writer.WriteNumberValue(obj.PrimitiveValue + 100); - // Ignore obj.RefValue - } - } - - private class MyClassWithValueType - { - public MyClassWithValueType() { } - - public MyValueTypeWithProperties Value { get; set; } - } - - [Fact] - public static void JsonIgnoreCondition_WhenWritingDefault_OnValueTypeWithCustomConverter() - { - var obj = new MyClassWithValueType(); - - // Baseline without custom options. - Assert.True(EqualityComparer.Default.Equals(default, obj.Value)); - string json = JsonSerializer.Serialize(obj); - Assert.Equal("{\"Value\":100}", json); - - var options = new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault - }; - - // Verify ignored. - Assert.True(EqualityComparer.Default.Equals(default, obj.Value)); - json = JsonSerializer.Serialize(obj, options); - Assert.Equal("{}", json); - - // Change a primitive value so it's no longer a default value. - obj.Value = new MyValueTypeWithProperties { PrimitiveValue = 1 }; - Assert.False(EqualityComparer.Default.Equals(default, obj.Value)); - json = JsonSerializer.Serialize(obj, options); - Assert.Equal("{\"Value\":101}", json); - - // Change reference value so it's no longer a default value. - obj.Value = new MyValueTypeWithProperties { RefValue = 1 }; - Assert.False(EqualityComparer.Default.Equals(default, obj.Value)); - json = JsonSerializer.Serialize(obj, options); - Assert.Equal("{\"Value\":100}", json); - } - - [Fact] - public static void JsonIgnoreCondition_ConverterCalledOnDeserialize() - { - // Verify converter is called. - Assert.Throws(() => JsonSerializer.Deserialize("{}")); - - var options = new JsonSerializerOptions - { - IgnoreNullValues = true - }; - - Assert.Throws(() => JsonSerializer.Deserialize("{}", options)); - } - - [Fact] - public static void JsonIgnoreCondition_WhenWritingNull_OnValueTypeWithCustomConverter() - { - string json; - var obj = new MyClassWithValueType(); - - // Baseline without custom options. - Assert.True(EqualityComparer.Default.Equals(default, obj.Value)); - json = JsonSerializer.Serialize(obj); - Assert.Equal("{\"Value\":100}", json); - - var options = new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - - // Verify not ignored; MyValueTypeWithProperties is not null. - Assert.True(EqualityComparer.Default.Equals(default, obj.Value)); - json = JsonSerializer.Serialize(obj, options); - Assert.Equal("{\"Value\":100}", json); - } - - [Fact] - public static void JsonIgnoreCondition_WhenWritingDefault_OnRootTypes() - { - string json; - int i = 0; - object obj = null; - - // Baseline without custom options. - json = JsonSerializer.Serialize(obj); - Assert.Equal("null", json); - - json = JsonSerializer.Serialize(i); - Assert.Equal("0", json); - - var options = new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault - }; - - // We don't ignore when applied to root types; only properties. - - json = JsonSerializer.Serialize(obj, options); - Assert.Equal("null", json); - - json = JsonSerializer.Serialize(i, options); - Assert.Equal("0", json); - } - - private struct MyValueTypeWithBoxedPrimitive - { - public object BoxedPrimitive { get; set; } - } - - [Fact] - public static void JsonIgnoreCondition_WhenWritingDefault_OnBoxedPrimitive() - { - string json; - - MyValueTypeWithBoxedPrimitive obj = new MyValueTypeWithBoxedPrimitive { BoxedPrimitive = 0 }; - - // Baseline without custom options. - json = JsonSerializer.Serialize(obj); - Assert.Equal("{\"BoxedPrimitive\":0}", json); - - var options = new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault - }; - - // No check if the boxed object's value type is a default value (0 in this case). - json = JsonSerializer.Serialize(obj, options); - Assert.Equal("{\"BoxedPrimitive\":0}", json); - - obj = new MyValueTypeWithBoxedPrimitive(); - json = JsonSerializer.Serialize(obj, options); - Assert.Equal("{}", json); - } - - private class MyClassWithValueTypeInterfaceProperty - { - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public IInterface MyProp { get; set; } - - public interface IInterface { } - public struct MyStruct : IInterface { } - } - - [Fact] - public static void JsonIgnoreCondition_WhenWritingDefault_OnInterface() - { - // MyProp should be ignored due to [JsonIgnore]. - var obj = new MyClassWithValueTypeInterfaceProperty(); - string json = JsonSerializer.Serialize(obj); - Assert.Equal("{}", json); - - // No check if the interface property's value type is a default value. - obj = new MyClassWithValueTypeInterfaceProperty { MyProp = new MyClassWithValueTypeInterfaceProperty.MyStruct() }; - json = JsonSerializer.Serialize(obj); - Assert.Equal("{\"MyProp\":{}}", json); - } + public PropertyVisibilityTestsDynamic() : base(new JsonSerializerWrapperForString_Dynamic()) { } } } 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 3ba9831cb6db0..62b646631196f 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 @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);net461 true @@ -16,7 +16,28 @@ + + + + + + + + + + + + + + + + + + + + + @@ -118,6 +139,7 @@ + @@ -144,8 +166,6 @@ - - @@ -160,22 +180,6 @@ - - - - - - - - - - - - - - - -