diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index a68bc3b28c15e..0a70692bc5176 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -18,6 +18,8 @@ namespace System.Text.Json.SourceGeneration { public sealed partial class JsonSourceGenerator { + private const string OptionsLocalVariableName = "options"; + private sealed partial class Emitter { // Literals in generated source @@ -25,14 +27,13 @@ private sealed partial class Emitter private const string CtorParamInitMethodNameSuffix = "CtorParamInit"; private const string DefaultOptionsStaticVarName = "s_defaultOptions"; private const string DefaultContextBackingStaticVarName = "s_defaultContext"; - private const string ElementInfoPropName = "ElementInfo"; internal const string GetConverterFromFactoryMethodName = "GetConverterFromFactory"; private const string InfoVarName = "info"; internal const string JsonContextVarName = "jsonContext"; - private const string KeyInfoPropName = "KeyInfo"; private const string NumberHandlingPropName = "NumberHandling"; private const string ObjectCreatorPropName = "ObjectCreator"; private const string OptionsInstanceVariableName = "Options"; + private const string JsonTypeInfoReturnValueLocalVariableName = "jsonTypeInfo"; private const string PropInitMethodNameSuffix = "PropInit"; private const string RuntimeCustomConverterFetchingMethodName = "GetRuntimeProvidedCustomConverter"; private const string SerializeHandlerPropName = "SerializeHandler"; @@ -49,7 +50,6 @@ private sealed partial class Emitter private const string InvalidOperationExceptionTypeRef = "global::System.InvalidOperationException"; private const string TypeTypeRef = "global::System.Type"; 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"; @@ -63,13 +63,13 @@ private sealed partial class Emitter private const string JsonCollectionInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues"; private const string JsonIgnoreConditionTypeRef = "global::System.Text.Json.Serialization.JsonIgnoreCondition"; private const string JsonNumberHandlingTypeRef = "global::System.Text.Json.Serialization.JsonNumberHandling"; - private const string JsonSerializerContextTypeRef = "global::System.Text.Json.Serialization.JsonSerializerContext"; private const string JsonMetadataServicesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonMetadataServices"; private const string JsonObjectInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues"; private const string JsonParameterInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues"; private const string JsonPropertyInfoTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo"; private const string JsonPropertyInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues"; private const string JsonTypeInfoTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonTypeInfo"; + private const string JsonTypeInfoResolverTypeRef = "global::System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver"; private static DiagnosticDescriptor TypeNotSupported { get; } = new DiagnosticDescriptor( id: "SYSLIB1030", @@ -131,14 +131,14 @@ public void Emit() isRootContextDef: true); // Add GetJsonTypeInfo override implementation. - AddSource($"{contextName}.GetJsonTypeInfo.g.cs", GetGetTypeInfoImplementation()); + AddSource($"{contextName}.GetJsonTypeInfo.g.cs", GetGetTypeInfoImplementation(), interfaceImplementation: JsonTypeInfoResolverTypeRef); // Add property name initialization. AddSource($"{contextName}.PropertyNames.g.cs", GetPropertyNameInitialization()); } } - private void AddSource(string fileName, string source, bool isRootContextDef = false) + private void AddSource(string fileName, string source, bool isRootContextDef = false, string? interfaceImplementation = null) { string? generatedCodeAttributeSource = isRootContextDef ? s_generatedCodeAttributeSource : null; @@ -175,7 +175,7 @@ namespace {@namespace} // Add the core implementation for the derived context class. string partialContextImplementation = $@" -{generatedCodeAttributeSource}{declarationList[0]} +{generatedCodeAttributeSource}{declarationList[0]}{(interfaceImplementation is null ? "" : ": " + interfaceImplementation)} {{ {IndentSource(source, Math.Max(1, declarationCount - 1))} }}"; @@ -313,7 +313,7 @@ private static string GenerateForTypeWithKnownConverter(TypeGenerationSpec typeM string typeCompilableName = typeMetadata.TypeRef; string typeFriendlyName = typeMetadata.TypeInfoPropertyName; - string metadataInitSource = $@"_{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, {JsonMetadataServicesTypeRef}.{typeFriendlyName}Converter);"; + string metadataInitSource = $@"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.{typeFriendlyName}Converter);"; return GenerateForType(typeMetadata, metadataInitSource); } @@ -321,42 +321,20 @@ private static string GenerateForTypeWithKnownConverter(TypeGenerationSpec typeM private static string GenerateForTypeWithUnknownConverter(TypeGenerationSpec typeMetadata) { string typeCompilableName = typeMetadata.TypeRef; - string typeFriendlyName = typeMetadata.TypeInfoPropertyName; // TODO (https://github.com/dotnet/runtime/issues/52218): consider moving this verification source to common helper. StringBuilder metadataInitSource = new( $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic}; - {TypeTypeRef} typeToConvert = typeof({typeCompilableName});"); + {TypeTypeRef} typeToConvert = typeof({typeCompilableName});"); - if (typeMetadata.IsValueType) - { - metadataInitSource.Append($@" - if (!converter.CanConvert(typeToConvert)) - {{ - {TypeTypeRef}? underlyingType = {NullableTypeRef}.GetUnderlyingType(typeToConvert); - if (underlyingType != null && converter.CanConvert(underlyingType)) - {{ - // Allow nullable handling to forward to the underlying type's converter. - converter = {JsonMetadataServicesTypeRef}.GetNullableConverter<{typeCompilableName}>(this.{typeFriendlyName})!; - converter = (({ JsonConverterFactoryTypeRef })converter).CreateConverter(typeToConvert, { OptionsInstanceVariableName })!; - }} - else - {{ - throw new {InvalidOperationExceptionTypeRef}(string.Format(""{ExceptionMessages.IncompatibleConverterType}"", converter.GetType(), typeToConvert)); - }} - }}"); - } - else - { - metadataInitSource.Append($@" - if (!converter.CanConvert(typeToConvert)) - {{ - throw new {InvalidOperationExceptionTypeRef}(string.Format(""{ExceptionMessages.IncompatibleConverterType}"", converter.GetType(), typeToConvert)); - }}"); - } + metadataInitSource.Append($@" + if (!converter.CanConvert(typeToConvert)) + {{ + throw new {InvalidOperationExceptionTypeRef}(string.Format(""{ExceptionMessages.IncompatibleConverterType}"", converter.GetType(), typeToConvert)); + }}"); metadataInitSource.Append($@" - _{typeFriendlyName} = { JsonMetadataServicesTypeRef }.{ GetCreateValueInfoMethodRef(typeCompilableName)} ({ OptionsInstanceVariableName}, converter); "); + {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)} ({OptionsLocalVariableName}, converter); "); return GenerateForType(typeMetadata, metadataInitSource.ToString()); } @@ -364,19 +342,15 @@ private static string GenerateForTypeWithUnknownConverter(TypeGenerationSpec typ private static string GenerateForNullable(TypeGenerationSpec typeMetadata) { string typeCompilableName = typeMetadata.TypeRef; - string typeFriendlyName = typeMetadata.TypeInfoPropertyName; TypeGenerationSpec? underlyingTypeMetadata = typeMetadata.NullableUnderlyingTypeMetadata; Debug.Assert(underlyingTypeMetadata != null); + string underlyingTypeCompilableName = underlyingTypeMetadata.TypeRef; - string underlyingTypeFriendlyName = underlyingTypeMetadata.TypeInfoPropertyName; - string underlyingTypeInfoNamedArg = underlyingTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen - ? "underlyingTypeInfo: null" - : $"underlyingTypeInfo: {underlyingTypeFriendlyName}"; - - string metadataInitSource = @$"_{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}( - {OptionsInstanceVariableName}, - {JsonMetadataServicesTypeRef}.GetNullableConverter<{underlyingTypeCompilableName}>({underlyingTypeInfoNamedArg})); + + string metadataInitSource = @$"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}( + {OptionsLocalVariableName}, + {JsonMetadataServicesTypeRef}.GetNullableConverter<{underlyingTypeCompilableName}>({OptionsLocalVariableName})); "; return GenerateForType(typeMetadata, metadataInitSource); @@ -385,9 +359,8 @@ private static string GenerateForNullable(TypeGenerationSpec typeMetadata) private static string GenerateForUnsupportedType(TypeGenerationSpec typeMetadata) { string typeCompilableName = typeMetadata.TypeRef; - string typeFriendlyName = typeMetadata.TypeInfoPropertyName; - string metadataInitSource = $"_{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, {JsonMetadataServicesTypeRef}.GetUnsupportedTypeConverter<{typeCompilableName}>());"; + string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetUnsupportedTypeConverter<{typeCompilableName}>());"; return GenerateForType(typeMetadata, metadataInitSource); } @@ -395,9 +368,8 @@ private static string GenerateForUnsupportedType(TypeGenerationSpec typeMetadata private static string GenerateForEnum(TypeGenerationSpec typeMetadata) { string typeCompilableName = typeMetadata.TypeRef; - string typeFriendlyName = typeMetadata.TypeInfoPropertyName; - string metadataInitSource = $"_{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, {JsonMetadataServicesTypeRef}.GetEnumConverter<{typeCompilableName}>({OptionsInstanceVariableName}));"; + string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetEnumConverter<{typeCompilableName}>({OptionsLocalVariableName}));"; return GenerateForType(typeMetadata, metadataInitSource); } @@ -408,29 +380,11 @@ private string GenerateForCollection(TypeGenerationSpec typeGenerationSpec) TypeGenerationSpec? collectionKeyTypeMetadata = typeGenerationSpec.CollectionKeyTypeMetadata; Debug.Assert(!(typeGenerationSpec.ClassType == ClassType.Dictionary && collectionKeyTypeMetadata == null)); string? keyTypeCompilableName = collectionKeyTypeMetadata?.TypeRef; - string? keyTypeReadableName = collectionKeyTypeMetadata?.TypeInfoPropertyName; - - string? keyTypeMetadataPropertyName; - if (typeGenerationSpec.ClassType != ClassType.Dictionary) - { - keyTypeMetadataPropertyName = "null"; - } - else - { - keyTypeMetadataPropertyName = collectionKeyTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen - ? "null" - : $"this.{keyTypeReadableName}"; - } // Value metadata TypeGenerationSpec? collectionValueTypeMetadata = typeGenerationSpec.CollectionValueTypeMetadata; Debug.Assert(collectionValueTypeMetadata != null); string valueTypeCompilableName = collectionValueTypeMetadata.TypeRef; - string valueTypeReadableName = collectionValueTypeMetadata.TypeInfoPropertyName; - - string valueTypeMetadataPropertyName = collectionValueTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen - ? "null" - : $"this.{valueTypeReadableName}"; string numberHandlingArg = $"{GetNumberHandlingAsStr(typeGenerationSpec.NumberHandling)}"; @@ -481,8 +435,8 @@ private string GenerateForCollection(TypeGenerationSpec typeGenerationSpec) _ => $"{JsonMetadataServicesTypeRef}.Create{collectionType}Info<" }; - string dictInfoCreationPrefix = $"{collectionInfoCreationPrefix}{typeRef}, {keyTypeCompilableName!}, {valueTypeCompilableName}>({OptionsInstanceVariableName}, {InfoVarName}"; - string enumerableInfoCreationPrefix = $"{collectionInfoCreationPrefix}{typeRef}, {valueTypeCompilableName}>({OptionsInstanceVariableName}, {InfoVarName}"; + string dictInfoCreationPrefix = $"{collectionInfoCreationPrefix}{typeRef}, {keyTypeCompilableName!}, {valueTypeCompilableName}>({OptionsLocalVariableName}, {InfoVarName}"; + string enumerableInfoCreationPrefix = $"{collectionInfoCreationPrefix}{typeRef}, {valueTypeCompilableName}>({OptionsLocalVariableName}, {InfoVarName}"; string immutableCollectionCreationSuffix = $"createRangeFunc: {typeGenerationSpec.ImmutableCollectionBuilderName}"; string collectionTypeInfoValue; @@ -490,23 +444,23 @@ private string GenerateForCollection(TypeGenerationSpec typeGenerationSpec) switch (collectionType) { case CollectionType.Array: - collectionTypeInfoValue = $"{collectionInfoCreationPrefix}{valueTypeCompilableName}>({OptionsInstanceVariableName}, {InfoVarName})"; + collectionTypeInfoValue = $"{collectionInfoCreationPrefix}{valueTypeCompilableName}>({OptionsLocalVariableName}, {InfoVarName})"; break; case CollectionType.IEnumerable: case CollectionType.IList: - collectionTypeInfoValue = $"{collectionInfoCreationPrefix}{typeRef}>({OptionsInstanceVariableName}, {InfoVarName})"; + collectionTypeInfoValue = $"{collectionInfoCreationPrefix}{typeRef}>({OptionsLocalVariableName}, {InfoVarName})"; break; case CollectionType.Stack: case CollectionType.Queue: string addMethod = collectionType == CollectionType.Stack ? "Push" : "Enqueue"; string addFuncNamedArg = $"addFunc: (collection, {ValueVarName}) => collection.{addMethod}({ValueVarName})"; - collectionTypeInfoValue = $"{collectionInfoCreationPrefix}{typeRef}>({OptionsInstanceVariableName}, {InfoVarName}, {addFuncNamedArg})"; + collectionTypeInfoValue = $"{collectionInfoCreationPrefix}{typeRef}>({OptionsLocalVariableName}, {InfoVarName}, {addFuncNamedArg})"; break; case CollectionType.ImmutableEnumerable: collectionTypeInfoValue = $"{enumerableInfoCreationPrefix}, {immutableCollectionCreationSuffix})"; break; case CollectionType.IDictionary: - collectionTypeInfoValue = $"{collectionInfoCreationPrefix}{typeRef}>({OptionsInstanceVariableName}, {InfoVarName})"; + collectionTypeInfoValue = $"{collectionInfoCreationPrefix}{typeRef}>({OptionsLocalVariableName}, {InfoVarName})"; break; case CollectionType.Dictionary: case CollectionType.IDictionaryOfTKeyTValue: @@ -522,15 +476,13 @@ private string GenerateForCollection(TypeGenerationSpec typeGenerationSpec) } string metadataInitSource = @$"{JsonCollectionInfoValuesTypeRef}<{typeRef}> {InfoVarName} = new {JsonCollectionInfoValuesTypeRef}<{typeRef}>() - {{ - {ObjectCreatorPropName} = {objectCreatorValue}, - {KeyInfoPropName} = {keyTypeMetadataPropertyName!}, - {ElementInfoPropName} = {valueTypeMetadataPropertyName}, - {NumberHandlingPropName} = {numberHandlingArg}, - {SerializeHandlerPropName} = {serializeHandlerValue} - }}; - - _{typeGenerationSpec.TypeInfoPropertyName} = {collectionTypeInfoValue}; + {{ + {ObjectCreatorPropName} = {objectCreatorValue}, + {NumberHandlingPropName} = {numberHandlingArg}, + {SerializeHandlerPropName} = {serializeHandlerValue} + }}; + + {JsonTypeInfoReturnValueLocalVariableName} = {collectionTypeInfoValue}; "; return GenerateForType(typeGenerationSpec, metadataInitSource, serializeHandlerSource); @@ -643,14 +595,14 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata) string? ctorParamMetadataInitFuncSource = null; string? serializeFuncSource = null; - string propInitMethodName = "null"; + string propInitMethod = "null"; string ctorParamMetadataInitMethodName = "null"; string serializeMethodName = "null"; if (typeMetadata.GenerateMetadata) { propMetadataInitFuncSource = GeneratePropMetadataInitFunc(typeMetadata); - propInitMethodName = $"{typeFriendlyName}{PropInitMethodNameSuffix}"; + propInitMethod = $"_ => {typeFriendlyName}{PropInitMethodNameSuffix}({OptionsLocalVariableName})"; if (constructionStrategy == ObjectConstructionStrategy.ParameterizedConstructor) { @@ -669,23 +621,23 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata) string genericArg = typeMetadata.TypeRef; string objectInfoInitSource = $@"{JsonObjectInfoValuesTypeRef}<{genericArg}> {ObjectInfoVarName} = new {JsonObjectInfoValuesTypeRef}<{genericArg}>() - {{ - {ObjectCreatorPropName} = {creatorInvocation}, - ObjectWithParameterizedConstructorCreator = {parameterizedCreatorInvocation}, - PropertyMetadataInitializer = {propInitMethodName}, - ConstructorParameterMetadataInitializer = {ctorParamMetadataInitMethodName}, - {NumberHandlingPropName} = {GetNumberHandlingAsStr(typeMetadata.NumberHandling)}, - {SerializeHandlerPropName} = {serializeMethodName} - }}; + {{ + {ObjectCreatorPropName} = {creatorInvocation}, + ObjectWithParameterizedConstructorCreator = {parameterizedCreatorInvocation}, + PropertyMetadataInitializer = {propInitMethod}, + ConstructorParameterMetadataInitializer = {ctorParamMetadataInitMethodName}, + {NumberHandlingPropName} = {GetNumberHandlingAsStr(typeMetadata.NumberHandling)}, + {SerializeHandlerPropName} = {serializeMethodName} + }}; - _{typeFriendlyName} = {JsonMetadataServicesTypeRef}.CreateObjectInfo<{typeMetadata.TypeRef}>({OptionsInstanceVariableName}, {ObjectInfoVarName});"; + {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.CreateObjectInfo<{typeMetadata.TypeRef}>({OptionsLocalVariableName}, {ObjectInfoVarName});"; string additionalSource = @$"{propMetadataInitFuncSource}{serializeFuncSource}{ctorParamMetadataInitFuncSource}"; return GenerateForType(typeMetadata, objectInfoInitSource, additionalSource); } - private string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenerationSpec) + private static string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenerationSpec) { const string PropVarName = "properties"; @@ -697,18 +649,13 @@ private string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenerationSpe ? $"{ArrayTypeRef}.Empty<{JsonPropertyInfoTypeRef}>()" : $"new {JsonPropertyInfoTypeRef}[{propCount}]"; - string contextTypeRef = _currentContext.ContextTypeRef; string propInitMethodName = $"{typeGenerationSpec.TypeInfoPropertyName}{PropInitMethodNameSuffix}"; StringBuilder sb = new(); sb.Append($@" - -private static {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerContextTypeRef} context) +private static {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}) {{ - {contextTypeRef} {JsonContextVarName} = ({contextTypeRef})context; - {JsonSerializerOptionsTypeRef} options = context.Options; - {JsonPropertyInfoTypeRef}[] {PropVarName} = {propertyArrayInstantiationValue}; "); @@ -722,10 +669,6 @@ private string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenerationSpe string declaringTypeCompilableName = memberMetadata.DeclaringTypeRef; - string memberTypeFriendlyName = memberTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen - ? "null" - : $"{JsonContextVarName}.{memberTypeMetadata.TypeInfoPropertyName}"; - string jsonPropertyNameValue = memberMetadata.JsonPropertyName != null ? @$"""{memberMetadata.JsonPropertyName}""" : "null"; @@ -773,7 +716,6 @@ private string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenerationSpe IsPublic = {FormatBool(memberMetadata.IsPublic)}, IsVirtual = {FormatBool(memberMetadata.IsVirtual)}, DeclaringType = typeof({memberMetadata.DeclaringTypeRef}), - PropertyTypeInfo = {memberTypeFriendlyName}, Converter = {converterValue}, Getter = {getterValue}, Setter = {setterValue}, @@ -785,8 +727,8 @@ private string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenerationSpe JsonPropertyName = {jsonPropertyNameValue} }}; - {PropVarName}[{i}] = {JsonMetadataServicesTypeRef}.CreatePropertyInfo<{memberTypeCompilableName}>(options, {infoVarName}); - "); + {PropVarName}[{i}] = {JsonMetadataServicesTypeRef}.CreatePropertyInfo<{memberTypeCompilableName}>({OptionsLocalVariableName}, {infoVarName}); +"); } sb.Append(@$" @@ -1131,28 +1073,29 @@ private static string GenerateForType(TypeGenerationSpec typeMetadata, string me return @$"private {typeInfoPropertyTypeRef}? _{typeFriendlyName}; public {typeInfoPropertyTypeRef} {typeFriendlyName} {{ - get - {{ - if (_{typeFriendlyName} == null) - {{ - {WrapWithCheckForCustomConverter(metadataInitSource, typeCompilableName, typeFriendlyName, GetNumberHandlingAsStr(typeMetadata.NumberHandling))} - }} + get => _{typeFriendlyName} ??= {typeMetadata.CreateTypeInfoMethodName}({OptionsInstanceVariableName}); +}} - return _{typeFriendlyName}; - }} -}}{additionalSource}"; +private static {typeInfoPropertyTypeRef} {typeMetadata.CreateTypeInfoMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}) +{{ + {typeInfoPropertyTypeRef}? {JsonTypeInfoReturnValueLocalVariableName} = null; + {WrapWithCheckForCustomConverter(metadataInitSource, typeCompilableName)} + + return {JsonTypeInfoReturnValueLocalVariableName}; +}} +{additionalSource}"; } - private static string WrapWithCheckForCustomConverter(string source, string typeCompilableName, string typeFriendlyName, string numberHandlingNamedArg) + private static string WrapWithCheckForCustomConverter(string source, string typeCompilableName) => @$"{JsonConverterTypeRef}? customConverter; - if ({OptionsInstanceVariableName}.Converters.Count > 0 && (customConverter = {RuntimeCustomConverterFetchingMethodName}(typeof({typeCompilableName}))) != null) - {{ - _{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, customConverter); - }} - else - {{ - {IndentSource(source, numIndentations: 1)} - }}"; + if ({OptionsLocalVariableName}.Converters.Count > 0 && (customConverter = {RuntimeCustomConverterFetchingMethodName}({OptionsLocalVariableName}, typeof({typeCompilableName}))) != null) + {{ + {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsLocalVariableName}, customConverter); + }} + else + {{ + {source} + }}"; private string GetRootJsonContextImplementation() { @@ -1172,7 +1115,7 @@ private string GetRootJsonContextImplementation() {{ }} -public {contextTypeName}({JsonSerializerOptionsTypeRef} options) : base(options) +public {contextTypeName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}) : base({OptionsLocalVariableName}) {{ }} @@ -1214,9 +1157,9 @@ private string GetLogicForDefaultSerializerOptionsInit() private static string GetFetchLogicForRuntimeSpecifiedCustomConverter() { // TODO (https://github.com/dotnet/runtime/issues/52218): use a dictionary if count > ~15. - return @$"private {JsonConverterTypeRef}? {RuntimeCustomConverterFetchingMethodName}({TypeTypeRef} type) + return @$"private static {JsonConverterTypeRef}? {RuntimeCustomConverterFetchingMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}, {TypeTypeRef} type) {{ - {IListTypeRef}<{JsonConverterTypeRef}> converters = {OptionsInstanceVariableName}.Converters; + {IListTypeRef}<{JsonConverterTypeRef}> converters = {OptionsLocalVariableName}.Converters; for (int i = 0; i < converters.Count; i++) {{ @@ -1226,7 +1169,7 @@ private static string GetFetchLogicForRuntimeSpecifiedCustomConverter() {{ if (converter is {JsonConverterFactoryTypeRef} factory) {{ - converter = factory.CreateConverter(type, {OptionsInstanceVariableName}); + converter = factory.CreateConverter(type, {OptionsLocalVariableName}); if (converter == null || converter is {JsonConverterFactoryTypeRef}) {{ throw new {InvalidOperationExceptionTypeRef}(string.Format(""{ExceptionMessages.InvalidJsonConverterFactoryOutput}"", factory.GetType())); @@ -1245,9 +1188,9 @@ private static string GetFetchLogicForGetCustomConverter_PropertiesWithFactories { return @$" -private {JsonConverterTypeRef} {GetConverterFromFactoryMethodName}({JsonConverterFactoryTypeRef} factory) +private static {JsonConverterTypeRef} {GetConverterFromFactoryMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}, {JsonConverterFactoryTypeRef} factory) {{ - return ({JsonConverterTypeRef}) {GetConverterFromFactoryMethodName}(typeof(T), factory); + return ({JsonConverterTypeRef}) {GetConverterFromFactoryMethodName}({OptionsLocalVariableName}, typeof(T), factory); }}"; } @@ -1255,9 +1198,9 @@ private static string GetFetchLogicForGetCustomConverter_TypesWithFactories() { return @$" -private {JsonConverterTypeRef} {GetConverterFromFactoryMethodName}({TypeTypeRef} type, {JsonConverterFactoryTypeRef} factory) +private static {JsonConverterTypeRef} {GetConverterFromFactoryMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}, {TypeTypeRef} type, {JsonConverterFactoryTypeRef} factory) {{ - {JsonConverterTypeRef}? converter = factory.CreateConverter(type, {Emitter.OptionsInstanceVariableName}); + {JsonConverterTypeRef}? converter = factory.CreateConverter(type, {OptionsLocalVariableName}); if (converter == null || converter is {JsonConverterFactoryTypeRef}) {{ throw new {InvalidOperationExceptionTypeRef}(string.Format(""{ExceptionMessages.InvalidJsonConverterFactoryOutput}"", factory.GetType())); @@ -1274,8 +1217,7 @@ private string GetGetTypeInfoImplementation() sb.Append(@$"public override {JsonTypeInfoTypeRef} GetTypeInfo({TypeTypeRef} type) {{"); - HashSet types = new(_currentContext.RootSerializableTypes); - types.UnionWith(_currentContext.ImplicitlyRegisteredTypes); + HashSet types = new(_currentContext.TypesWithMetadataGenerated); // TODO (https://github.com/dotnet/runtime/issues/52218): Make this Dictionary-lookup-based if root-serializable type count > 64. foreach (TypeGenerationSpec metadata in types) @@ -1291,10 +1233,40 @@ private string GetGetTypeInfoImplementation() } } - sb.Append(@" + sb.AppendLine(@" return null!; }"); + // Explicit IJsonTypeInfoResolver implementation + sb.AppendLine(); + sb.Append(@$"{JsonTypeInfoTypeRef}? {JsonTypeInfoResolverTypeRef}.GetTypeInfo({TypeTypeRef} type, {JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}) +{{ + if ({OptionsInstanceVariableName} == {OptionsLocalVariableName}) + {{ + return this.GetTypeInfo(type); + }} + else + {{"); + // TODO (https://github.com/dotnet/runtime/issues/52218): Make this Dictionary-lookup-based if root-serializable type count > 64. + foreach (TypeGenerationSpec metadata in types) + { + if (metadata.ClassType != ClassType.TypeUnsupportedBySourceGen) + { + sb.Append($@" + if (type == typeof({metadata.TypeRef})) + {{ + return {metadata.CreateTypeInfoMethodName}({OptionsLocalVariableName}); + }} +"); + } + } + + sb.Append($@" + return null; + }} +}} +"); + return sb.ToString(); } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index b539258ad2905..86c38c3c45165 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -1473,11 +1473,11 @@ private static bool PropertyAccessorCanBeReferenced(MethodInfo? accessor) if (forType) { - return $"{Emitter.GetConverterFromFactoryMethodName}(typeof({type.GetCompilableName()}), new {converterType.GetCompilableName()}())"; + return $"{Emitter.GetConverterFromFactoryMethodName}({OptionsLocalVariableName}, typeof({type.GetCompilableName()}), new {converterType.GetCompilableName()}())"; } else { - return $"{Emitter.JsonContextVarName}.{Emitter.GetConverterFromFactoryMethodName}<{type.GetCompilableName()}>(new {converterType.GetCompilableName()}())"; + return $"{Emitter.GetConverterFromFactoryMethodName}<{type.GetCompilableName()}>({OptionsLocalVariableName}, new {converterType.GetCompilableName()}())"; } } diff --git a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs index 98cc904cdf434..770e45d395b23 100644 --- a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs @@ -31,6 +31,11 @@ internal sealed class TypeGenerationSpec /// public string TypeInfoPropertyName { get; set; } + /// + /// Method used to generate JsonTypeInfo given options instance + /// + public string CreateTypeInfoMethodName => $"Create_{TypeInfoPropertyName}"; + public JsonSourceGenerationMode GenerationMode { get; set; } public bool GenerateMetadata => GenerationModeIsSpecified(JsonSourceGenerationMode.Metadata); 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 8a5df1d2379c6..86e3c7011e176 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -357,6 +357,13 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { } public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } } public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } } public System.Collections.Generic.IList PolymorphicTypeConfigurations { get { throw null; } } + public System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver TypeInfoResolver + { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + get { throw null; } + set { } + } public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } } public bool WriteIndented { get { throw null; } set { } } public void AddContext() where TContext : System.Text.Json.Serialization.JsonSerializerContext, new() { } @@ -950,12 +957,13 @@ public JsonSerializableAttribute(System.Type type) { } public string? TypeInfoPropertyName { get { throw null; } set { } } public System.Text.Json.Serialization.JsonSourceGenerationMode GenerationMode { get { throw null; } set { } } } - public abstract partial class JsonSerializerContext + public abstract partial class JsonSerializerContext : System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver { protected JsonSerializerContext(System.Text.Json.JsonSerializerOptions? options) { } protected abstract System.Text.Json.JsonSerializerOptions? GeneratedSerializerOptions { get; } public System.Text.Json.JsonSerializerOptions Options { get { throw null; } } public abstract System.Text.Json.Serialization.Metadata.JsonTypeInfo? GetTypeInfo(System.Type type); + System.Text.Json.Serialization.Metadata.JsonTypeInfo System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver.GetTypeInfo(Type type, JsonSerializerOptions options) { throw null; } } [System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple = false)] public sealed partial class JsonSourceGenerationOptionsAttribute : System.Text.Json.Serialization.JsonAttribute @@ -1044,6 +1052,20 @@ protected ReferenceResolver() { } } namespace System.Text.Json.Serialization.Metadata { + public class DefaultJsonTypeInfoResolver : System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver + { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + public DefaultJsonTypeInfoResolver() { } + + public virtual System.Text.Json.Serialization.Metadata.JsonTypeInfo GetTypeInfo(System.Type type, System.Text.Json.JsonSerializerOptions options) { throw null; } + + public System.Collections.Generic.IList> Modifiers { get; } + } + public interface IJsonTypeInfoResolver + { + System.Text.Json.Serialization.Metadata.JsonTypeInfo? GetTypeInfo(System.Type type, System.Text.Json.JsonSerializerOptions options); + } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public sealed partial class JsonCollectionInfoValues { @@ -1116,6 +1138,7 @@ public static partial class JsonMetadataServices public static System.Text.Json.Serialization.JsonConverter GetUnsupportedTypeConverter() { 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; } + public static System.Text.Json.Serialization.JsonConverter GetNullableConverter(System.Text.Json.JsonSerializerOptions options) where T : struct { throw null; } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public sealed partial class JsonObjectInfoValues @@ -1138,10 +1161,17 @@ public JsonParameterInfoValues() { } public System.Type ParameterType { get { throw null; } init { } } public int Position { get { throw null; } init { } } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public abstract partial class JsonPropertyInfo { internal JsonPropertyInfo() { } + public System.Text.Json.Serialization.JsonConverter? CustomConverter { get { throw null; } set { } } + public System.Func? Get { get { throw null; } set { } } + public string Name { get { throw null; } set { } } + public System.Text.Json.Serialization.JsonNumberHandling? NumberHandling { get { throw null; } set { } } + public System.Type PropertyType { get { throw null; } } + public System.Text.Json.JsonSerializerOptions Options { get { throw null; } } + public System.Action? Set { get { throw null; } set { } } + public System.Func? ShouldSerialize { get { throw null; } set { } } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public sealed partial class JsonPropertyInfoValues @@ -1162,15 +1192,40 @@ public JsonPropertyInfoValues() { } public System.Text.Json.Serialization.Metadata.JsonTypeInfo PropertyTypeInfo { get { throw null; } init { } } public System.Action? Setter { get { throw null; } init { } } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - public partial class JsonTypeInfo + public static class JsonTypeInfoResolver + { + public static System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver Combine(params System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver[] resolvers) { throw null; } + } + public abstract partial class JsonTypeInfo { internal JsonTypeInfo() { } + public System.Text.Json.JsonSerializerOptions Options { get { throw null; } } + public System.Collections.Generic.IList Properties { get { throw null; } } + public System.Type Type { get { throw null; } } + public System.Text.Json.Serialization.JsonConverter Converter { get { throw null; } } + public System.Func? CreateObject { get { throw null; } set { } } + public System.Text.Json.Serialization.Metadata.JsonTypeInfoKind Kind { get { throw null; } } + public System.Text.Json.Serialization.JsonNumberHandling? NumberHandling { get { throw null; } set { } } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use generic overload or System.Text.Json source generation for native AOT applications.")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use generic overload or System.Text.Json source generation for native AOT applications.")] + public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateJsonTypeInfo(System.Text.Json.JsonSerializerOptions options) { throw null; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use generic overload or System.Text.Json source generation for native AOT applications.")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use generic overload or System.Text.Json source generation for native AOT applications.")] + public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateJsonTypeInfo(System.Type type, System.Text.Json.JsonSerializerOptions options) { throw null; } + public JsonPropertyInfo CreateJsonPropertyInfo(Type propertyType, string name) { throw null; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public abstract partial class JsonTypeInfo : System.Text.Json.Serialization.Metadata.JsonTypeInfo { internal JsonTypeInfo() { } + public new System.Func? CreateObject { get { throw null; } set { } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public System.Action? SerializeHandler { get { throw null; } } } + public enum JsonTypeInfoKind + { + None = 0, + Object = 1, + Enumerable = 2, + Dictionary = 3 + } } diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 6983f813456a3..c5b2e82a14bfc 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -1,17 +1,17 @@ - @@ -243,6 +243,15 @@ The requested operation requires an element of type '{0}', but the target element has type '{1}'. + + Default TypeInfoResolver and custom TypeInfoResolver cannot be changed after first usage. + + + JsonTypeInfo cannot be changed after first usage. + + + JsonPropertyInfo cannot be changed after first usage. + Max depth must be positive. @@ -390,6 +399,12 @@ The converter '{0}' is not compatible with the type '{1}'. + + TypeInfoResolver expected to return JsonTypeInfo of type '{0}' but returned JsonTypeInfo of type '{1}'. + + + TypeInfoResolver expected to return JsonTypeInfo options bound to the JsonSerializerOptions provided in the argument. + The converter '{0}' wrote too much or not enough. @@ -572,13 +587,13 @@ Metadata for type '{0}' was not provided to the serializer. The serializer method used does not support reflection-based creation of serialization-related type metadata. If using source generation, ensure that all root types passed to the serializer have been indicated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically. - + Collection is read-only. - + Number was less than 0. - + Destination array was not long enough. @@ -638,4 +653,16 @@ Runtime type '{0}' has a diamond ambiguity between derived types '{1}' and '{2}' of polymorphic type '{3}'. Consider either removing one of the derived types or removing the 'JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor' setting. + + Operation is not possible when Kind is JsonTypeKind.None. + + + One of the provided resolvers is null. + + + JsonPropertyInfo with name '{0}' for type '{1}' is already bound to different JsonTypeInfo. + + + JsonTypeInfo metadata references a JsonSerializerOptions instance that doesn't specify a TypeInfoResolver. + 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 0e4522699262a..d76756fb60540 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -61,6 +61,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + @@ -101,6 +102,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + @@ -119,6 +121,12 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + + + + + + @@ -311,7 +319,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET - + @@ -376,12 +384,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET - - + + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.KeyCollection.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.KeyCollection.cs index a0e9edb109bfb..997ee59563900 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.KeyCollection.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.KeyCollection.cs @@ -36,9 +36,9 @@ IEnumerator IEnumerable.GetEnumerator() } } - public void Add(string propertyName) => ThrowHelper.ThrowNotSupportedException_NodeCollectionIsReadOnly(); + public void Add(string propertyName) => ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - public void Clear() => ThrowHelper.ThrowNotSupportedException_NodeCollectionIsReadOnly(); + public void Clear() => ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); public bool Contains(string propertyName) => _parent.ContainsProperty(propertyName); @@ -46,14 +46,14 @@ public void CopyTo(string[] propertyNameArray, int index) { if (index < 0) { - ThrowHelper.ThrowArgumentOutOfRangeException_NodeArrayIndexNegative(nameof(index)); + ThrowHelper.ThrowArgumentOutOfRangeException_ArrayIndexNegative(nameof(index)); } foreach (KeyValuePair item in _parent) { if (index >= propertyNameArray.Length) { - ThrowHelper.ThrowArgumentException_NodeArrayTooSmall(nameof(propertyNameArray)); + ThrowHelper.ThrowArgumentException_ArrayTooSmall(nameof(propertyNameArray)); } propertyNameArray[index++] = item.Key; @@ -68,7 +68,7 @@ public IEnumerator GetEnumerator() } } - bool ICollection.Remove(string propertyName) => throw ThrowHelper.GetNotSupportedException_NodeCollectionIsReadOnly(); + bool ICollection.Remove(string propertyName) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.ValueCollection.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.ValueCollection.cs index c907454d800e6..c81a67cd29dbf 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.ValueCollection.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.ValueCollection.cs @@ -36,9 +36,9 @@ IEnumerator IEnumerable.GetEnumerator() } } - public void Add(T? jsonNode) => ThrowHelper.ThrowNotSupportedException_NodeCollectionIsReadOnly(); + public void Add(T? jsonNode) => ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - public void Clear() => ThrowHelper.ThrowNotSupportedException_NodeCollectionIsReadOnly(); + public void Clear() => ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); public bool Contains(T? jsonNode) => _parent.ContainsValue(jsonNode); @@ -46,14 +46,14 @@ public void CopyTo(T?[] nodeArray, int index) { if (index < 0) { - ThrowHelper.ThrowArgumentOutOfRangeException_NodeArrayIndexNegative(nameof(index)); + ThrowHelper.ThrowArgumentOutOfRangeException_ArrayIndexNegative(nameof(index)); } foreach (KeyValuePair item in _parent) { if (index >= nodeArray.Length) { - ThrowHelper.ThrowArgumentException_NodeArrayTooSmall(nameof(nodeArray)); + ThrowHelper.ThrowArgumentException_ArrayTooSmall(nameof(nodeArray)); } nodeArray[index++] = item.Value; @@ -68,7 +68,7 @@ public void CopyTo(T?[] nodeArray, int index) } } - bool ICollection.Remove(T? node) => throw ThrowHelper.GetNotSupportedException_NodeCollectionIsReadOnly(); + bool ICollection.Remove(T? node) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.cs index 8e3180341dba4..5eeac2cda951d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.cs @@ -38,7 +38,7 @@ public void Add(string propertyName, T? value) { if (IsReadOnly) { - ThrowHelper.ThrowNotSupportedException_NodeCollectionIsReadOnly(); + ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); } if (propertyName == null) @@ -53,7 +53,7 @@ public void Add(KeyValuePair property) { if (IsReadOnly) { - ThrowHelper.ThrowNotSupportedException_NodeCollectionIsReadOnly(); + ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); } Add(property.Key, property.Value); @@ -63,7 +63,7 @@ public bool TryAdd(string propertyName, T value) { if (IsReadOnly) { - ThrowHelper.ThrowNotSupportedException_NodeCollectionIsReadOnly(); + ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); } // A check for a null propertyName is not required since this method is only called by internal code. @@ -76,7 +76,7 @@ public void Clear() { if (IsReadOnly) { - ThrowHelper.ThrowNotSupportedException_NodeCollectionIsReadOnly(); + ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); } _propertyList.Clear(); @@ -105,7 +105,7 @@ public bool Remove(string propertyName) { if (IsReadOnly) { - ThrowHelper.ThrowNotSupportedException_NodeCollectionIsReadOnly(); + ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); } if (propertyName == null) @@ -133,14 +133,14 @@ public void CopyTo(KeyValuePair[] array, int index) { if (index < 0) { - ThrowHelper.ThrowArgumentOutOfRangeException_NodeArrayIndexNegative(nameof(index)); + ThrowHelper.ThrowArgumentOutOfRangeException_ArrayIndexNegative(nameof(index)); } foreach (KeyValuePair item in _propertyList) { if (index >= array.Length) { - ThrowHelper.ThrowArgumentException_NodeArrayTooSmall(nameof(array)); + ThrowHelper.ThrowArgumentException_ArrayTooSmall(nameof(array)); } array[index++] = item; @@ -211,7 +211,7 @@ public T? this[string propertyName] { if (IsReadOnly) { - ThrowHelper.ThrowNotSupportedException_NodeCollectionIsReadOnly(); + ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); } if (propertyName == null) @@ -278,15 +278,15 @@ private void AddValue(string propertyName, T? value) { if (!TryAddValue(propertyName, value)) { - ThrowHelper.ThrowArgumentException_DuplicateKey(propertyName); + ThrowHelper.ThrowArgumentException_DuplicateKey(nameof(propertyName), propertyName); } } - private bool TryAddValue(string propertyName, T? value) + internal bool TryAddValue(string propertyName, T? value) { if (IsReadOnly) { - ThrowHelper.ThrowNotSupportedException_NodeCollectionIsReadOnly(); + ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); } CreateDictionaryIfThresholdMet(); @@ -319,7 +319,7 @@ private void CreateDictionaryIfThresholdMet() } } - private bool ContainsValue(T? value) + internal bool ContainsValue(T? value) { foreach (T? item in GetValueCollection()) { @@ -383,7 +383,7 @@ public bool TryRemoveProperty(string propertyName, out T? existing) { if (IsReadOnly) { - ThrowHelper.ThrowNotSupportedException_NodeCollectionIsReadOnly(); + ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); } if (_propertyDictionary != null) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyInfoDictionaryValueList.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyInfoDictionaryValueList.cs new file mode 100644 index 0000000000000..5640a9baef555 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyInfoDictionaryValueList.cs @@ -0,0 +1,206 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json +{ + internal sealed class JsonPropertyInfoDictionaryValueList : IList + { + private readonly JsonPropertyDictionary _parent; + private List? _items; + private JsonTypeInfo _parentTypeInfo; + + [MemberNotNullWhen(false, nameof(_items))] + public bool IsReadOnly => _items == null; + public int Count => IsReadOnly ? _parent.Count : _items.Count; + + public JsonPropertyInfo this[int index] + { + get => IsReadOnly ? _parent.List[index].Value! : _items[index]; + set + { + if (IsReadOnly) + ThrowCollectionIsReadOnly(); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + value.EnsureChildOf(_parentTypeInfo); + _items[index] = value; + } + } + + public JsonPropertyInfoDictionaryValueList(JsonPropertyDictionary parent, JsonTypeInfo parentTypeInfo, bool isReadOnly) + { + _parent = parent; + _parentTypeInfo = parentTypeInfo; + + Debug.Assert(!_parent.IsReadOnly, $"{nameof(JsonPropertyDictionary)} is read-only but editable value list is created"); + + if (!isReadOnly) + { + // We cannot ensure keys won't change while editing therefore we operate on the internal copy. + // Once we're done editing FinishEditingAndMakeReadOnly should be called then we switch to operating directly on _parent + _items = new List(_parent.Count); + foreach (var kv in _parent.List) + { + Debug.Assert(kv.Value != null, $"{nameof(JsonPropertyDictionary)} contains null value"); + + // we need to do this so that property cannot be copied over elsewhere + // since source gen properties do not have parents by default + kv.Value.EnsureChildOf(parentTypeInfo); + _items.Add(kv.Value); + } + } + } + + public void FinishEditingAndMakeReadOnly(Type parentType) + { + Debug.Assert(!IsReadOnly, $"{nameof(FinishEditingAndMakeReadOnly)} called on read-only ValueList"); + + // We do not know if any of the keys needs to be updated therefore we need to re-create cache + _parent.Clear(); + + foreach (var item in _items) + { + string key = item.Name; + if (!_parent.TryAddValue(key, item)) + { + ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(parentType, key); + } + } + + // clearing those so that we don't keep GC from freeing and also mark it as read-only + _items = null; + } + + public void Add(JsonPropertyInfo item) + { + if (IsReadOnly) + ThrowCollectionIsReadOnly(); + + if (item == null) + throw new ArgumentNullException(nameof(item)); + + item.EnsureChildOf(_parentTypeInfo); + _items.Add(item); + } + + public void Clear() + { + if (IsReadOnly) + ThrowCollectionIsReadOnly(); + + _items.Clear(); + } + + public bool Contains(JsonPropertyInfo item) => IsReadOnly ? _parent.ContainsValue(item) : _items.Contains(item); + + public void CopyTo(JsonPropertyInfo[] array, int index) + { + if (index < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException_ArrayIndexNegative(nameof(index)); + } + + if (IsReadOnly) + { + foreach (KeyValuePair item in _parent) + { + if (index >= array.Length) + { + ThrowHelper.ThrowArgumentException_ArrayTooSmall(nameof(array)); + } + + array[index++] = item.Value!; + } + } + else + { + _items.CopyTo(array, index); + } + } + + public int IndexOf(JsonPropertyInfo item) + { + if (IsReadOnly) + { + int index = 0; + foreach (var kv in _parent.List) + { + if (kv.Value == item) + { + return index; + } + + index++; + } + + return -1; + } + else + { + return _items.IndexOf(item); + } + } + + public void Insert(int index, JsonPropertyInfo item) + { + if (IsReadOnly) + ThrowCollectionIsReadOnly(); + + if (item == null) + throw new ArgumentNullException(nameof(item)); + + item.EnsureChildOf(_parentTypeInfo); + _items.Insert(index, item); + } + + public bool Remove(JsonPropertyInfo item) + { + if (IsReadOnly) + ThrowCollectionIsReadOnly(); + + return _items.Remove(item); + } + + public void RemoveAt(int index) + { + if (IsReadOnly) + ThrowCollectionIsReadOnly(); + + _items.RemoveAt(index); + } + + public IEnumerator GetEnumerator() + { + if (IsReadOnly) + { + foreach (KeyValuePair item in _parent) + { + yield return item.Value!; + } + } + else + { + foreach (JsonPropertyInfo item in _items) + { + yield return item; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + [DoesNotReturn] + private static void ThrowCollectionIsReadOnly() + { + ThrowHelper.ThrowInvalidOperationException_CollectionIsReadOnly(); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs index fb37d18818b38..5199a93bba29c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs @@ -4,30 +4,25 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; namespace System.Text.Json.Serialization { /// - /// A list of configuration items that respects the options class being immutable once (de)serialization occurs. + /// A list of configuration items that can be locked for modification /// - internal sealed class ConfigurationList : IList + internal abstract class ConfigurationList : IList { private readonly List _list; - private readonly JsonSerializerOptions _options; - public Action? OnElementAdded { get; set; } - - public ConfigurationList(JsonSerializerOptions options) + public ConfigurationList(IList? source = null) { - _options = options; - _list = new List(); + _list = source is null ? new List() : new List(source); } - public ConfigurationList(JsonSerializerOptions options, IList source) - { - _options = options; - _list = new List(source is ConfigurationList cl ? cl._list : source); - } + protected abstract bool IsLockedInstance { get; } + protected abstract void VerifyMutable(); + protected virtual void OnItemAdded(TItem item) { } public TItem this[int index] { @@ -42,15 +37,15 @@ public TItem this[int index] throw new ArgumentNullException(nameof(value)); } - _options.VerifyMutable(); + VerifyMutable(); _list[index] = value; - OnElementAdded?.Invoke(value); + OnItemAdded(value); } } public int Count => _list.Count; - public bool IsReadOnly => false; + public bool IsReadOnly => IsLockedInstance; public void Add(TItem item) { @@ -59,14 +54,14 @@ public void Add(TItem item) ThrowHelper.ThrowArgumentNullException(nameof(item)); } - _options.VerifyMutable(); + VerifyMutable(); _list.Add(item); - OnElementAdded?.Invoke(item); + OnItemAdded(item); } public void Clear() { - _options.VerifyMutable(); + VerifyMutable(); _list.Clear(); } @@ -97,20 +92,20 @@ public void Insert(int index, TItem item) ThrowHelper.ThrowArgumentNullException(nameof(item)); } - _options.VerifyMutable(); + VerifyMutable(); _list.Insert(index, item); - OnElementAdded?.Invoke(item); + OnItemAdded(item); } public bool Remove(TItem item) { - _options.VerifyMutable(); + VerifyMutable(); return _list.Remove(item); } public void RemoveAt(int index) { - _options.VerifyMutable(); + VerifyMutable(); _list.RemoveAt(index); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs new file mode 100644 index 0000000000000..0ecafc2e31244 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs @@ -0,0 +1,70 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.Serialization.Converters +{ + /// + /// Converter wrapper which casts SourceType into TargetType + /// + internal sealed class CastingConverter : JsonConverter + { + private JsonConverter _sourceConverter; + + internal override Type? KeyType => _sourceConverter.KeyType; + internal override Type? ElementType => _sourceConverter.ElementType; + + public override bool HandleNull => _sourceConverter.HandleNull; + internal override ConverterStrategy ConverterStrategy => _sourceConverter.ConverterStrategy; + + internal CastingConverter(JsonConverter sourceConverter) : base(initialize: false) + { + _sourceConverter = sourceConverter; + Initialize(); + + IsInternalConverter = sourceConverter.IsInternalConverter; + IsInternalConverterForNumberType = sourceConverter.IsInternalConverterForNumberType; + RequiresReadAhead = sourceConverter.RequiresReadAhead; + CanUseDirectReadOrWrite = sourceConverter.CanUseDirectReadOrWrite; + } + + public override TTarget? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => Cast(_sourceConverter.Read(ref reader, typeToConvert, options)); + + public override void Write(Utf8JsonWriter writer, TTarget value, JsonSerializerOptions options) + => _sourceConverter.Write(writer, Cast(value), options); + + internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out TTarget? value) + { + bool result = _sourceConverter.OnTryRead(ref reader, typeToConvert, options, ref state, out TSource? sourceValue); + value = Cast(sourceValue); + return result; + } + + internal override bool OnTryWrite(Utf8JsonWriter writer, TTarget value, JsonSerializerOptions options, ref WriteStack state) + => _sourceConverter.OnTryWrite(writer, Cast(value), options, ref state); + + public override TTarget ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => Cast(_sourceConverter.ReadAsPropertyName(ref reader, typeToConvert, options)); + + internal override TTarget ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => Cast(_sourceConverter.ReadAsPropertyNameCore(ref reader, typeToConvert, options)); + + public override void WriteAsPropertyName(Utf8JsonWriter writer, TTarget value, JsonSerializerOptions options) + => _sourceConverter.WriteAsPropertyName(writer, Cast(value), options); + + internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, TTarget value, JsonSerializerOptions options, bool isWritingExtensionDataProperty) + => _sourceConverter.WriteAsPropertyNameCore(writer, Cast(value), options, isWritingExtensionDataProperty); + + internal override TTarget ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options) + => Cast(_sourceConverter.ReadNumberWithCustomHandling(ref reader, handling, options)); + + internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, TTarget value, JsonNumberHandling handling) + => _sourceConverter.WriteNumberWithCustomHandling(writer, Cast(value), handling); + + private static TCastTarget Cast(TCastSource? source) => (TCastTarget)(object?)source!; + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs index 5b32b9cc83a8f..2ee506e720496 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs @@ -41,7 +41,7 @@ protected virtual void CreateCollection(ref Utf8JsonReader reader, ref ReadStack } } - state.Current.ReturnValue = typeInfo.CreateObject()!; + state.Current.ReturnValue = typeInfo.CreateObject(); Debug.Assert(state.Current.ReturnValue is TCollection); } @@ -49,7 +49,7 @@ protected virtual void ConvertCollection(ref ReadStack state, JsonSerializerOpti protected static JsonConverter GetElementConverter(JsonTypeInfo elementTypeInfo) { - JsonConverter converter = (JsonConverter)elementTypeInfo.PropertyInfoForTypeInfo.ConverterBase; + JsonConverter converter = (JsonConverter)elementTypeInfo.Converter; Debug.Assert(converter != null); // It should not be possible to have a null converter at this point. return converter; @@ -57,7 +57,7 @@ protected static JsonConverter GetElementConverter(JsonTypeInfo elemen protected static JsonConverter GetElementConverter(ref WriteStack state) { - JsonConverter converter = (JsonConverter)state.Current.JsonPropertyInfo!.ConverterBase; + JsonConverter converter = (JsonConverter)state.Current.JsonPropertyInfo!.EffectiveConverter; Debug.Assert(converter != null); // It should not be possible to have a null converter at this point. return converter; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs index 721f1c64d0faf..78660b8f6aa8c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs @@ -70,7 +70,7 @@ protected virtual void CreateCollection(ref Utf8JsonReader reader, ref ReadStack protected static JsonConverter GetConverter(JsonTypeInfo typeInfo) { - JsonConverter converter = (JsonConverter)typeInfo.PropertyInfoForTypeInfo.ConverterBase; + JsonConverter converter = (JsonConverter)typeInfo.Converter; Debug.Assert(converter != null); // It should not be possible to have a null converter at this point. return converter; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/StackOrQueueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/StackOrQueueConverter.cs index 5832092cbb043..59f3f3c336ec9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/StackOrQueueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/StackOrQueueConverter.cs @@ -21,7 +21,7 @@ protected sealed override void Add(in object? value, ref ReadStack state) protected sealed override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options) { JsonTypeInfo typeInfo = state.Current.JsonTypeInfo; - JsonTypeInfo.ConstructorDelegate? constructorDelegate = typeInfo.CreateObject; + Func? constructorDelegate = typeInfo.CreateObject; if (constructorDelegate == null) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs index f42adbc31fe40..19a3d3c74c348 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs @@ -37,12 +37,12 @@ public override bool CanConvert(Type typeToConvert) => case FSharpKind.Option: elementType = typeToConvert.GetGenericArguments()[0]; converterFactoryType = typeof(FSharpOptionConverter<,>).MakeGenericType(typeToConvert, elementType); - constructorArguments = new object[] { options.GetConverterInternal(elementType) }; + constructorArguments = new object[] { options.GetConverterFromTypeInfo(elementType) }; break; case FSharpKind.ValueOption: elementType = typeToConvert.GetGenericArguments()[0]; converterFactoryType = typeof(FSharpValueOptionConverter<,>).MakeGenericType(typeToConvert, elementType); - constructorArguments = new object[] { options.GetConverterInternal(elementType) }; + constructorArguments = new object[] { options.GetConverterFromTypeInfo(elementType) }; break; case FSharpKind.List: elementType = typeToConvert.GetGenericArguments()[0]; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs index b597c4d16da6a..bec6fddd9ef79 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs @@ -15,7 +15,7 @@ namespace System.Text.Json.Serialization.Converters /// The type to converter internal sealed class JsonMetadataServicesConverter : JsonResumableConverter { - private readonly Func> _converterCreator; + private readonly Func>? _converterCreator; private readonly ConverterStrategy _converterStrategy; @@ -26,7 +26,7 @@ internal JsonConverter Converter { get { - _converter ??= _converterCreator(); + _converter ??= _converterCreator!(); Debug.Assert(_converter != null); Debug.Assert(_converter.ConverterStrategy == _converterStrategy); return _converter; @@ -54,6 +54,12 @@ public JsonMetadataServicesConverter(Func> converterCreator, Co _converterStrategy = converterStrategy; } + public JsonMetadataServicesConverter(JsonConverter converter) + { + _converter = converter; + _converterStrategy = converter.ConverterStrategy; + } + internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T? value) => Converter.OnTryRead(ref reader, typeToConvert, options, ref state, out value); @@ -67,7 +73,7 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializer jsonTypeInfo is JsonTypeInfo info && info.SerializeHandler != null && !state.CurrentContainsMetadata && // Do not use the fast path if state needs to write metadata. - info.Options.JsonSerializerContext?.CanUseSerializationLogic == true) + info.Options.SerializerContext?.CanUseSerializationLogic == true) { info.SerializeHandler(writer, value); return true; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs index 6c0a095061fac..d077627720bda 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs @@ -86,7 +86,7 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, object? va Debug.Assert(value != null); Type runtimeType = value.GetType(); - JsonConverter runtimeConverter = options.GetConverterInternal(runtimeType); + JsonConverter runtimeConverter = options.GetConverterFromTypeInfo(runtimeType); if (runtimeConverter == this) { ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(runtimeType, this); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index cc8216363a4da..19a14dac9980f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -36,7 +36,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(jsonTypeInfo.Type, ref reader, ref state); } - obj = jsonTypeInfo.CreateObject!()!; + obj = jsonTypeInfo.CreateObject()!; if (obj is IJsonOnDeserializing onDeserializing) { @@ -132,7 +132,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(jsonTypeInfo.Type, ref reader, ref state); } - obj = jsonTypeInfo.CreateObject!()!; + obj = jsonTypeInfo.CreateObject()!; if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Id)) { @@ -206,7 +206,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, if (state.Current.PropertyState < StackFramePropertyState.ReadValue) { - if (!jsonPropertyInfo.ShouldDeserialize) + if (!jsonPropertyInfo.CanDeserialize) { if (!reader.TrySkip()) { @@ -301,7 +301,7 @@ internal sealed override bool OnTryWrite( for (int i = 0; i < properties.Count; i++) { JsonPropertyInfo jsonPropertyInfo = properties[i].Value!; - if (jsonPropertyInfo.ShouldSerialize) + if (jsonPropertyInfo.CanSerialize) { // Remember the current property for JsonPath support if an exception is thrown. state.Current.JsonPropertyInfo = jsonPropertyInfo; @@ -317,7 +317,7 @@ internal sealed override bool OnTryWrite( // Write extension data after the normal properties. JsonPropertyInfo? dataExtensionProperty = jsonTypeInfo.DataExtensionProperty; - if (dataExtensionProperty?.ShouldSerialize == true) + if (dataExtensionProperty?.CanSerialize == true) { // Remember the current property for JsonPath support if an exception is thrown. state.Current.JsonPropertyInfo = dataExtensionProperty; @@ -355,14 +355,14 @@ internal sealed override bool OnTryWrite( { JsonPropertyInfo? jsonPropertyInfo = propertyList![state.Current.EnumeratorIndex].Value; Debug.Assert(jsonPropertyInfo != null); - if (jsonPropertyInfo.ShouldSerialize) + if (jsonPropertyInfo.CanSerialize) { state.Current.JsonPropertyInfo = jsonPropertyInfo; state.Current.NumberHandling = jsonPropertyInfo.EffectiveNumberHandling; if (!jsonPropertyInfo.GetMemberAndWriteJson(obj!, ref state, writer)) { - Debug.Assert(jsonPropertyInfo.ConverterBase.ConverterStrategy != ConverterStrategy.Value); + Debug.Assert(jsonPropertyInfo.EffectiveConverter.ConverterStrategy != ConverterStrategy.Value); return false; } @@ -384,7 +384,7 @@ internal sealed override bool OnTryWrite( if (state.Current.EnumeratorIndex == propertyList.Count) { JsonPropertyInfo? dataExtensionProperty = jsonTypeInfo.DataExtensionProperty; - if (dataExtensionProperty?.ShouldSerialize == true) + if (dataExtensionProperty?.CanSerialize == true) { // Remember the current property for JsonPath support if an exception is thrown. state.Current.JsonPropertyInfo = dataExtensionProperty; @@ -434,7 +434,7 @@ protected static void ReadPropertyValue( bool useExtensionProperty) { // Skip the property if not found. - if (!jsonPropertyInfo.ShouldDeserialize) + if (!jsonPropertyInfo.CanDeserialize) { reader.Skip(); } @@ -464,7 +464,7 @@ protected static bool ReadAheadPropertyValue(ref ReadStack state, ref Utf8JsonRe if (!state.Current.UseExtensionProperty) { - if (!SingleValueReadWithReadAhead(jsonPropertyInfo.ConverterBase.RequiresReadAhead, ref reader, ref state)) + if (!SingleValueReadWithReadAhead(jsonPropertyInfo.EffectiveConverter.RequiresReadAhead, ref reader, ref state)) { return false; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index a3aaf6f298fc9..9440b5375f1d8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -23,6 +23,14 @@ internal abstract partial class ObjectWithParameterizedConstructorConverter : internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value) { + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; + + if (jsonTypeInfo.CreateObject != null) + { + // Contract customization: fall back to default object converter if user has set a default constructor delegate. + return base.OnTryRead(ref reader, typeToConvert, options, ref state, out value); + } + object obj; ArgumentState argumentState = state.Current.CtorArgumentState!; @@ -91,7 +99,6 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo else { // Slower path that supports continuation and metadata reads. - JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; if (state.Current.ObjectState == StackFrameObjectState.None) { @@ -289,7 +296,7 @@ private void ReadConstructorArguments(ref ReadStack state, ref Utf8JsonReader re out _, createExtensionProperty: false); - if (jsonPropertyInfo.ShouldDeserialize) + if (jsonPropertyInfo.CanDeserialize) { ArgumentState argumentState = state.Current.CtorArgumentState!; @@ -454,7 +461,7 @@ private static bool HandlePropertyWithContinuation( { if (state.Current.PropertyState < StackFramePropertyState.ReadValue) { - if (!jsonPropertyInfo.ShouldDeserialize) + if (!jsonPropertyInfo.CanDeserialize) { if (!reader.TrySkip()) { @@ -488,7 +495,7 @@ private static bool HandlePropertyWithContinuation( } } - Debug.Assert(jsonPropertyInfo.ShouldDeserialize); + Debug.Assert(jsonPropertyInfo.CanDeserialize); // Ensure that the cache has enough capacity to add this property. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs index cbc89a4fb7bba..36f74f39bf8d0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs @@ -22,7 +22,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer Type valueTypeToConvert = typeToConvert.GetGenericArguments()[0]; - JsonConverter valueConverter = options.GetConverterInternal(valueTypeToConvert); + JsonConverter valueConverter = options.GetConverterFromTypeInfo(valueTypeToConvert); Debug.Assert(valueConverter != null); // If the value type has an interface or object converter, just return that converter directly. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index fd6acba560a81..bfff00aa3e39a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -1,8 +1,10 @@ // 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.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Text.Json.Serialization.Converters; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization @@ -65,10 +67,12 @@ internal virtual void ReadElementAndSetProperty( throw new InvalidOperationException(SR.NodeJsonObjectCustomConverterNotAllowedOnExtensionProperty); } - internal abstract JsonPropertyInfo CreateJsonPropertyInfo(); + internal abstract JsonPropertyInfo CreateJsonPropertyInfo(JsonTypeInfo parentTypeInfo); internal abstract JsonParameterInfo CreateJsonParameterInfo(); + internal abstract JsonConverter CreateCastingConverter(); + internal abstract Type? ElementType { get; } internal abstract Type? KeyType { get; } @@ -121,6 +125,19 @@ internal static bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state) // Whether a type (ConverterStrategy.Object) is deserialized using a parameterized constructor. internal virtual bool ConstructorIsParameterized { get; } + /// + /// For reflection-based metadata generation, indicates whether the + /// converter avails of default constructors when deserializing types. + /// + internal bool UsesDefaultConstructor => + ConverterStrategy switch + { + ConverterStrategy.Object => !ConstructorIsParameterized && this is not ObjectConverter, + ConverterStrategy.Enumerable or + ConverterStrategy.Dictionary => true, + _ => false + }; + internal ConstructorInfo? ConstructorInfo { get; set; } /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs index 103425277d1c7..dcb8c95011108 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs @@ -32,7 +32,7 @@ protected JsonConverterFactory() { } /// public abstract JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options); - internal override JsonPropertyInfo CreateJsonPropertyInfo() + internal override JsonPropertyInfo CreateJsonPropertyInfo(JsonTypeInfo parentTypeInfo) { Debug.Fail("We should never get here."); @@ -133,5 +133,11 @@ internal sealed override void WriteAsPropertyNameCoreAsObject( throw new InvalidOperationException(); } + + internal sealed override JsonConverter CreateCastingConverter() + { + ThrowHelper.ThrowInvalidOperationException_ConverterCanConvertMultipleTypes(typeof(TTarget), this); + return null!; + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs index 98e477f95d398..a4dee4e3ff90a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs @@ -17,6 +17,21 @@ public abstract partial class JsonConverter : JsonConverter /// When overidden, constructs a new instance. /// protected internal JsonConverter() + { + Initialize(); + } + + internal JsonConverter(bool initialize) + { + // Initialize uses abstract members, in order for them to be initialized correctly + // without throwing we need to delay call to Initialize + if (initialize) + { + Initialize(); + } + } + + internal void Initialize() { IsValueType = typeof(T).IsValueType; IsInternalConverter = GetType().Assembly == typeof(JsonConverter).Assembly; @@ -54,9 +69,9 @@ public override bool CanConvert(Type typeToConvert) internal override ConverterStrategy ConverterStrategy => ConverterStrategy.Value; - internal sealed override JsonPropertyInfo CreateJsonPropertyInfo() + internal sealed override JsonPropertyInfo CreateJsonPropertyInfo(JsonTypeInfo parentTypeInfo) { - return new JsonPropertyInfo(); + return new JsonPropertyInfo(parentTypeInfo); } internal sealed override JsonParameterInfo CreateJsonParameterInfo() @@ -64,6 +79,11 @@ internal sealed override JsonParameterInfo CreateJsonParameterInfo() return new JsonParameterInfo(); } + internal sealed override JsonConverter CreateCastingConverter() + { + return new CastingConverter(this); + } + internal override Type? KeyType => null; internal override Type? ElementType => null; @@ -318,7 +338,7 @@ value is not null && state.Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted) { JsonTypeInfo jsonTypeInfo = state.PeekNestedJsonTypeInfo(); - Debug.Assert(jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.TypeToConvert == TypeToConvert); + Debug.Assert(jsonTypeInfo.Converter.TypeToConvert == TypeToConvert); bool canBePolymorphic = CanBePolymorphic || jsonTypeInfo.PolymorphicTypeResolver is not null; JsonConverter? polymorphicConverter = canBePolymorphic ? @@ -528,7 +548,11 @@ public abstract void Write( /// Method should be overridden in custom converters of types used in deserialized dictionary keys. public virtual T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (!IsInternalConverter && options.TryGetDefaultSimpleConverter(TypeToConvert, out JsonConverter? defaultConverter)) + if (!IsInternalConverter && + options.SerializerContext is null && // For consistency do not return any default converters for + // options instances linked to a JsonSerializerContext, + // even if the default converters might have been rooted. + DefaultJsonTypeInfoResolver.TryGetDefaultSimpleConverter(TypeToConvert, out JsonConverter? defaultConverter)) { // .NET 5 backward compatibility: hardcode the default converter for primitive key serialization. Debug.Assert(defaultConverter.IsInternalConverter && defaultConverter is JsonConverter); @@ -562,7 +586,11 @@ internal virtual T ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeTo /// Method should be overridden in custom converters of types used in serialized dictionary keys. public virtual void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { - if (!IsInternalConverter && options.TryGetDefaultSimpleConverter(TypeToConvert, out JsonConverter? defaultConverter)) + if (!IsInternalConverter && + options.SerializerContext is null && // For consistency do not return any default converters for + // options instances linked to a JsonSerializerContext, + // even if the default converters might have been rooted. + DefaultJsonTypeInfoResolver.TryGetDefaultSimpleConverter(TypeToConvert, out JsonConverter? defaultConverter)) { // .NET 5 backward compatibility: hardcode the default converter for primitive key serialization. Debug.Assert(defaultConverter.IsInternalConverter && defaultConverter is JsonConverter); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index 04aa45166638d..f51a2cac4bbc3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -121,12 +121,16 @@ internal static void CreateDataExtensionProperty( genericArgs[1].UnderlyingSystemType == typeof(JsonElement) || genericArgs[1].UnderlyingSystemType == typeof(Nodes.JsonNode)); #endif - if (jsonPropertyInfo.JsonTypeInfo.CreateObject == null) + + Func? createObjectForExtensionDataProp = jsonPropertyInfo.JsonTypeInfo.CreateObject + ?? jsonPropertyInfo.JsonTypeInfo.CreateObjectForExtensionDataProperty; + + if (createObjectForExtensionDataProp == null) { // Avoid a reference to the JsonNode type for trimming if (jsonPropertyInfo.PropertyType.FullName == JsonTypeInfo.JsonObjectTypeName) { - extensionData = jsonPropertyInfo.ConverterBase.CreateObject(options); + extensionData = jsonPropertyInfo.EffectiveConverter.CreateObject(options); } else { @@ -135,7 +139,7 @@ internal static void CreateDataExtensionProperty( } else { - extensionData = jsonPropertyInfo.JsonTypeInfo.CreateObject(); + extensionData = createObjectForExtensionDataProp(); } jsonPropertyInfo.SetExtensionDictionaryAsObject(obj, extensionData); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs index 29869dda32cfd..6a157f9ba3f73 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs @@ -35,7 +35,7 @@ public static partial class JsonSerializer state.Initialize(jsonTypeInfo); TValue? value; - JsonConverter jsonConverter = jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase; + JsonConverter jsonConverter = jsonTypeInfo.Converter; // For performance, the code below is a lifted ReadCore() above. if (jsonConverter is JsonConverter converter) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs index 55ba3ef0a2496..16415a1cdb634 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs @@ -440,7 +440,7 @@ private static async IAsyncEnumerable CreateAsyncEnumerableDeserializer< ref bufferState, ref jsonReaderState, ref readStack, - queueTypeInfo.PropertyInfoForTypeInfo.ConverterBase, + queueTypeInfo.Converter, options); if (readStack.Current.ReturnValue is Queue queue) @@ -469,7 +469,7 @@ private static async IAsyncEnumerable CreateAsyncEnumerableDeserializer< ReadStack readStack = default; jsonTypeInfo.EnsureConfigured(); readStack.Initialize(jsonTypeInfo, supportContinuation: true); - JsonConverter converter = readStack.Current.JsonPropertyInfo!.ConverterBase; + JsonConverter converter = readStack.Current.JsonPropertyInfo!.EffectiveConverter; var jsonReaderState = new JsonReaderState(options.GetReaderOptions()); try @@ -500,7 +500,7 @@ private static async IAsyncEnumerable CreateAsyncEnumerableDeserializer< ReadStack readStack = default; jsonTypeInfo.EnsureConfigured(); readStack.Initialize(jsonTypeInfo, supportContinuation: true); - JsonConverter converter = readStack.Current.JsonPropertyInfo!.ConverterBase; + JsonConverter converter = readStack.Current.JsonPropertyInfo!.EffectiveConverter; var jsonReaderState = new JsonReaderState(options.GetReaderOptions()); try diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs index d98fd97672bdb..5fb3adb99ba6b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs @@ -422,7 +422,7 @@ public static partial class JsonSerializer var newReader = new Utf8JsonReader(rentedSpan, originalReaderOptions); - JsonConverter jsonConverter = state.Current.JsonPropertyInfo!.ConverterBase; + JsonConverter jsonConverter = state.Current.JsonPropertyInfo!.EffectiveConverter; TValue? value = ReadCore(jsonConverter, ref newReader, jsonTypeInfo.Options, ref state); // The reader should have thrown if we have remaining bytes. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs index e9d3932df45f3..42da3e131bd4d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs @@ -41,7 +41,7 @@ private static void WriteUsingGeneratedSerializer(Utf8JsonWriter writer, if (jsonTypeInfo.HasSerialize && jsonTypeInfo is JsonTypeInfo typedInfo && - typedInfo.Options.JsonSerializerContext?.CanUseSerializationLogic == true) + typedInfo.Options.SerializerContext?.CanUseSerializationLogic == true) { Debug.Assert(typedInfo.SerializeHandler != null); typedInfo.SerializeHandler(writer, value); @@ -59,15 +59,15 @@ private static void WriteUsingSerializer(Utf8JsonWriter writer, in TValu Debug.Assert(!jsonTypeInfo.HasSerialize || jsonTypeInfo is not JsonTypeInfo || - jsonTypeInfo.Options.JsonSerializerContext == null || - !jsonTypeInfo.Options.JsonSerializerContext.CanUseSerializationLogic, + jsonTypeInfo.Options.SerializerContext == null || + !jsonTypeInfo.Options.SerializerContext.CanUseSerializationLogic, "Incorrect method called. WriteUsingGeneratedSerializer() should have been called instead."); WriteStack state = default; jsonTypeInfo.EnsureConfigured(); state.Initialize(jsonTypeInfo, supportContinuation: false, supportAsync: false); - JsonConverter converter = jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase; + JsonConverter converter = jsonTypeInfo.Converter; Debug.Assert(converter != null); Debug.Assert(jsonTypeInfo.Options != null); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs index 0b8385b5210d6..7e05b6347d1b6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization @@ -8,7 +9,7 @@ namespace System.Text.Json.Serialization /// /// Provides metadata about a set of types that is relevant to JSON serialization. /// - public abstract partial class JsonSerializerContext + public abstract partial class JsonSerializerContext : IJsonTypeInfoResolver { private bool? _canUseSerializationLogic; @@ -19,9 +20,9 @@ public abstract partial class JsonSerializerContext /// when instanciating the context, then a new instance is bound and returned. /// /// - /// The instance cannot be mutated once it is bound with the context instance. + /// The instance cannot be mutated once it is bound to the context instance. /// - public JsonSerializerOptions Options => _options ??= new JsonSerializerOptions { JsonSerializerContext = this }; + public JsonSerializerOptions Options => _options ??= new JsonSerializerOptions { TypeInfoResolver = this }; /// /// Indicates whether pre-generated serialization logic for types in the context @@ -83,8 +84,8 @@ protected JsonSerializerContext(JsonSerializerOptions? options) { if (options != null) { - options.JsonSerializerContext = this; - _options = options; + options.TypeInfoResolver = this; + Debug.Assert(_options == options, "options.TypeInfoResolver setter did not assign options"); } } @@ -94,5 +95,16 @@ protected JsonSerializerContext(JsonSerializerOptions? options) /// The type to fetch metadata about. /// The metadata for the specified type, or if the context has no metadata for the type. public abstract JsonTypeInfo? GetTypeInfo(Type type); + + JsonTypeInfo? IJsonTypeInfoResolver.GetTypeInfo(Type type, JsonSerializerOptions options) + { + if (options != null && _options != options) + { + // TODO is this the appropriate exception message to throw? + ThrowHelper.ThrowInvalidOperationException_SerializerContextOptionsImmutable(); + } + + return GetTypeInfo(type); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs index 482cdc0466fb0..a1f67039f0b80 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs @@ -23,15 +23,27 @@ public sealed partial class JsonSerializerOptions // Simple LRU cache for the public (de)serialize entry points that avoid some lookups in _cachingContext. private volatile JsonTypeInfo? _lastTypeInfo; + /// + /// This method returns configured non-null JsonTypeInfo + /// internal JsonTypeInfo GetOrAddJsonTypeInfo(Type type) { if (_cachingContext == null) { InitializeCachingContext(); - Debug.Assert(_cachingContext != null); } - return _cachingContext.GetOrAddJsonTypeInfo(type); + JsonTypeInfo? typeInfo = _cachingContext.GetOrAddJsonTypeInfo(type); + + if (typeInfo == null) + { + ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type); + return null; + } + + typeInfo.EnsureConfigured(); + + return typeInfo; } internal bool TryGetJsonTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo) @@ -71,13 +83,11 @@ internal void ClearCaches() _lastTypeInfo = null; } + [MemberNotNull(nameof(_cachingContext))] private void InitializeCachingContext() { + _isLockedInstance = true; _cachingContext = TrackedCachingContexts.GetOrCreate(this); - if (IsInitializedForReflectionSerializer) - { - _cachingContext.Options.IsInitializedForReflectionSerializer = true; - } } /// @@ -88,7 +98,6 @@ private void InitializeCachingContext() /// internal sealed class CachingContext { - private readonly ConcurrentDictionary _converterCache = new(); private readonly ConcurrentDictionary _jsonTypeInfoCache = new(); public CachingContext(JsonSerializerOptions options) @@ -99,15 +108,29 @@ public CachingContext(JsonSerializerOptions options) public JsonSerializerOptions Options { get; } // Property only accessed by reflection in testing -- do not remove. // If changing please ensure that src/ILLink.Descriptors.LibraryBuild.xml is up-to-date. - public int Count => _converterCache.Count + _jsonTypeInfoCache.Count; - public JsonConverter GetOrAddConverter(Type type) => _converterCache.GetOrAdd(type, Options.GetConverterFromType); - public JsonTypeInfo GetOrAddJsonTypeInfo(Type type) => _jsonTypeInfoCache.GetOrAdd(type, Options.GetJsonTypeInfoFromContextOrCreate); + public int Count => _jsonTypeInfoCache.Count; + + public JsonTypeInfo? GetOrAddJsonTypeInfo(Type type) + { + if (_jsonTypeInfoCache.TryGetValue(type, out JsonTypeInfo? typeInfo)) + { + return typeInfo; + } + + typeInfo = Options.GetTypeInfoInternal(type); + if (typeInfo != null) + { + return _jsonTypeInfoCache.GetOrAdd(type, _ => typeInfo); + } + + return null; + } + public bool TryGetJsonTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo) => _jsonTypeInfoCache.TryGetValue(type, out typeInfo); public bool IsJsonTypeInfoCached(Type type) => _jsonTypeInfoCache.ContainsKey(type); public void Clear() { - _converterCache.Clear(); _jsonTypeInfoCache.Clear(); } } @@ -129,6 +152,7 @@ internal static class TrackedCachingContexts public static CachingContext GetOrCreate(JsonSerializerOptions options) { + Debug.Assert(options._isLockedInstance, "Cannot create caching contexts for mutable JsonSerializerOptions instances"); ConcurrentDictionary> cache = s_cache; if (cache.TryGetValue(options, out WeakReference? wr) && wr.TryGetTarget(out CachingContext? ctx)) @@ -167,7 +191,7 @@ public static CachingContext GetOrCreate(JsonSerializerOptions options) { // Copy fields ignored by the copy constructor // but are necessary to determine equivalence. - _serializerContext = options._serializerContext, + _typeInfoResolver = options._typeInfoResolver, }; Debug.Assert(key._cachingContext == null); @@ -269,6 +293,7 @@ private sealed class EqualityComparer : IEqualityComparer public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right) { Debug.Assert(left != null && right != null); + return left._dictionaryKeyPolicy == right._dictionaryKeyPolicy && left._jsonPropertyNamingPolicy == right._jsonPropertyNamingPolicy && @@ -287,11 +312,9 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right) left._includeFields == right._includeFields && left._propertyNameCaseInsensitive == right._propertyNameCaseInsensitive && left._writeIndented == right._writeIndented && - left._serializerContext == right._serializerContext && + NormalizeResolver(left._typeInfoResolver) == NormalizeResolver(right._typeInfoResolver) && CompareLists(left._converters, right._converters) && -#pragma warning disable CA2252 // This API requires opting into preview features CompareLists(left._polymorphicTypeConfigurations, right._polymorphicTypeConfigurations); -#pragma warning restore CA2252 // This API requires opting into preview features static bool CompareLists(ConfigurationList left, ConfigurationList right) { @@ -334,11 +357,9 @@ public int GetHashCode(JsonSerializerOptions options) hc.Add(options._includeFields); hc.Add(options._propertyNameCaseInsensitive); hc.Add(options._writeIndented); - hc.Add(options._serializerContext); + hc.Add(NormalizeResolver(options._typeInfoResolver)); GetHashCode(ref hc, options._converters); -#pragma warning disable CA2252 // This API requires opting into preview features GetHashCode(ref hc, options._polymorphicTypeConfigurations); -#pragma warning restore CA2252 // This API requires opting into preview features static void GetHashCode(ref HashCode hc, ConfigurationList list) { @@ -351,6 +372,10 @@ static void GetHashCode(ref HashCode hc, ConfigurationList list) return hc.ToHashCode(); } + // An options instance might be locked but not initialized for reflection serialization yet. + private static IJsonTypeInfoResolver? NormalizeResolver(IJsonTypeInfoResolver? resolver) + => resolver ?? DefaultJsonTypeInfoResolver.DefaultInstance; + #if !NETCOREAPP /// /// Polyfill for System.HashCode. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index d41325d738138..e92ebb0408288 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Runtime.ExceptionServices; using System.Text.Json.Reflection; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Converters; @@ -19,127 +18,6 @@ namespace System.Text.Json /// public sealed partial class JsonSerializerOptions { - // The global list of built-in simple converters. - private static Dictionary? s_defaultSimpleConverters; - - // The global list of built-in converters that override CanConvert(). - private static JsonConverter[]? s_defaultFactoryConverters; - - // Stores the JsonTypeInfo factory, which requires unreferenced code and must be rooted by the reflection-based serializer. - private static Func? s_typeInfoCreationFunc; - - [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - private static void RootReflectionSerializerDependencies() - { - // s_typeInfoCreationFunc is the last field assigned. - // Use it as the sentinel to ensure that all dependencies are initialized. - if (Volatile.Read(ref s_typeInfoCreationFunc) is null) - { - s_defaultSimpleConverters = GetDefaultSimpleConverters(); - s_defaultFactoryConverters = GetDefaultFactoryConverters(); - // Explicitly ensure that the previous fields are initialized along with this one. - Volatile.Write(ref s_typeInfoCreationFunc, CreateJsonTypeInfo); - } - - [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) - { - JsonTypeInfo.ValidateType(type, null, null, options); - - MethodInfo methodInfo = typeof(JsonSerializerOptions).GetMethod(nameof(CreateReflectionJsonTypeInfo), BindingFlags.NonPublic | BindingFlags.Instance)!; -#if NETCOREAPP - return (JsonTypeInfo)methodInfo.MakeGenericMethod(type).Invoke(options, BindingFlags.NonPublic | BindingFlags.DoNotWrapExceptions, null, null, null)!; -#else - try - { - return (JsonTypeInfo)methodInfo.MakeGenericMethod(type).Invoke(options, null)!; - } - catch (TargetInvocationException ex) - { - // Some of the validation is done during construction (i.e. validity of JsonConverter, inner types etc.) - // therefore we need to unwrap TargetInvocationException for better user experience - ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); - throw null!; - } -#endif - } - } - - [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - private JsonTypeInfo CreateReflectionJsonTypeInfo() - { - return new ReflectionJsonTypeInfo(this); - } - - [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - private static JsonConverter[] GetDefaultFactoryConverters() - { - return new JsonConverter[] - { - // Check for disallowed types. - new UnsupportedTypeConverterFactory(), - // Nullable converter should always be next since it forwards to any nullable type. - new NullableConverterFactory(), - new EnumConverterFactory(), - new JsonNodeConverterFactory(), - new FSharpTypeConverterFactory(), - // IAsyncEnumerable takes precedence over IEnumerable. - new IAsyncEnumerableConverterFactory(), - // IEnumerable should always be second to last since they can convert any IEnumerable. - new IEnumerableConverterFactory(), - // Object should always be last since it converts any type. - new ObjectConverterFactory() - }; - } - - private static Dictionary GetDefaultSimpleConverters() - { - const int NumberOfSimpleConverters = 26; - var converters = new Dictionary(NumberOfSimpleConverters); - - // Use a dictionary for simple converters. - // When adding to this, update NumberOfSimpleConverters above. - Add(JsonMetadataServices.BooleanConverter); - Add(JsonMetadataServices.ByteConverter); - Add(JsonMetadataServices.ByteArrayConverter); - Add(JsonMetadataServices.CharConverter); - Add(JsonMetadataServices.DateTimeConverter); - Add(JsonMetadataServices.DateTimeOffsetConverter); -#if NETCOREAPP - Add(JsonMetadataServices.DateOnlyConverter); - Add(JsonMetadataServices.TimeOnlyConverter); -#endif - Add(JsonMetadataServices.DoubleConverter); - Add(JsonMetadataServices.DecimalConverter); - Add(JsonMetadataServices.GuidConverter); - Add(JsonMetadataServices.Int16Converter); - Add(JsonMetadataServices.Int32Converter); - Add(JsonMetadataServices.Int64Converter); - Add(JsonMetadataServices.JsonElementConverter); - Add(JsonMetadataServices.JsonDocumentConverter); - Add(JsonMetadataServices.ObjectConverter); - Add(JsonMetadataServices.SByteConverter); - Add(JsonMetadataServices.SingleConverter); - Add(JsonMetadataServices.StringConverter); - Add(JsonMetadataServices.TimeSpanConverter); - Add(JsonMetadataServices.UInt16Converter); - Add(JsonMetadataServices.UInt32Converter); - Add(JsonMetadataServices.UInt64Converter); - Add(JsonMetadataServices.UriConverter); - Add(JsonMetadataServices.VersionConverter); - - Debug.Assert(converters.Count <= NumberOfSimpleConverters); - - return converters; - - void Add(JsonConverter converter) => - converters.Add(converter.TypeToConvert, converter); - } - /// /// The list of custom converters. /// @@ -156,11 +34,11 @@ void Add(JsonConverter converter) => /// public IList PolymorphicTypeConfigurations => _polymorphicTypeConfigurations; - internal JsonConverter GetConverterFromMember(Type? parentClassType, Type propertyType, MemberInfo? memberInfo) + // This may return factory converter + internal JsonConverter? GetCustomConverterFromMember(Type? parentClassType, Type typeToConvert, MemberInfo? memberInfo) { - JsonConverter converter = null!; + JsonConverter? converter = null; - // Priority 1: attempt to get converter from JsonConverterAttribute on property. if (memberInfo != null) { Debug.Assert(parentClassType != null); @@ -170,24 +48,44 @@ internal JsonConverter GetConverterFromMember(Type? parentClassType, Type proper if (converterAttribute != null) { - converter = GetConverterFromAttribute(converterAttribute, typeToConvert: propertyType, classTypeAttributeIsOn: parentClassType!, memberInfo); + converter = GetConverterFromAttribute(converterAttribute, typeToConvert, classTypeAttributeIsOn: parentClassType!, memberInfo); } } - if (converter == null) - { - converter = GetConverterInternal(propertyType); - Debug.Assert(converter != null); - } + return converter; + } + /// + /// Gets converter for type but does not use TypeInfoResolver + /// + internal JsonConverter GetConverterForType(Type typeToConvert) + { + JsonConverter converter = GetConverterFromOptionsOrReflectionConverter(typeToConvert); + Debug.Assert(converter != null); + + converter = ExpandFactoryConverter(converter, typeToConvert); + + CheckConverterNullabilityIsSameAsPropertyType(converter, typeToConvert); + + return converter; + } + + [return: NotNullIfNotNull("converter")] + internal JsonConverter? ExpandFactoryConverter(JsonConverter? converter, Type typeToConvert) + { if (converter is JsonConverterFactory factory) { - converter = factory.GetConverterInternal(propertyType, this); + converter = factory.GetConverterInternal(typeToConvert, this); // A factory cannot return null; GetConverterInternal checked for that. Debug.Assert(converter != null); } + return converter; + } + + internal static void CheckConverterNullabilityIsSameAsPropertyType(JsonConverter converter, Type propertyType) + { // User has indicated that either: // a) a non-nullable-struct handling converter should handle a nullable struct type or // b) a nullable-struct handling converter should handle a non-nullable struct type. @@ -201,8 +99,6 @@ internal JsonConverter GetConverterFromMember(Type? parentClassType, Type proper { ThrowHelper.ThrowInvalidOperationException_ConverterCanConvertMultipleTypes(propertyType, converter); } - - return converter; } /// @@ -228,40 +124,68 @@ public JsonConverter GetConverter(Type typeToConvert) ThrowHelper.ThrowArgumentNullException(nameof(typeToConvert)); } - RootReflectionSerializerDependencies(); - return GetConverterInternal(typeToConvert); + DefaultJsonTypeInfoResolver.RootDefaultInstance(); + return GetConverterFromTypeInfo(typeToConvert); } - internal JsonConverter GetConverterInternal(Type typeToConvert) + /// + /// Same as GetConverter but does not root converters + /// + internal JsonConverter GetConverterFromTypeInfo(Type typeToConvert) { - // Only cache the value once (de)serialization has occurred since new converters can be added that may change the result. - if (_cachingContext != null) + if (_cachingContext == null) { - return _cachingContext.GetOrAddConverter(typeToConvert); + if (_isLockedInstance) + { + InitializeCachingContext(); + } + else + { + // We do not want to lock options instance here but we need to return correct answer + // which means we need to go through TypeInfoResolver but without caching because that's the + // only place which will have correct converter for JsonSerializerContext and reflection + // based resolver. It will also work correctly for combined resolvers. + return GetTypeInfoInternal(typeToConvert)?.Converter + ?? GetConverterFromOptionsOrReflectionConverter(typeToConvert); + + } } - return GetConverterFromType(typeToConvert); - } + JsonConverter? converter = _cachingContext.GetOrAddJsonTypeInfo(typeToConvert)?.Converter; - private JsonConverter GetConverterFromType(Type typeToConvert) - { - Debug.Assert(typeToConvert != null); + // we can get here if resolver returned null but converter was added for the type + converter ??= GetConverterFromOptions(typeToConvert); - // Priority 1: If there is a JsonSerializerContext, fetch the converter from there. - JsonConverter? converter = _serializerContext?.GetTypeInfo(typeToConvert)?.PropertyInfoForTypeInfo?.ConverterBase; + if (converter == null) + { + ThrowHelper.ThrowNotSupportedException_BuiltInConvertersNotRooted(typeToConvert); + return null!; + } - // Priority 2: Attempt to get custom converter added at runtime. - // Currently there is not a way at runtime to override the [JsonConverter] when applied to a property. + return converter; + } + + private JsonConverter? GetConverterFromOptions(Type typeToConvert) + { foreach (JsonConverter item in _converters) { if (item.CanConvert(typeToConvert)) { - converter = item; - break; + return item; } } - // Priority 3: Attempt to get converter from [JsonConverter] on the type being converted. + return null; + } + + private JsonConverter GetConverterFromOptionsOrReflectionConverter(Type typeToConvert) + { + Debug.Assert(typeToConvert != null); + + // Priority 1: Attempt to get custom converter from the Converters list. + JsonConverter? converter = GetConverterFromOptions(typeToConvert); + + // Priority 2: Attempt to get converter from [JsonConverter] on the type being converted. if (converter == null) { JsonConverterAttribute? converterAttribute = (JsonConverterAttribute?) @@ -273,38 +197,10 @@ private JsonConverter GetConverterFromType(Type typeToConvert) } } - // Priority 4: Attempt to get built-in converter. + // Priority 3: Attempt to get built-in converter. if (converter == null) { - if (s_defaultSimpleConverters == null || s_defaultFactoryConverters == null) - { - // (De)serialization using serializer's options-based methods has not yet occurred, so the built-in converters are not rooted. - // Even though source-gen code paths do not call this method , we do not root all the - // built-in converters here since we fetch converters for any type included for source generation from the binded context (Priority 1). - Debug.Assert(s_defaultSimpleConverters == null); - Debug.Assert(s_defaultFactoryConverters == null); - ThrowHelper.ThrowNotSupportedException_BuiltInConvertersNotRooted(typeToConvert); - return null!; - } - - if (s_defaultSimpleConverters.TryGetValue(typeToConvert, out JsonConverter? foundConverter)) - { - converter = foundConverter; - } - else - { - foreach (JsonConverter item in s_defaultFactoryConverters) - { - if (item.CanConvert(typeToConvert)) - { - converter = item; - break; - } - } - - // Since the object and IEnumerable converters cover all types, we should have a converter. - Debug.Assert(converter != null); - } + converter = DefaultJsonTypeInfoResolver.GetDefaultConverter(typeToConvert); } // Allow redirection for generic types or the enum converter. @@ -376,21 +272,6 @@ private JsonConverter GetConverterFromAttribute(JsonConverterAttribute converter return converter; } - internal bool TryGetDefaultSimpleConverter(Type typeToConvert, [NotNullWhen(true)] out JsonConverter? converter) - { - if (_serializerContext == null && // For consistency do not return any default converters for - // options instances linked to a JsonSerializerContext, - // even if the default converters might have been rooted. - s_defaultSimpleConverters != null && - s_defaultSimpleConverters.TryGetValue(typeToConvert, out converter)) - { - return true; - } - - converter = null; - return false; - } - private static Attribute? GetAttributeThatCanHaveMultiple(Type classType, Type attributeType, MemberInfo memberInfo) { object[] attributes = memberInfo.GetCustomAttributes(attributeType, inherit: false); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index e444cc9c27b07..a4403c8829b76 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -9,7 +10,6 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -using System.Threading; namespace System.Text.Json { @@ -35,7 +35,7 @@ public sealed partial class JsonSerializerOptions public static JsonSerializerOptions Default { get; } = CreateDefaultImmutableInstance(); // For any new option added, adding it to the options copied in the copy constructor below must be considered. - private JsonSerializerContext? _serializerContext; + private IJsonTypeInfoResolver? _typeInfoResolver; private MemberAccessor? _memberAccessorStrategy; private JsonNamingPolicy? _dictionaryKeyPolicy; private JsonNamingPolicy? _jsonPropertyNamingPolicy; @@ -43,9 +43,7 @@ public sealed partial class JsonSerializerOptions private ReferenceHandler? _referenceHandler; private JavaScriptEncoder? _encoder; private ConfigurationList _converters; -#pragma warning disable CA2252 // This API requires opting into preview features private ConfigurationList _polymorphicTypeConfigurations; -#pragma warning restore CA2252 // This API requires opting into preview features private JsonIgnoreCondition _defaultIgnoreCondition; private JsonNumberHandling _numberHandling; private JsonUnknownTypeHandling _unknownTypeHandling; @@ -60,20 +58,15 @@ public sealed partial class JsonSerializerOptions private bool _propertyNameCaseInsensitive; private bool _writeIndented; + private bool _isLockedInstance; + /// /// Constructs a new instance. /// public JsonSerializerOptions() { - _converters = new ConfigurationList(this); - -#pragma warning disable CA2252 // This API requires opting into preview features - _polymorphicTypeConfigurations = new ConfigurationList(this) - { - OnElementAdded = static config => { config.IsAssignedToOptionsInstance = true; } - }; -#pragma warning restore CA2252 // This API requires opting into preview features - + _converters = new ConverterList(this); + _polymorphicTypeConfigurations = new PolymorphicConfigurationList(this); TrackOptionsInstance(this); } @@ -96,10 +89,8 @@ public JsonSerializerOptions(JsonSerializerOptions options) _jsonPropertyNamingPolicy = options._jsonPropertyNamingPolicy; _readCommentHandling = options._readCommentHandling; _referenceHandler = options._referenceHandler; - _converters = new ConfigurationList(this, options._converters); -#pragma warning disable CA2252 // This API requires opting into preview features - _polymorphicTypeConfigurations = new ConfigurationList(this, options._polymorphicTypeConfigurations); -#pragma warning restore CA2252 // This API requires opting into preview features + _converters = new ConverterList(this, options._converters); + _polymorphicTypeConfigurations = new PolymorphicConfigurationList(this, options._polymorphicTypeConfigurations); _encoder = options._encoder; _defaultIgnoreCondition = options._defaultIgnoreCondition; _numberHandling = options._numberHandling; @@ -114,7 +105,9 @@ public JsonSerializerOptions(JsonSerializerOptions options) _includeFields = options._includeFields; _propertyNameCaseInsensitive = options._propertyNameCaseInsensitive; _writeIndented = options._writeIndented; - + // Preserve backward compatibility with .NET 6 + // This should almost certainly be changed, cf. https://github.com/dotnet/aspnetcore/issues/38720 + _typeInfoResolver = options._typeInfoResolver is JsonSerializerContext ? null : options._typeInfoResolver; EffectiveMaxDepth = options.EffectiveMaxDepth; ReferenceHandlingStrategy = options.ReferenceHandlingStrategy; @@ -166,10 +159,48 @@ public JsonSerializerOptions(JsonSerializerDefaults defaults) : this() { VerifyMutable(); TContext context = new(); - _serializerContext = context; + _typeInfoResolver = context; + _isLockedInstance = true; context._options = this; } + /// + /// Gets or sets JsonTypeInfo resolver. + /// + public IJsonTypeInfoResolver TypeInfoResolver + { + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + get + { + return _typeInfoResolver ?? DefaultJsonTypeInfoResolver.RootDefaultInstance(); + } + set + { + VerifyMutable(); + + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (value is JsonSerializerContext ctx) + { + if (ctx._options != null && ctx._options != this) + { + // TODO evaluate if this is the appropriate behaviour; + ThrowHelper.ThrowInvalidOperationException_SerializerContextOptionsImmutable(); + } + + // Associate options instance with context and lock for further modification + ctx._options = this; + _isLockedInstance = true; + } + + _typeInfoResolver = value; + } + } + /// /// Defines whether an extra comma at the end of a list of JSON values in an object or array /// is allowed (and ignored) within the JSON payload being deserialized. @@ -559,15 +590,7 @@ public ReferenceHandler? ReferenceHandler } } - internal JsonSerializerContext? JsonSerializerContext - { - get => _serializerContext; - set - { - VerifyMutable(); - _serializerContext = value; - } - } + internal JsonSerializerContext? SerializerContext => _typeInfoResolver as JsonSerializerContext; // The cached value used to determine if ReferenceHandler should use Preserve or IgnoreCycles semanitcs or None of them. internal ReferenceHandlingStrategy ReferenceHandlingStrategy = ReferenceHandlingStrategy.None; @@ -596,45 +619,70 @@ internal MemberAccessor MemberAccessorStrategy } } - /// - /// Whether the options instance has been primed for reflection-based serialization. - /// - internal bool IsInitializedForReflectionSerializer; + internal bool IsInitializedForReflectionSerializer { get; private set; } + // Effective resolver, populated when enacting reflection-based fallback + // Should not be taken into account when calculating options equality. + private IJsonTypeInfoResolver? _effectiveJsonTypeInfoResolver; /// /// Initializes the converters for the reflection-based serializer. - /// must be checked before calling. /// [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] internal void InitializeForReflectionSerializer() { - RootReflectionSerializerDependencies(); - Volatile.Write(ref IsInitializedForReflectionSerializer, true); - if (_cachingContext != null) + if (_typeInfoResolver is JsonSerializerContext ctx) + { + // .NET 6 backward compatibility; use fallback to reflection serialization + // TODO: Consider removing this behaviour (needs to be filed as a breaking change). + _effectiveJsonTypeInfoResolver = JsonTypeInfoResolver.Combine(ctx, DefaultJsonTypeInfoResolver.RootDefaultInstance()); + } + else { - _cachingContext.Options.IsInitializedForReflectionSerializer = true; + _typeInfoResolver ??= DefaultJsonTypeInfoResolver.RootDefaultInstance(); } + + if (_cachingContext != null && _cachingContext.Options != this) + { + // We're using a shared caching context deriving from a different options instance; + // for coherence ensure that it has been opted in for reflection-based serialization as well. + _cachingContext.Options.InitializeForReflectionSerializer(); + } + + IsInitializedForReflectionSerializer = true; } - private JsonTypeInfo GetJsonTypeInfoFromContextOrCreate(Type type) + internal bool IsInitializedForMetadataGeneration { get; private set; } + internal void InitializeForMetadataGeneration() { - JsonTypeInfo? info = _serializerContext?.GetTypeInfo(type); - if (info == null && IsInitializedForReflectionSerializer) + IJsonTypeInfoResolver? resolver = _effectiveJsonTypeInfoResolver ?? _typeInfoResolver; + if (resolver == null) { - Debug.Assert( - s_typeInfoCreationFunc != null, - "Reflection-based JsonTypeInfo creator should be initialized if IsInitializedForReflectionSerializer is true."); - info = s_typeInfoCreationFunc(type, this); + ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoUsedButTypeInfoResolverNotSet(); } - if (info == null) + _isLockedInstance = true; + IsInitializedForMetadataGeneration = true; + } + + private JsonTypeInfo? GetTypeInfoInternal(Type type) + { + IJsonTypeInfoResolver? resolver = _effectiveJsonTypeInfoResolver ?? _typeInfoResolver; + JsonTypeInfo? info = resolver?.GetTypeInfo(type, this); + + if (info != null) { - ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type); - return null!; + if (info.Type != type) + { + ThrowHelper.ThrowInvalidOperationException_ResolverTypeNotCompatible(type, info.Type); + } + + if (info.Options != this) + { + ThrowHelper.ThrowInvalidOperationException_ResolverTypeInfoOptionsNotCompatible(); + } } - info.EnsureConfigured(); return info; } @@ -681,16 +729,44 @@ internal JsonWriterOptions GetWriterOptions() internal void VerifyMutable() { - if (_cachingContext != null || _serializerContext != null) + if (_isLockedInstance) + { + ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable(_typeInfoResolver as JsonSerializerContext); + } + } + + private sealed class ConverterList : ConfigurationList + { + private readonly JsonSerializerOptions _options; + + public ConverterList(JsonSerializerOptions options, IList? source = null) + : base(source) { - ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable(_serializerContext); + _options = options; } + + protected override bool IsLockedInstance => _options._isLockedInstance; + protected override void VerifyMutable() => _options.VerifyMutable(); + } + + private sealed class PolymorphicConfigurationList : ConfigurationList + { + private readonly JsonSerializerOptions _options; + + public PolymorphicConfigurationList(JsonSerializerOptions options, IList? source = null) + : base(source) + { + _options = options; + } + + protected override bool IsLockedInstance => _options._isLockedInstance; + protected override void VerifyMutable() => _options.VerifyMutable(); + protected override void OnItemAdded(JsonPolymorphicTypeConfiguration config) => config.IsAssignedToOptionsInstance = true; } private static JsonSerializerOptions CreateDefaultImmutableInstance() { - var options = new JsonSerializerOptions(); - options.InitializeCachingContext(); // eagerly initialize caching context to close type for modification. + var options = new JsonSerializerOptions { _isLockedInstance = true }; return options; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/CustomJsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/CustomJsonTypeInfoOfT.cs new file mode 100644 index 0000000000000..874815923eacb --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/CustomJsonTypeInfoOfT.cs @@ -0,0 +1,47 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json.Serialization.Converters; + +namespace System.Text.Json.Serialization.Metadata +{ + /// + /// Creates and initializes serialization metadata for a type. + /// + /// + internal sealed class CustomJsonTypeInfo : JsonTypeInfo + { + /// + /// Creates serialization metadata for a type using a simple converter. + /// + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + internal CustomJsonTypeInfo(JsonSerializerOptions options) + : base(GetConverter(options), + options) + { + } + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + private static JsonConverter GetConverter(JsonSerializerOptions options) + { + DefaultJsonTypeInfoResolver.RootDefaultInstance(); + return GetEffectiveConverter( + typeof(T), + parentClassType: null, // A TypeInfo never has a "parent" class. + memberInfo: null, // A TypeInfo never has a "parent" property. + options); + } + + internal override JsonParameterInfoValues[] GetParameterInfoValues() + { + // Parametrized constructors not supported yet for custom types + return Array.Empty(); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs new file mode 100644 index 0000000000000..1ab5bff497acf --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs @@ -0,0 +1,126 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Converters; + +namespace System.Text.Json.Serialization.Metadata +{ + public partial class DefaultJsonTypeInfoResolver + { + private static Dictionary? s_defaultSimpleConverters; + private static JsonConverterFactory[]? s_defaultFactoryConverters; + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + private static JsonConverterFactory[] GetDefaultFactoryConverters() + { + return new JsonConverterFactory[] + { + // Check for disallowed types. + new UnsupportedTypeConverterFactory(), + // Nullable converter should always be next since it forwards to any nullable type. + new NullableConverterFactory(), + new EnumConverterFactory(), + new JsonNodeConverterFactory(), + new FSharpTypeConverterFactory(), + // IAsyncEnumerable takes precedence over IEnumerable. + new IAsyncEnumerableConverterFactory(), + // IEnumerable should always be second to last since they can convert any IEnumerable. + new IEnumerableConverterFactory(), + // Object should always be last since it converts any type. + new ObjectConverterFactory() + }; + } + + private static Dictionary GetDefaultSimpleConverters() + { + const int NumberOfSimpleConverters = 26; + var converters = new Dictionary(NumberOfSimpleConverters); + + // Use a dictionary for simple converters. + // When adding to this, update NumberOfSimpleConverters above. + Add(JsonMetadataServices.BooleanConverter); + Add(JsonMetadataServices.ByteConverter); + Add(JsonMetadataServices.ByteArrayConverter); + Add(JsonMetadataServices.CharConverter); + Add(JsonMetadataServices.DateTimeConverter); + Add(JsonMetadataServices.DateTimeOffsetConverter); +#if NETCOREAPP + Add(JsonMetadataServices.DateOnlyConverter); + Add(JsonMetadataServices.TimeOnlyConverter); +#endif + Add(JsonMetadataServices.DoubleConverter); + Add(JsonMetadataServices.DecimalConverter); + Add(JsonMetadataServices.GuidConverter); + Add(JsonMetadataServices.Int16Converter); + Add(JsonMetadataServices.Int32Converter); + Add(JsonMetadataServices.Int64Converter); + Add(JsonMetadataServices.JsonElementConverter); + Add(JsonMetadataServices.JsonDocumentConverter); + Add(JsonMetadataServices.ObjectConverter); + Add(JsonMetadataServices.SByteConverter); + Add(JsonMetadataServices.SingleConverter); + Add(JsonMetadataServices.StringConverter); + Add(JsonMetadataServices.TimeSpanConverter); + Add(JsonMetadataServices.UInt16Converter); + Add(JsonMetadataServices.UInt32Converter); + Add(JsonMetadataServices.UInt64Converter); + Add(JsonMetadataServices.UriConverter); + Add(JsonMetadataServices.VersionConverter); + + Debug.Assert(converters.Count <= NumberOfSimpleConverters); + + return converters; + + void Add(JsonConverter converter) => + converters.Add(converter.TypeToConvert, converter); + } + + internal static JsonConverter GetDefaultConverter(Type typeToConvert) + { + if (s_defaultSimpleConverters == null || s_defaultFactoryConverters == null) + { + // (De)serialization using serializer's options-based methods has not yet occurred, so the built-in converters are not rooted. + // Even though source-gen code paths do not call this method , we do not root all the + // built-in converters here since we fetch converters for any type included for source generation from the binded context (Priority 1). + ThrowHelper.ThrowNotSupportedException_BuiltInConvertersNotRooted(typeToConvert); + return null!; + } + + JsonConverter? converter; + if (s_defaultSimpleConverters.TryGetValue(typeToConvert, out converter)) + { + return converter; + } + else + { + foreach (JsonConverter item in s_defaultFactoryConverters) + { + if (item.CanConvert(typeToConvert)) + { + converter = item; + break; + } + } + + // Since the object and IEnumerable converters cover all types, we should have a converter. + Debug.Assert(converter != null); + return converter; + } + } + + internal static bool TryGetDefaultSimpleConverter(Type typeToConvert, [NotNullWhen(true)] out JsonConverter? converter) + { + if (s_defaultSimpleConverters is null) + { + converter = null; + return false; + } + + return s_defaultSimpleConverters.TryGetValue(typeToConvert, out converter); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.cs new file mode 100644 index 0000000000000..d0baa8e2e18dc --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.cs @@ -0,0 +1,138 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Threading; + +namespace System.Text.Json.Serialization.Metadata +{ + /// + /// Default JsonTypeInfo resolver. + /// + public partial class DefaultJsonTypeInfoResolver : IJsonTypeInfoResolver + { + private bool _mutable; + + /// + /// Constructs DefaultJsonTypeInfoResolver. + /// + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + public DefaultJsonTypeInfoResolver() : this(mutable: true) + { + } + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + private DefaultJsonTypeInfoResolver(bool mutable) + { + _mutable = mutable; + + s_defaultFactoryConverters ??= GetDefaultFactoryConverters(); + s_defaultSimpleConverters ??= GetDefaultSimpleConverters(); + } + + /// + public virtual JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _mutable = false; + + JsonTypeInfo.ValidateType(type, null, null, options); + JsonTypeInfo typeInfo = CreateJsonTypeInfo(type, options); + + if (_modifiers != null) + { + foreach (Action modifier in _modifiers) + { + modifier(typeInfo); + } + } + + return typeInfo; + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "The ctor is marked RequiresUnreferencedCode.")] + [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", + Justification = "The ctor is marked RequiresDynamicCode.")] + private JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) + { + MethodInfo methodInfo = typeof(DefaultJsonTypeInfoResolver).GetMethod(nameof(CreateReflectionJsonTypeInfo), BindingFlags.NonPublic | BindingFlags.Static)!; +#if NETCOREAPP + return (JsonTypeInfo)methodInfo.MakeGenericMethod(type).Invoke(null, BindingFlags.NonPublic | BindingFlags.DoNotWrapExceptions, null, new[] { options }, null)!; +#else + try + { + return (JsonTypeInfo)methodInfo.MakeGenericMethod(type).Invoke(null, new[] { options })!; + } + catch (TargetInvocationException ex) + { + // Some of the validation is done during construction (i.e. validity of JsonConverter, inner types etc.) + // therefore we need to unwrap TargetInvocationException for better user experience + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + throw null!; + } +#endif + } + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + private static JsonTypeInfo CreateReflectionJsonTypeInfo(JsonSerializerOptions options) => new ReflectionJsonTypeInfo(options); + + /// + /// List of JsonTypeInfo modifiers. Modifying callbacks are called consecutively after initial resolution + /// and cannot be changed after GetTypeInfo is called. + /// + public IList> Modifiers => _modifiers ??= new ModifierCollection(this); + private ModifierCollection? _modifiers; + + private sealed class ModifierCollection : ConfigurationList> + { + private readonly DefaultJsonTypeInfoResolver _resolver; + + public ModifierCollection(DefaultJsonTypeInfoResolver resolver) + { + _resolver = resolver; + } + + protected override bool IsLockedInstance => !_resolver._mutable; + protected override void VerifyMutable() + { + if (!_resolver._mutable) + { + ThrowHelper.ThrowInvalidOperationException_TypeInfoResolverImmutable(); + } + } + } + + internal static DefaultJsonTypeInfoResolver? DefaultInstance => s_defaultInstance; + private static DefaultJsonTypeInfoResolver? s_defaultInstance; + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + internal static DefaultJsonTypeInfoResolver RootDefaultInstance() + { + if (s_defaultInstance is DefaultJsonTypeInfoResolver result) + { + return result; + } + + var newInstance = new DefaultJsonTypeInfoResolver(mutable: false); + DefaultJsonTypeInfoResolver? originalInstance = Interlocked.CompareExchange(ref s_defaultInstance, newInstance, comparand: null); + return originalInstance ?? newInstance; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/IJsonTypeInfoResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/IJsonTypeInfoResolver.cs new file mode 100644 index 0000000000000..22f74387153d4 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/IJsonTypeInfoResolver.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace System.Text.Json.Serialization.Metadata +{ + /// + /// Exposes method for resolving Type into JsonTypeInfo for given options. + /// + public interface IJsonTypeInfoResolver + { + /// + /// Resolves Type into JsonTypeInfo which defines serialization and deserialization logic. + /// + /// Type to be resolved. + /// JsonSerializerOptions instance defining resolution parameters. + /// Returns JsonTypeInfo instance or null if the resolver cannot produce metadata for this type. + JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs index 4b605b7175e59..879c1884a8a89 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs @@ -264,13 +264,39 @@ public static JsonConverter GetEnumConverter(JsonSerializerOptions options ThrowHelper.ThrowArgumentNullException(nameof(underlyingTypeInfo)); } - JsonConverter? underlyingConverter = underlyingTypeInfo.PropertyInfoForTypeInfo?.ConverterBase as JsonConverter; - if (underlyingConverter == null) + JsonConverter underlyingConverter = GetTypedConverter(underlyingTypeInfo.Converter); + + return new NullableConverter(underlyingConverter); + } + + /// + /// Creates a instance that converts values. + /// + /// The generic definition for the underlying nullable type. + /// The to use for serialization and deserialization. + /// A instance that converts values + /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. + public static JsonConverter GetNullableConverter(JsonSerializerOptions options) where T : struct + { + if (options is null) { - throw new InvalidOperationException(SR.Format(SR.SerializationConverterNotCompatible, underlyingConverter, typeof(T))); + ThrowHelper.ThrowArgumentNullException(nameof(options)); } + JsonConverter underlyingConverter = GetTypedConverter(options.GetConverterFromTypeInfo(typeof(T))); + return new NullableConverter(underlyingConverter); } + + internal static JsonConverter GetTypedConverter(JsonConverter converter) + { + JsonConverter? typedConverter = converter as JsonConverter; + if (typedConverter == null) + { + throw new InvalidOperationException(SR.Format(SR.SerializationConverterNotCompatible, typedConverter, typeof(T))); + } + + return typedConverter; + } } } 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 848e45f23c51f..a60ec2fe2d508 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 @@ -37,34 +37,18 @@ public static JsonPropertyInfo CreatePropertyInfo(JsonSerializerOptions optio throw new ArgumentException(nameof(propertyInfo.DeclaringType)); } - JsonTypeInfo? propertyTypeInfo = propertyInfo.PropertyTypeInfo; - if (propertyTypeInfo == null) - { - throw new ArgumentException(nameof(propertyInfo.PropertyTypeInfo)); - } - string? propertyName = propertyInfo.PropertyName; if (propertyName == null) { throw new ArgumentException(nameof(propertyInfo.PropertyName)); } - JsonConverter? converter = propertyInfo.Converter; - if (converter == null) - { - converter = propertyTypeInfo.PropertyInfoForTypeInfo.ConverterBase as JsonConverter; - if (converter == null) - { - throw new InvalidOperationException(SR.Format(SR.ConverterForPropertyMustBeValid, declaringType, propertyName, typeof(T))); - } - } - if (!propertyInfo.IsProperty && propertyInfo.IsVirtual) { throw new InvalidOperationException(SR.Format(SR.FieldCannotBeVirtual, nameof(propertyInfo.IsProperty), nameof(propertyInfo.IsVirtual))); } - JsonPropertyInfo jsonPropertyInfo = new JsonPropertyInfo(); + JsonPropertyInfo jsonPropertyInfo = new JsonPropertyInfo(parentTypeInfo: null); jsonPropertyInfo.InitializeForSourceGen(options, propertyInfo); return jsonPropertyInfo; } @@ -100,6 +84,15 @@ public static JsonTypeInfo CreateObjectInfo(JsonSerializerOptions options, /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. public static JsonTypeInfo CreateValueInfo(JsonSerializerOptions options, JsonConverter converter) { + if (options is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(options)); + } + if (converter is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(converter)); + } + JsonTypeInfo info = new SourceGenJsonTypeInfo(converter, options); return info; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs index 91a25e0b41393..4d9403b81d022 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs @@ -61,7 +61,7 @@ public virtual void Initialize(JsonParameterInfoValues parameterInfo, JsonProper PropertyType = matchingProperty.PropertyType; NameAsUtf8Bytes = matchingProperty.NameAsUtf8Bytes!; - ConverterBase = matchingProperty.ConverterBase; + ConverterBase = matchingProperty.EffectiveConverter; IgnoreDefaultValuesOnRead = matchingProperty.IgnoreDefaultValuesOnRead; NumberHandling = matchingProperty.EffectiveNumberHandling; MatchingPropertyCanBeNull = matchingProperty.PropertyTypeCanBeNull; 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 8c9236cbdd35b..ae19e452200a4 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 @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json.Reflection; @@ -13,57 +14,120 @@ namespace System.Text.Json.Serialization.Metadata /// Provides JSON serialization-related metadata about a property or field. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] - [EditorBrowsable(EditorBrowsableState.Never)] public abstract class JsonPropertyInfo { internal static readonly JsonPropertyInfo s_missingProperty = GetPropertyPlaceholder(); + internal JsonTypeInfo? ParentTypeInfo { get; private set; } private JsonTypeInfo? _jsonTypeInfo; internal ConverterStrategy ConverterStrategy; - internal abstract JsonConverter ConverterBase { get; set; } + /// + /// Converter resolved from PropertyType and not taking in consideration any custom attributes or custom settings. + /// - for reflection we store the original value since we need it in order to construct typed JsonPropertyInfo + /// - for source gen it remains null, we will initialize it only if someone used resolver to remove CustomConverter + /// + internal JsonConverter? DefaultConverterForType { get; set; } + + /// + /// Converter after applying CustomConverter (i.e. JsonConverterAttribute) + /// + internal abstract JsonConverter EffectiveConverter { get; set; } + + /// + /// Custom converter override at the property level, equivalent to JsonConverterAttribute annotation + /// + public JsonConverter? CustomConverter + { + get => _customConverter; + set + { + CheckMutable(); + _customConverter = value; + } + } + + private JsonConverter? _customConverter; + + /// + /// Getter delegate. Property cannot be serialized without it. + /// + public Func? Get + { + get => _untypedGet; + set => SetGetter(value); + } + + /// + /// Setter delegate. Property cannot be deserialized without it. + /// + public Action? Set + { + get => _untypedSet; + set => SetSetter(value); + } + + private protected Func? _untypedGet; + private protected Action? _untypedSet; + + private protected abstract void SetGetter(Delegate? getter); + private protected abstract void SetSetter(Delegate? setter); + + /// + /// Decides if property with given declaring object and property value should be serialized. + /// If not set it is equivalent to always returning true. + /// + public Func? ShouldSerialize + { + get => _shouldSerialize; + set + { + CheckMutable(); + _shouldSerialize = value; + // By default we will go through faster path (not using delegate) and use IgnoreCondition + // If users sets it explicitly we always go through delegate + IgnoreCondition = null; + IsIgnored = false; + _shouldSerializeIsExplicitlySet = true; + } + } + + private protected Func? _shouldSerialize; + private bool _shouldSerializeIsExplicitlySet; - internal JsonPropertyInfo() + internal JsonPropertyInfo(JsonTypeInfo? parentTypeInfo) { + // null parentTypeInfo means it's not tied yet + ParentTypeInfo = parentTypeInfo; } internal static JsonPropertyInfo GetPropertyPlaceholder() { - JsonPropertyInfo info = new JsonPropertyInfo(); + JsonPropertyInfo info = new JsonPropertyInfo(parentTypeInfo: null); Debug.Assert(!info.IsForTypeInfo); - Debug.Assert(!info.ShouldDeserialize); - Debug.Assert(!info.ShouldSerialize); + Debug.Assert(!info.CanDeserialize); + Debug.Assert(!info.CanSerialize); info.Name = string.Empty; return info; } - // Create a property that is ignored at run-time. - internal static JsonPropertyInfo CreateIgnoredPropertyPlaceholder( - MemberInfo memberInfo, - Type memberType, - bool isVirtual, - JsonSerializerOptions options) + /// + /// Type associated with JsonPropertyInfo + /// + public Type PropertyType { get; private protected set; } = null!; + + private protected void CheckMutable() { - JsonPropertyInfo jsonPropertyInfo = new JsonPropertyInfo(); - - jsonPropertyInfo.Options = options; - jsonPropertyInfo.MemberInfo = memberInfo; - jsonPropertyInfo.IsIgnored = true; - jsonPropertyInfo.PropertyType = memberType; - jsonPropertyInfo.IsVirtual = isVirtual; - jsonPropertyInfo.DeterminePropertyName(); - - Debug.Assert(!jsonPropertyInfo.ShouldDeserialize); - Debug.Assert(!jsonPropertyInfo.ShouldSerialize); - return jsonPropertyInfo; + if (_isConfigured) + { + ThrowHelper.ThrowInvalidOperationException_PropertyInfoImmutable(); + } } - internal Type PropertyType { get; set; } = null!; - private bool _isConfigured; internal void EnsureConfigured() @@ -80,6 +144,9 @@ internal void EnsureConfigured() internal virtual void Configure() { + Debug.Assert(ParentTypeInfo != null, "We should have ensured parent is assigned in JsonTypeInfo"); + DeclaringTypeNumberHandling = ParentTypeInfo.NumberHandling; + if (!IsForTypeInfo) { CacheNameAsUtf8BytesAndEscapedNameSection(); @@ -90,19 +157,28 @@ internal virtual void Configure() return; } + DetermineEffectiveConverter(); + ConverterStrategy = EffectiveConverter.ConverterStrategy; + if (IsForTypeInfo) { DetermineNumberHandlingForTypeInfo(); } else { - PropertyTypeCanBeNull = PropertyType.CanBeNull(); DetermineNumberHandlingForProperty(); - DetermineIgnoreCondition(IgnoreCondition); + + if (!IsIgnored) + { + DetermineIgnoreCondition(IgnoreCondition); + } + DetermineSerializationCapabilities(IgnoreCondition); } } + internal abstract void DetermineEffectiveConverter(); + internal void GetPolicies() { Debug.Assert(MemberInfo != null); @@ -161,7 +237,14 @@ internal void CacheNameAsUtf8BytesAndEscapedNameSection() internal void DetermineSerializationCapabilities(JsonIgnoreCondition? ignoreCondition) { - Debug.Assert(MemberType == MemberTypes.Property || MemberType == MemberTypes.Field); + if (IsIgnored) + { + CanSerialize = false; + CanDeserialize = false; + return; + } + + Debug.Assert(MemberType == MemberTypes.Property || MemberType == MemberTypes.Field || MemberType == default); if ((ConverterStrategy & (ConverterStrategy.Enumerable | ConverterStrategy.Dictionary)) == 0) { @@ -176,22 +259,22 @@ internal void DetermineSerializationCapabilities(JsonIgnoreCondition? ignoreCond : !Options.IgnoreReadOnlyFields); // We serialize if there is a getter + not ignoring readonly properties. - ShouldSerialize = HasGetter && (HasSetter || serializeReadOnlyProperty); + CanSerialize = HasGetter && (HasSetter || serializeReadOnlyProperty || _shouldSerializeIsExplicitlySet); // We deserialize if there is a setter. - ShouldDeserialize = HasSetter; + CanDeserialize = HasSetter; } else { if (HasGetter) { - Debug.Assert(ConverterBase != null); + Debug.Assert(EffectiveConverter != null); - ShouldSerialize = true; + CanSerialize = true; if (HasSetter) { - ShouldDeserialize = true; + CanDeserialize = true; } } } @@ -199,6 +282,23 @@ internal void DetermineSerializationCapabilities(JsonIgnoreCondition? ignoreCond internal void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition) { + if (_shouldSerializeIsExplicitlySet) + { + Debug.Assert(ignoreCondition == null); +#pragma warning disable SYSLIB0020 // JsonSerializerOptions.IgnoreNullValues is obsolete + if (Options.IgnoreNullValues) +#pragma warning restore SYSLIB0020 + { + Debug.Assert(Options.DefaultIgnoreCondition == JsonIgnoreCondition.Never); + if (PropertyTypeCanBeNull) + { + IgnoreDefaultValuesOnRead = true; + } + } + + return; + } + if (ignoreCondition != null) { // This is not true for CodeGen scenarios since we do not cache this as of yet. @@ -249,7 +349,7 @@ internal void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition) internal void DetermineNumberHandlingForTypeInfo() { - if (DeclaringTypeNumberHandling != null && DeclaringTypeNumberHandling != JsonNumberHandling.Strict && !ConverterBase.IsInternalConverter) + if (DeclaringTypeNumberHandling != null && DeclaringTypeNumberHandling != JsonNumberHandling.Strict && !EffectiveConverter.IsInternalConverter) { ThrowHelper.ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(this); } @@ -276,8 +376,8 @@ internal void DetermineNumberHandlingForProperty() if (numberHandlingIsApplicable) { - // Priority 1: Get handling from attribute on property/field, or its parent class type. - JsonNumberHandling? handling = NumberHandling ?? DeclaringTypeNumberHandling; + // Priority 1: Get handling from attribute on property/field, its parent class type or property type. + JsonNumberHandling? handling = NumberHandling ?? DeclaringTypeNumberHandling ?? JsonTypeInfo.NumberHandling; // Priority 2: Get handling from JsonSerializerOptions instance. if (!handling.HasValue && Options.NumberHandling != JsonNumberHandling.Strict) @@ -295,21 +395,21 @@ internal void DetermineNumberHandlingForProperty() private bool NumberHandingIsApplicable() { - if (ConverterBase.IsInternalConverterForNumberType) + if (EffectiveConverter.IsInternalConverterForNumberType) { return true; } Type potentialNumberType; - if (!ConverterBase.IsInternalConverter || + if (!EffectiveConverter.IsInternalConverter || ((ConverterStrategy.Enumerable | ConverterStrategy.Dictionary) & ConverterStrategy) == 0) { potentialNumberType = PropertyType; } else { - Debug.Assert(ConverterBase.ElementType != null); - potentialNumberType = ConverterBase.ElementType; + Debug.Assert(EffectiveConverter.ElementType != null); + potentialNumberType = EffectiveConverter.ElementType; } potentialNumberType = Nullable.GetUnderlyingType(potentialNumberType) ?? potentialNumberType; @@ -349,16 +449,16 @@ internal string GetDebugInfo(int indent = 0) sb.AppendLine($"{ind} NameAsUtf8.Length: {(NameAsUtf8Bytes?.Length ?? -1)},"); sb.AppendLine($"{ind} IsConfigured: {_isConfigured},"); sb.AppendLine($"{ind} IsIgnored: {IsIgnored},"); - sb.AppendLine($"{ind} ShouldSerialize: {ShouldSerialize},"); - sb.AppendLine($"{ind} ShouldDeserialize: {ShouldDeserialize},"); + sb.AppendLine($"{ind} CanSerialize: {CanSerialize},"); + sb.AppendLine($"{ind} CanDeserialize: {CanDeserialize},"); sb.AppendLine($"{ind}}}"); return sb.ToString(); } #endif - internal bool HasGetter { get; set; } - internal bool HasSetter { get; set; } + internal bool HasGetter => _untypedGet is not null; + internal bool HasSetter => _untypedSet is not null; internal abstract void Initialize( Type parentClassType, @@ -369,7 +469,8 @@ internal abstract void Initialize( JsonConverter converter, JsonIgnoreCondition? ignoreCondition, JsonSerializerOptions options, - JsonTypeInfo? jsonTypeInfo = null); + JsonTypeInfo? jsonTypeInfo = null, + bool isUserDefinedProperty = false); internal bool IgnoreDefaultValuesOnRead { get; private set; } internal bool IgnoreDefaultValuesOnWrite { get; private set; } @@ -385,12 +486,28 @@ internal abstract void Initialize( // 3) EscapedNameSection. The escaped verson of NameAsUtf8Bytes plus the wrapping quotes and a trailing colon. Used during serialization. /// - /// The unescaped name of the property. - /// Is either the actual CLR property name, + /// The name of the property. + /// It is either the actual .NET property name, /// the value specified in JsonPropertyNameAttribute, - /// or the value returned from PropertyNamingPolicy(clrPropertyName). + /// or the value returned from PropertyNamingPolicy. /// - internal string Name { get; set; } = null!; + public string Name + { + get => _name; + set + { + CheckMutable(); + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _name = value; + } + } + + private string _name = null!; /// /// Utf8 version of Name. @@ -402,7 +519,10 @@ internal abstract void Initialize( /// internal byte[] EscapedNameSection { get; set; } = null!; - internal JsonSerializerOptions Options { get; set; } = null!; // initialized in Init method + /// + /// Options associated with JsonPropertyInfo + /// + public JsonSerializerOptions Options { get; internal set; } = null!; // initialized in Init method /// /// The property order. @@ -441,7 +561,7 @@ internal bool ReadJsonAndAddExtensionProperty( { // Avoid a type reference to JsonObject and its converter to support trimming. Debug.Assert(propValue is Nodes.JsonObject); - ConverterBase.ReadElementAndSetProperty(propValue, state.Current.JsonPropertyNameAsString!, ref reader, Options, ref state); + EffectiveConverter.ReadElementAndSetProperty(propValue, state.Current.JsonPropertyNameAsString!, ref reader, Options, ref state); } return true; @@ -453,14 +573,14 @@ JsonConverter GetDictionaryValueConverter(Type dictionaryValueType) if (dictionaryValueInfo != null) { // Fast path when there is a generic type such as Dictionary<,>. - converter = dictionaryValueInfo.PropertyInfoForTypeInfo.ConverterBase; + converter = dictionaryValueInfo.Converter; } else { // Slower path for non-generic types that implement IDictionary<,>. // It is possible to cache this converter on JsonTypeInfo if we assume the property value // will always be the same type for all instances. - converter = Options.GetConverterInternal(dictionaryValueType); + converter = Options.GetConverterFromTypeInfo(dictionaryValueType); } Debug.Assert(converter != null); @@ -482,7 +602,7 @@ internal bool ReadJsonExtensionDataValue(ref ReadStack state, ref Utf8JsonReader return true; } - JsonConverter converter = (JsonConverter)Options.GetConverterInternal(typeof(JsonElement)); + JsonConverter converter = (JsonConverter)Options.GetConverterFromTypeInfo(typeof(JsonElement)); if (!converter.TryRead(ref reader, typeof(JsonElement), Options, ref state, out JsonElement jsonElement)) { // JsonElement is a struct that must be read in full. @@ -494,10 +614,23 @@ internal bool ReadJsonExtensionDataValue(ref ReadStack state, ref Utf8JsonReader return true; } + internal void EnsureChildOf(JsonTypeInfo parent) + { + if (ParentTypeInfo == null) + { + ParentTypeInfo = parent; + } + else if (ParentTypeInfo != parent) + { + ThrowHelper.ThrowInvalidOperationException_JsonPropertyInfoIsBoundToDifferentJsonTypeInfo(this); + } + } + internal Type DeclaringType { get; set; } = null!; internal MemberInfo? MemberInfo { get; set; } + [AllowNull] internal JsonTypeInfo JsonTypeInfo { get @@ -528,9 +661,9 @@ internal JsonTypeInfo JsonTypeInfo internal abstract void SetExtensionDictionaryAsObject(object obj, object? extensionDict); - internal bool ShouldSerialize { get; set; } + internal bool CanSerialize { get; set; } - internal bool ShouldDeserialize { get; set; } + internal bool CanDeserialize { get; set; } internal bool IsIgnored { get; set; } @@ -557,7 +690,17 @@ internal JsonTypeInfo JsonTypeInfo /// /// Number handling specific to this property, i.e. set by attribute /// - internal JsonNumberHandling? NumberHandling { get; set; } + public JsonNumberHandling? NumberHandling + { + get => _numberHandling; + set + { + CheckMutable(); + _numberHandling = value; + } + } + + private JsonNumberHandling? _numberHandling; /// /// Number handling after considering options and declaring type number handling 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 51bf1f0a343bf..1cd99983fa73c 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 @@ -27,16 +27,79 @@ internal sealed class JsonPropertyInfo : JsonPropertyInfo // the property's type, we track that and whether the property type can be null. private bool _propertyTypeEqualsTypeToConvert; - internal Func? Get { get; set; } + private Func? _typedGet; + private Action? _typedSet; - internal Action? Set { get; set; } + internal JsonPropertyInfo(JsonTypeInfo? parentTypeInfo) : base(parentTypeInfo) + { + } + + internal new Func? Get + { + get => _typedGet; + set => SetGetter(value); + } + + internal new Action? Set + { + get => _typedSet; + set => SetSetter(value); + } + + private protected override void SetGetter(Delegate? getter) + { + Debug.Assert(getter is null or Func or Func); + + CheckMutable(); + + if (getter is null) + { + _typedGet = null; + _untypedGet = null; + } + else if (getter is Func typedGetter) + { + _typedGet = typedGetter; + _untypedGet = getter is Func untypedGet ? untypedGet : obj => typedGetter(obj); + } + else + { + Func untypedGet = (Func)getter; + _typedGet = (obj => (T)untypedGet(obj)!); + _untypedGet = untypedGet; + } + } + + private protected override void SetSetter(Delegate? setter) + { + Debug.Assert(setter is null or Action or Action); + + CheckMutable(); + + if (setter is null) + { + _typedSet = null; + _untypedSet = null; + } + else if (setter is Action typedSetter) + { + _typedSet = typedSetter; + _untypedSet = setter is Action untypedSet ? untypedSet : (obj, value) => typedSetter(obj, (T)value!); + } + else + { + Action untypedSet = (Action)setter; + _typedSet = ((obj, value) => untypedSet(obj, (T)value!)); + _untypedSet = untypedSet; + } + } internal override object? DefaultValue => default(T); - public JsonConverter Converter { get; internal set; } = null!; + internal JsonConverter TypedEffectiveConverter { get; private set; } = null!; internal override void Initialize( - Type parentClassType, + Type declaringType, Type declaredPropertyType, ConverterStrategy converterStrategy, MemberInfo? memberInfo, @@ -44,83 +107,89 @@ internal override void Initialize( JsonConverter converter, JsonIgnoreCondition? ignoreCondition, JsonSerializerOptions options, - JsonTypeInfo? jsonTypeInfo = null) + JsonTypeInfo? jsonTypeInfo = null, + bool isUserDefinedProperty = false) { Debug.Assert(converter != null); PropertyType = declaredPropertyType; + _propertyTypeEqualsTypeToConvert = typeof(T) == declaredPropertyType; + PropertyTypeCanBeNull = PropertyType.CanBeNull(); ConverterStrategy = converterStrategy; if (jsonTypeInfo != null) { JsonTypeInfo = jsonTypeInfo; } - ConverterBase = converter; + DefaultConverterForType = converter; Options = options; - DeclaringType = parentClassType; + DeclaringType = declaringType; MemberInfo = memberInfo; IsVirtual = isVirtual; IgnoreCondition = ignoreCondition; + IsIgnored = ignoreCondition == JsonIgnoreCondition.Always; if (memberInfo != null) { - switch (memberInfo) + if (!IsIgnored) { - case PropertyInfo propertyInfo: - { - bool useNonPublicAccessors = GetAttribute(propertyInfo) != null; - - MethodInfo? getMethod = propertyInfo.GetMethod; - if (getMethod != null && (getMethod.IsPublic || useNonPublicAccessors)) + switch (memberInfo) + { + case PropertyInfo propertyInfo: { - HasGetter = true; - Get = options.MemberAccessorStrategy.CreatePropertyGetter(propertyInfo); + bool useNonPublicAccessors = GetAttribute(propertyInfo) != null; + + MethodInfo? getMethod = propertyInfo.GetMethod; + if (getMethod != null && (getMethod.IsPublic || useNonPublicAccessors)) + { + Get = options.MemberAccessorStrategy.CreatePropertyGetter(propertyInfo); + } + + MethodInfo? setMethod = propertyInfo.SetMethod; + if (setMethod != null && (setMethod.IsPublic || useNonPublicAccessors)) + { + Set = options.MemberAccessorStrategy.CreatePropertySetter(propertyInfo); + } + + MemberType = MemberTypes.Property; + + break; } - MethodInfo? setMethod = propertyInfo.SetMethod; - if (setMethod != null && (setMethod.IsPublic || useNonPublicAccessors)) + case FieldInfo fieldInfo: { - HasSetter = true; - Set = options.MemberAccessorStrategy.CreatePropertySetter(propertyInfo); - } + Debug.Assert(fieldInfo.IsPublic); - MemberType = MemberTypes.Property; + Get = options.MemberAccessorStrategy.CreateFieldGetter(fieldInfo); - break; - } + if (!fieldInfo.IsInitOnly) + { + Set = options.MemberAccessorStrategy.CreateFieldSetter(fieldInfo); + } - case FieldInfo fieldInfo: - { - Debug.Assert(fieldInfo.IsPublic); + MemberType = MemberTypes.Field; - HasGetter = true; - Get = options.MemberAccessorStrategy.CreateFieldGetter(fieldInfo); + break; + } - if (!fieldInfo.IsInitOnly) + default: { - HasSetter = true; - Set = options.MemberAccessorStrategy.CreateFieldSetter(fieldInfo); + Debug.Fail($"Invalid memberInfo type: {memberInfo.GetType().FullName}"); + break; } - - MemberType = MemberTypes.Field; - - break; - } - - default: - { - Debug.Fail($"Invalid memberInfo type: {memberInfo.GetType().FullName}"); - break; - } + } } GetPolicies(); } - else + else if (!isUserDefinedProperty) { IsForTypeInfo = true; - HasGetter = true; - HasSetter = true; + } + + if (IgnoreCondition != null) + { + _shouldSerialize = GetShouldSerializeForIgnoreCondition(IgnoreCondition.Value); } } @@ -129,62 +198,62 @@ internal void InitializeForSourceGen(JsonSerializerOptions options, JsonProperty Options = options; ClrName = propertyInfo.PropertyName; + string name; + // Property name settings. if (propertyInfo.JsonPropertyName != null) { - Name = propertyInfo.JsonPropertyName; + name = propertyInfo.JsonPropertyName; } else if (options.PropertyNamingPolicy == null) { - Name = ClrName; + name = ClrName; } else { - Name = options.PropertyNamingPolicy.ConvertName(ClrName); - if (Name == null) - { - ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameNull(DeclaringType, this); - } + name = options.PropertyNamingPolicy.ConvertName(ClrName); } + // Compat: We need to do validation before we assign Name so that we get InvalidOperationException rather than ArgumentNullException + if (name == null) + { + ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameNull(DeclaringType, this); + } + + Name = name; + SrcGen_IsPublic = propertyInfo.IsPublic; SrcGen_HasJsonInclude = propertyInfo.HasJsonInclude; SrcGen_IsExtensionData = propertyInfo.IsExtensionData; PropertyType = typeof(T); + _propertyTypeEqualsTypeToConvert = true; + PropertyTypeCanBeNull = PropertyType.CanBeNull(); - JsonTypeInfo propertyTypeInfo = propertyInfo.PropertyTypeInfo; + JsonTypeInfo? propertyTypeInfo = propertyInfo.PropertyTypeInfo; Type declaringType = propertyInfo.DeclaringType; - JsonConverter? converter = propertyInfo.Converter; - if (converter == null) - { - converter = propertyTypeInfo.PropertyInfoForTypeInfo.ConverterBase as JsonConverter; - if (converter == null) - { - throw new InvalidOperationException(SR.Format(SR.ConverterForPropertyMustBeValid, declaringType, ClrName, typeof(T))); - } - } + JsonConverter? typedCustomConverter = propertyInfo.Converter; + CustomConverter = typedCustomConverter; - ConverterBase = converter; + JsonConverter? typedNonCustomConverter = propertyTypeInfo?.Converter as JsonConverter; + DefaultConverterForType = typedNonCustomConverter; - if (propertyInfo.IgnoreCondition == JsonIgnoreCondition.Always) - { - IsIgnored = true; - Debug.Assert(!ShouldSerialize); - Debug.Assert(!ShouldDeserialize); - } - else + IsIgnored = propertyInfo.IgnoreCondition == JsonIgnoreCondition.Always; + if (!IsIgnored) { Get = propertyInfo.Getter!; Set = propertyInfo.Setter; - HasGetter = Get != null; - HasSetter = Set != null; - JsonTypeInfo = propertyTypeInfo; - DeclaringType = declaringType; - IgnoreCondition = propertyInfo.IgnoreCondition; - MemberType = propertyInfo.IsProperty ? MemberTypes.Property : MemberTypes.Field; - ConverterStrategy = Converter!.ConverterStrategy; - NumberHandling = propertyInfo.NumberHandling; + } + + JsonTypeInfo = propertyTypeInfo; + DeclaringType = declaringType; + IgnoreCondition = propertyInfo.IgnoreCondition; + MemberType = propertyInfo.IsProperty ? MemberTypes.Property : MemberTypes.Field; + NumberHandling = propertyInfo.NumberHandling; + + if (IgnoreCondition != null) + { + _shouldSerialize = GetShouldSerializeForIgnoreCondition(IgnoreCondition.Value); } } @@ -194,21 +263,39 @@ internal override void Configure() if (!IsForTypeInfo && !IsIgnored) { - _converterIsExternalAndPolymorphic = !ConverterBase.IsInternalConverter && PropertyType != ConverterBase.TypeToConvert; - _propertyTypeEqualsTypeToConvert = typeof(T) == PropertyType; + _converterIsExternalAndPolymorphic = !EffectiveConverter.IsInternalConverter && PropertyType != EffectiveConverter.TypeToConvert; } } - internal override JsonConverter ConverterBase + internal override void DetermineEffectiveConverter() + { + JsonConverter? customConverter = CustomConverter; + if (customConverter != null) + { + customConverter = Options.ExpandFactoryConverter(customConverter, PropertyType); + JsonSerializerOptions.CheckConverterNullabilityIsSameAsPropertyType(customConverter, PropertyType); + } + + JsonConverter effectiveConverter = customConverter ?? DefaultConverterForType ?? Options.GetConverterFromTypeInfo(PropertyType); + if (effectiveConverter.TypeToConvert == PropertyType) + { + EffectiveConverter = effectiveConverter; + } + else + { + EffectiveConverter = effectiveConverter.CreateCastingConverter(); + } + } + + internal override JsonConverter EffectiveConverter { get { - return Converter; + return TypedEffectiveConverter; } set { - Debug.Assert(value is JsonConverter); - Converter = (JsonConverter)value; + TypedEffectiveConverter = (JsonConverter)value; } } @@ -231,7 +318,7 @@ internal override bool GetMemberAndWriteJson(object obj, ref WriteStack state, U #if NETCOREAPP !typeof(T).IsValueType && // treated as a constant by recent versions of the JIT. #else - !Converter.IsValueType && + !TypedEffectiveConverter.IsValueType && #endif Options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.IgnoreCycles && value is not null && @@ -276,11 +363,18 @@ value is not null && } } + if (ShouldSerialize?.Invoke(obj, value) == false) + { + // We return true here. + // False means that there is not enough data. + return true; + } + if (value == null) { Debug.Assert(PropertyTypeCanBeNull); - if (Converter.HandleNullOnWrite) + if (TypedEffectiveConverter.HandleNullOnWrite) { if (state.Current.PropertyState < StackFramePropertyState.Name) { @@ -289,10 +383,10 @@ value is not null && } int originalDepth = writer.CurrentDepth; - Converter.Write(writer, value, Options); + TypedEffectiveConverter.Write(writer, value, Options); if (originalDepth != writer.CurrentDepth) { - ThrowHelper.ThrowJsonException_SerializationConverterWrite(Converter); + ThrowHelper.ThrowJsonException_SerializationConverterWrite(TypedEffectiveConverter); } } else @@ -310,7 +404,7 @@ value is not null && writer.WritePropertyNameSection(EscapedNameSection); } - return Converter.TryWrite(writer, value, Options, ref state); + return TypedEffectiveConverter.TryWrite(writer, value, Options, ref state); } } @@ -319,13 +413,20 @@ internal override bool GetMemberAndWriteJsonExtensionData(object obj, ref WriteS bool success; T value = Get!(obj); + if (ShouldSerialize?.Invoke(obj, value) == false) + { + // We return true here. + // False means that there is not enough data. + return true; + } + if (value == null) { success = true; } else { - success = Converter.TryWriteDataExtensionProperty(writer, value, Options, ref state); + success = TypedEffectiveConverter.TryWriteDataExtensionProperty(writer, value, Options, ref state); } return success; @@ -336,11 +437,11 @@ internal override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref bool success; bool isNullToken = reader.TokenType == JsonTokenType.Null; - if (isNullToken && !Converter.HandleNullOnRead && !state.IsContinuation) + if (isNullToken && !TypedEffectiveConverter.HandleNullOnRead && !state.IsContinuation) { if (!PropertyTypeCanBeNull) { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Converter.TypeToConvert); + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypedEffectiveConverter.TypeToConvert); } Debug.Assert(default(T) == null); @@ -353,7 +454,7 @@ internal override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref success = true; } - else if (Converter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) + else if (TypedEffectiveConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // CanUseDirectReadOrWrite == false when using streams Debug.Assert(!state.IsContinuation); @@ -361,7 +462,7 @@ internal override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref if (!isNullToken || !IgnoreDefaultValuesOnRead || !PropertyTypeCanBeNull) { // Optimize for internal converters by avoiding the extra call to TryRead. - T? fastValue = Converter.Read(ref reader, PropertyType, Options); + T? fastValue = TypedEffectiveConverter.Read(ref reader, PropertyType, Options); Set!(obj, fastValue!); } @@ -372,7 +473,7 @@ internal override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref success = true; if (!isNullToken || !IgnoreDefaultValuesOnRead || !PropertyTypeCanBeNull || state.IsContinuation) { - success = Converter.TryRead(ref reader, PropertyType, Options, ref state, out T? value); + success = TypedEffectiveConverter.TryRead(ref reader, PropertyType, Options, ref state, out T? value); if (success) { #if !DEBUG @@ -405,11 +506,11 @@ internal override bool ReadJsonAsObject(ref ReadStack state, ref Utf8JsonReader { bool success; bool isNullToken = reader.TokenType == JsonTokenType.Null; - if (isNullToken && !Converter.HandleNullOnRead && !state.IsContinuation) + if (isNullToken && !TypedEffectiveConverter.HandleNullOnRead && !state.IsContinuation) { if (!PropertyTypeCanBeNull) { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Converter.TypeToConvert); + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypedEffectiveConverter.TypeToConvert); } value = default(T); @@ -418,17 +519,17 @@ internal override bool ReadJsonAsObject(ref ReadStack state, ref Utf8JsonReader else { // Optimize for internal converters by avoiding the extra call to TryRead. - if (Converter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) + if (TypedEffectiveConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { // CanUseDirectReadOrWrite == false when using streams Debug.Assert(!state.IsContinuation); - value = Converter.Read(ref reader, PropertyType, Options); + value = TypedEffectiveConverter.Read(ref reader, PropertyType, Options); success = true; } else { - success = Converter.TryRead(ref reader, PropertyType, Options, ref state, out T? typedValue); + success = TypedEffectiveConverter.TryRead(ref reader, PropertyType, Options, ref state, out T? typedValue); value = typedValue; } } @@ -442,5 +543,59 @@ internal override void SetExtensionDictionaryAsObject(object obj, object? extens T typedValue = (T)extensionDict!; Set!(obj, typedValue); } + + private Func GetShouldSerializeForIgnoreCondition(JsonIgnoreCondition ignoreCondition) + { + switch (ignoreCondition) + { + case JsonIgnoreCondition.Always: return ShouldSerializeIgnoreConditionAlways; + case JsonIgnoreCondition.Never: return ShouldSerializeIgnoreConditionNever; + case JsonIgnoreCondition.WhenWritingNull: + if (!PropertyTypeCanBeNull) + { + return ShouldSerializeIgnoreConditionNever; + } + + goto case JsonIgnoreCondition.WhenWritingDefault; + case JsonIgnoreCondition.WhenWritingDefault: + { + if (_propertyTypeEqualsTypeToConvert) + { + return ShouldSerializeIgnoreConditionWhenWritingDefaultPropertyTypeEqualsTypeToConvert; + } + else + { + return ShouldSerializeIgnoreConditionWhenWritingDefaultPropertyTypeNotEqualsTypeToConvert; + } + } + default: + Debug.Fail($"Unknown value of JsonIgnoreCondition '{ignoreCondition}'"); + return null!; + } + } + + internal static bool ShouldSerializeIgnoreConditionAlways(object obj, object? value) => false; + internal static bool ShouldSerializeIgnoreConditionNever(object obj, object? value) => true; + internal static bool ShouldSerializeIgnoreConditionWhenWritingDefaultPropertyTypeEqualsTypeToConvert(object obj, object? value) + { + if (value == null) + { + return false; + } + + T typedValue = (T)value; + return !EqualityComparer.Default.Equals(default, typedValue); + } + + internal bool ShouldSerializeIgnoreConditionWhenWritingDefaultPropertyTypeNotEqualsTypeToConvert(object obj, object? value) + { + if (value == null) + { + return false; + } + + Debug.Assert(JsonTypeInfo.Type == PropertyType); + return !JsonTypeInfo.DefaultValueHolder.IsDefaultValue(value); + } } } 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 5688406084063..7b1202a7886ca 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 @@ -10,7 +10,7 @@ namespace System.Text.Json.Serialization.Metadata { - public partial class JsonTypeInfo + public abstract partial class JsonTypeInfo { /// /// Cached typeof(object). It is faster to cache this than to call typeof(object) multiple times. @@ -51,7 +51,7 @@ public partial class JsonTypeInfo internal Func? CtorParamInitFunc; - internal static JsonPropertyInfo CreateProperty( + internal JsonPropertyInfo CreateProperty( Type declaredPropertyType, MemberInfo? memberInfo, Type parentClassType, @@ -59,10 +59,12 @@ internal static JsonPropertyInfo CreateProperty( JsonConverter converter, JsonSerializerOptions options, JsonIgnoreCondition? ignoreCondition = null, - JsonTypeInfo? jsonTypeInfo = null) + JsonTypeInfo? jsonTypeInfo = null, + JsonConverter? customConverter = null, + bool isUserDefinedProperty = false) { // Create the JsonPropertyInfo instance. - JsonPropertyInfo jsonPropertyInfo = converter.CreateJsonPropertyInfo(); + JsonPropertyInfo jsonPropertyInfo = converter.CreateJsonPropertyInfo(parentTypeInfo: this); jsonPropertyInfo.Initialize( parentClassType, @@ -73,7 +75,10 @@ internal static JsonPropertyInfo CreateProperty( converter, ignoreCondition, options, - jsonTypeInfo); + jsonTypeInfo, + isUserDefinedProperty: isUserDefinedProperty); + + jsonPropertyInfo.CustomConverter = customConverter; return jsonPropertyInfo; } @@ -82,7 +87,7 @@ internal static JsonPropertyInfo CreateProperty( /// Create a for a given Type. /// See . /// - private static JsonPropertyInfo CreatePropertyInfoForTypeInfo( + private JsonPropertyInfo CreatePropertyInfoForTypeInfo( Type declaredPropertyType, JsonConverter converter, JsonSerializerOptions options, 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 3d8932b19fd05..055c94c8334b9 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 @@ -14,18 +14,58 @@ namespace System.Text.Json.Serialization.Metadata /// /// Provides JSON serialization-related metadata about a type. /// - /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. [DebuggerDisplay("{DebuggerDisplay,nq}")] - [EditorBrowsable(EditorBrowsableState.Never)] - public partial class JsonTypeInfo + public abstract partial class JsonTypeInfo { internal const string JsonObjectTypeName = "System.Text.Json.Nodes.JsonObject"; - internal delegate object? ConstructorDelegate(); - internal delegate T ParameterizedConstructorDelegate(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3); - internal ConstructorDelegate? CreateObject { get; set; } + private JsonPropertyInfoDictionaryValueList? _properties; + + /// + /// Object constructor. If set to null type is not deserializable. + /// + public Func? CreateObject + { + get => _createObject; + set + { + SetCreateObject(value); + } + } + + private protected abstract void SetCreateObject(Delegate? createObject); + private protected Func? _createObject; + + internal Func? CreateObjectForExtensionDataProperty { get; private protected set; } + + /// + /// Gets JsonPropertyInfo list. Only applicable when Kind is Object. + /// + public IList Properties + { + get + { + if (_properties != null) + { + return _properties; + } + + if (Kind == JsonTypeInfoKind.Object) + { + // We need to ensure SourceGen had a chance to add properties + LateAddProperties(); + } + + PropertyCache ??= CreatePropertyCache(capacity: 0); + + bool isReadOnly = _isConfigured || Kind != JsonTypeInfoKind.Object; + _properties = new JsonPropertyInfoDictionaryValueList(PropertyCache, this, isReadOnly); + + return _properties; + } + } internal object? CreateObjectWithArgs { get; set; } @@ -51,7 +91,7 @@ internal void ValidateCanBeUsedForDeserialization() { if (ThrowOnDeserialize) { - ThrowHelper.ThrowInvalidOperationException_NoMetadataForTypeProperties(Options.JsonSerializerContext, Type); + ThrowHelper.ThrowInvalidOperationException_NoMetadataForTypeProperties(Options.SerializerContext, Type); } } @@ -134,9 +174,30 @@ internal JsonTypeInfo? KeyTypeInfo internal Type? KeyType { get; set; } - internal JsonSerializerOptions Options { get; set; } + /// + /// Options associated with JsonTypeInfo + /// + public JsonSerializerOptions Options { get; private set; } - internal Type Type { get; private set; } + /// + /// Type associated with JsonTypeInfo + /// + public Type Type { get; private set; } + + /// + /// Converter associated with the type for the given options instance + /// + public JsonConverter Converter + // For JsonTypeInfo CustomConverter is always null + // while NonCustomConverter always contains final converter. + // This property can be used before JsonTypeInfo is configured (especially in SourceGen case) + // therefore it's safer to return NonCustomConverter rather than EffectiveConverter. + => PropertyInfoForTypeInfo.DefaultConverterForType!; + + /// + /// Determines the kind of contract metadata current JsonTypeInfo instance is customizing + /// + public JsonTypeInfoKind Kind { get; private set; } /// /// The JsonPropertyInfo for this JsonTypeInfo. It is used to obtain the converter for the TypeInfo. @@ -162,7 +223,20 @@ internal JsonTypeInfo? KeyTypeInfo internal DefaultValueHolder DefaultValueHolder => _defaultValueHolder ??= DefaultValueHolder.CreateHolder(Type); private DefaultValueHolder? _defaultValueHolder; - internal JsonNumberHandling? NumberHandling { get; set; } + /// + /// Type specific value overriding JsonSerializerOptions NumberHandling. For DefaultJsonTypeInfoResolver it is equivalent to JsonNumberHandlingAttribute value. + /// + public JsonNumberHandling? NumberHandling + { + get => _numberHandling; + set + { + CheckMutable(); + _numberHandling = value; + } + } + + private JsonNumberHandling? _numberHandling; internal JsonTypeInfo(Type type, JsonConverter converter, JsonSerializerOptions options) { @@ -192,9 +266,19 @@ internal JsonTypeInfo(Type type, JsonConverter converter, JsonSerializerOptions Debug.Fail($"Unexpected class type: {PropertyInfoForTypeInfo.ConverterStrategy}"); throw new InvalidOperationException(); } + + Kind = GetTypeInfoKind(type, PropertyInfoForTypeInfo.ConverterStrategy); + } + + private protected void CheckMutable() + { + if (_isConfigured) + { + ThrowHelper.ThrowInvalidOperationException_TypeInfoImmutable(); + } } - private volatile bool _isConfigured; + private protected volatile bool _isConfigured; private readonly object _configureLock = new object(); internal void EnsureConfigured() @@ -216,34 +300,62 @@ internal void EnsureConfigured() internal virtual void Configure() { Debug.Assert(Monitor.IsEntered(_configureLock), "Configure called directly, use EnsureConfigured which locks this method"); - JsonConverter converter = PropertyInfoForTypeInfo.ConverterBase; - Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == PropertyInfoForTypeInfo.ConverterBase.ConverterStrategy, - $"ConverterStrategy from PropertyInfoForTypeInfo.ConverterStrategy ({PropertyInfoForTypeInfo.ConverterStrategy}) does not match converter's ({PropertyInfoForTypeInfo.ConverterBase.ConverterStrategy})"); - converter.ConfigureJsonTypeInfo(this, Options); - PropertyInfoForTypeInfo.DeclaringTypeNumberHandling = NumberHandling; + if (!Options.IsInitializedForMetadataGeneration) + { + Options.InitializeForMetadataGeneration(); + } + + PropertyInfoForTypeInfo.EnsureChildOf(this); PropertyInfoForTypeInfo.EnsureConfigured(); - // Source gen currently when initializes properties - // also assigns JsonPropertyInfo's JsonTypeInfo which causes SO if there are any - // cycles in the object graph. For that reason properties cannot be added immediately. - // This is a no-op for ReflectionJsonTypeInfo - LateAddProperties(); + JsonConverter converter = Converter; + Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == Converter.ConverterStrategy, + $"ConverterStrategy from PropertyInfoForTypeInfo.ConverterStrategy ({PropertyInfoForTypeInfo.ConverterStrategy}) does not match converter's ({Converter.ConverterStrategy})"); + + converter.ConfigureJsonTypeInfo(this, Options); + + if (_properties != null) + { + // If user tried to access Properties for something else than JsonTypeInfoKind.Object + // Properties will already be read-only + if (!_properties.IsReadOnly) + { + _properties.FinishEditingAndMakeReadOnly(Type); + } + } + else + { + // Resolver didn't modify properties + + // Source gen currently when initializes properties + // also assigns JsonPropertyInfo's JsonTypeInfo which causes SO if there are any + // cycles in the object graph. For that reason properties cannot be added immediately. + // This is a no-op for ReflectionJsonTypeInfo + LateAddProperties(); + } - DataExtensionProperty?.EnsureConfigured(); + if (DataExtensionProperty != null) + { + DataExtensionProperty.EnsureChildOf(this); + DataExtensionProperty.EnsureConfigured(); + } - if (converter.ConverterStrategy == ConverterStrategy.Object && PropertyCache != null) + if (converter.ConverterStrategy == ConverterStrategy.Object) { + PropertyCache ??= CreatePropertyCache(capacity: 0); + foreach (var jsonPropertyInfoKv in PropertyCache.List) { JsonPropertyInfo jsonPropertyInfo = jsonPropertyInfoKv.Value!; - jsonPropertyInfo.DeclaringTypeNumberHandling = NumberHandling; + + jsonPropertyInfo.EnsureChildOf(this); jsonPropertyInfo.EnsureConfigured(); } if (converter.ConstructorIsParameterized) { - InitializeConstructorParameters(GetParameterInfoValues(), sourceGenMode: Options.JsonSerializerContext != null); + InitializeConstructorParameters(GetParameterInfoValues(), sourceGenMode: Options.SerializerContext != null); } } } @@ -290,16 +402,72 @@ internal string GetDebugInfo() internal virtual void LateAddProperties() { } - internal virtual JsonParameterInfoValues[] GetParameterInfoValues() + /// + /// Creates JsonTypeInfo + /// + /// Type for which JsonTypeInfo stores metadata for + /// Options associated with JsonTypeInfo + /// JsonTypeInfo instance + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use generic overload or System.Text.Json source generation for native AOT applications.")] + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use generic overload or System.Text.Json source generation for native AOT applications.")] + public static JsonTypeInfo CreateJsonTypeInfo(JsonSerializerOptions options) + { + return new CustomJsonTypeInfo(options); + } + + private static MethodInfo? s_createJsonTypeInfo; + + /// + /// Creates JsonTypeInfo + /// + /// Type for which JsonTypeInfo stores metadata for + /// Options associated with JsonTypeInfo + /// JsonTypeInfo instance + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use generic overload or System.Text.Json source generation for native AOT applications.")] + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use generic overload or System.Text.Json source generation for native AOT applications.")] + public static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) { - // If JsonTypeInfo becomes abstract this should be abstract as well - Debug.Fail("This should never be called."); - return null!; + s_createJsonTypeInfo ??= typeof(JsonTypeInfo).GetMethod(nameof(CreateJsonTypeInfo), new Type[] { typeof(JsonSerializerOptions) })!; + return (JsonTypeInfo)s_createJsonTypeInfo.MakeGenericMethod(type) + .Invoke(null, new object[] { options })!; } + /// + /// Creates JsonPropertyInfo + /// + /// Type of the property + /// Name of the property + /// JsonPropertyInfo instance + public JsonPropertyInfo CreateJsonPropertyInfo(Type propertyType, string name) + { + ValidateType(propertyType, Type, null, Options); + + JsonConverter converter = GetConverter(propertyType, + parentClassType: null, + memberInfo: null, + options: Options, + out _); + + JsonPropertyInfo propertyInfo = CreateProperty( + declaredPropertyType: propertyType, + memberInfo: null, + parentClassType: Type, + isVirtual: false, + converter: converter, + options: Options, + isUserDefinedProperty: true); + + propertyInfo.Name = name; + + return propertyInfo; + } + + internal abstract JsonParameterInfoValues[] GetParameterInfoValues(); + internal void CacheMember(JsonPropertyInfo jsonPropertyInfo, JsonPropertyDictionary? propertyCache, ref Dictionary? ignoredMembers) { - string memberName = jsonPropertyInfo.ClrName!; + Debug.Assert(jsonPropertyInfo.ClrName != null, "ClrName can be null in custom JsonPropertyInfo instances and should never be passed in this method"); + string memberName = jsonPropertyInfo.ClrName; // The JsonPropertyNameAttribute or naming policy resulted in a collision. if (!propertyCache!.TryAdd(jsonPropertyInfo.Name, jsonPropertyInfo)) @@ -322,7 +490,7 @@ internal void CacheMember(JsonPropertyInfo jsonPropertyInfo, JsonPropertyDiction 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); + ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, jsonPropertyInfo.Name); } // Ignore the current property. } @@ -383,7 +551,7 @@ internal void InitializeConstructorParameters(JsonParameterInfoValues[] jsonPara foreach (KeyValuePair kvp in PropertyCache.List) { JsonPropertyInfo jsonProperty = kvp.Value!; - string propertyName = jsonProperty.ClrName!; + string propertyName = jsonProperty.ClrName ?? jsonProperty.Name; ParameterLookupKey key = new(propertyName, jsonProperty.PropertyType); ParameterLookupValue value = new(jsonProperty); @@ -422,7 +590,8 @@ internal void InitializeConstructorParameters(JsonParameterInfoValues[] jsonPara else if (DataExtensionProperty != null && StringComparer.OrdinalIgnoreCase.Equals(paramToCheck.Name, DataExtensionProperty.Name)) { - ThrowHelper.ThrowInvalidOperationException_ExtensionDataCannotBindToCtorParam(DataExtensionProperty); + Debug.Assert(DataExtensionProperty.ClrName != null, "Custom property info cannot be data extension property"); + ThrowHelper.ThrowInvalidOperationException_ExtensionDataCannotBindToCtorParam(DataExtensionProperty.ClrName, DataExtensionProperty); } } @@ -440,7 +609,7 @@ internal static void ValidateType(Type type, Type? parentClassType, MemberInfo? internal static bool IsInvalidForSerialization(Type type) { - return type.IsPointer || IsByRefLike(type) || type.ContainsGenericParameters; + return type.IsPointer || type.IsByRef || IsByRefLike(type) || type.ContainsGenericParameters; } private static bool IsByRefLike(Type type) @@ -503,7 +672,43 @@ internal bool IsValidDataExtensionProperty(JsonPropertyInfo jsonPropertyInfo) // Avoid a reference to typeof(JsonNode) to support trimming. (memberType.FullName == JsonObjectTypeName && ReferenceEquals(memberType.Assembly, GetType().Assembly)); - return typeIsValid && Options.GetConverterInternal(memberType) != null; + return typeIsValid && Options.GetConverterFromTypeInfo(memberType) != null; + } + + internal JsonPropertyDictionary CreatePropertyCache(int capacity) + { + return new JsonPropertyDictionary(Options.PropertyNameCaseInsensitive, capacity); + } + + // This method gets the runtime information for a given type or property. + // The runtime information consists of the following: + // - class type, + // - element type (if the type is a collection), + // - the converter (either native or custom), if one exists. + internal static JsonConverter GetConverter( + Type type, + Type? parentClassType, + MemberInfo? memberInfo, + JsonSerializerOptions options, + out JsonConverter? customConverter) + { + Debug.Assert(type != null); + Debug.Assert(!IsInvalidForSerialization(type), $"Type `{type.FullName}` should already be validated."); + customConverter = parentClassType != null ? options.GetCustomConverterFromMember(parentClassType, type, memberInfo) : null; + return options.GetConverterForType(type); + } + + internal static JsonConverter GetEffectiveConverter( + Type type, + Type? parentClassType, + MemberInfo? memberInfo, + JsonSerializerOptions options) + { + JsonConverter converter = GetConverter(type, parentClassType, memberInfo, options, out JsonConverter? customConverter); + + customConverter = options.ExpandFactoryConverter(customConverter, type); + + return customConverter ?? converter; } private static JsonParameterInfo CreateConstructorParameter( @@ -517,7 +722,7 @@ private static JsonParameterInfo CreateConstructorParameter( return JsonParameterInfo.CreateIgnoredParameterPlaceholder(parameterInfo, jsonPropertyInfo, sourceGenMode); } - JsonConverter converter = jsonPropertyInfo.ConverterBase; + JsonConverter converter = jsonPropertyInfo.EffectiveConverter; JsonParameterInfo jsonParameterInfo = converter.CreateJsonParameterInfo(); jsonParameterInfo.Initialize(parameterInfo, jsonPropertyInfo, options); @@ -525,6 +730,23 @@ private static JsonParameterInfo CreateConstructorParameter( return jsonParameterInfo; } + private static JsonTypeInfoKind GetTypeInfoKind(Type type, ConverterStrategy converterStrategy) + { + // System.Object is semi-polimorphic and will not respect Properties + if (type == typeof(object)) + { + return JsonTypeInfoKind.None; + } + + return converterStrategy switch + { + ConverterStrategy.Object => JsonTypeInfoKind.Object, + ConverterStrategy.Enumerable => JsonTypeInfoKind.Enumerable, + ConverterStrategy.Dictionary => JsonTypeInfoKind.Dictionary, + _ => JsonTypeInfoKind.None + }; + } + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"ConverterStrategy.{PropertyInfoForTypeInfo.ConverterStrategy}, {Type.Name}"; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoKind.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoKind.cs new file mode 100644 index 0000000000000..200773d83218f --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoKind.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. + +namespace System.Text.Json.Serialization.Metadata +{ + /// + /// Determines the kind of contract metadata a given JsonTypeInfo instance is customizing + /// + public enum JsonTypeInfoKind + { + /// + /// Type is either a primitive value or uses a custom converter. JsonTypeInfo metadata does not apply here. + /// + None = 0, + /// + /// Type is serialized as object with properties + /// + Object = 1, + /// + /// Type is serialized as a collection with elements + /// + Enumerable = 2, + /// + /// Type is serialized as a dictionary with key/value pair entries + /// + Dictionary = 3 + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs index f5e3e341bbceb..0a92289aac198 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs @@ -10,11 +10,61 @@ namespace System.Text.Json.Serialization.Metadata /// Provides JSON serialization-related metadata about a type. /// /// The generic definition of the type. - [EditorBrowsable(EditorBrowsableState.Never)] public abstract class JsonTypeInfo : JsonTypeInfo { private Action? _serialize; + private Func? _typedCreateObject; + + /// + /// Function for creating object before properties are set. If set to null type is not deserializable. + /// + public new Func? CreateObject + { + get => _typedCreateObject; + set + { + SetCreateObject(value); + } + } + + private protected override void SetCreateObject(Delegate? createObject) + { + Debug.Assert(createObject is null or Func or Func); + + CheckMutable(); + + if (Kind == JsonTypeInfoKind.None) + { + Debug.Assert(_createObject == null); + Debug.Assert(_typedCreateObject == null); + ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKindNone(); + } + + Func? untypedCreateObject; + Func? typedCreateObject; + + if (createObject is null) + { + untypedCreateObject = null; + typedCreateObject = null; + } + else if (createObject is Func typedDelegate) + { + typedCreateObject = typedDelegate; + untypedCreateObject = createObject is Func untypedDelegate ? untypedDelegate : () => typedDelegate()!; + } + else + { + Debug.Assert(createObject is Func); + untypedCreateObject = (Func)createObject; + typedCreateObject = () => (T)untypedCreateObject(); + } + + _createObject = untypedCreateObject; + _typedCreateObject = typedCreateObject; + } + internal JsonTypeInfo(JsonConverter converter, JsonSerializerOptions options) : base(typeof(T), converter, options) { } @@ -24,6 +74,7 @@ internal JsonTypeInfo(JsonConverter converter, JsonSerializerOptions options) /// values specified at design time. /// /// The writer is not flushed after writing. + [EditorBrowsable(EditorBrowsableState.Never)] public Action? SerializeHandler { get @@ -32,6 +83,7 @@ public Action? SerializeHandler } private protected set { + Debug.Assert(!_isConfigured, "We should not mutate configured JsonTypeInfo"); _serialize = value; HasSerialize = value != null; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolver.cs new file mode 100644 index 0000000000000..9c111cd516607 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolver.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace System.Text.Json.Serialization.Metadata +{ + /// + /// Contains utilities for IJsonTypeInfoResolver + /// + public static class JsonTypeInfoResolver + { + /// + /// Combines multiple IJsonTypeInfoResolvers + /// + /// + /// + /// + /// All resolvers except last one should return null when they do not know how to create JsonTypeInfo for a given type. + /// Last resolver on the list should return non-null for most of the types unless explicit type blocking is desired. + /// + public static IJsonTypeInfoResolver Combine(params IJsonTypeInfoResolver[] resolvers) + { + if (resolvers == null) + { + throw new ArgumentNullException(nameof(resolvers)); + } + + foreach (var resolver in resolvers) + { + if (resolver == null) + { + throw new ArgumentNullException(nameof(resolvers), SR.CombineOneOfResolversIsNull); + } + } + + return new CombiningJsonTypeInfoResolver(resolvers); + } + + private sealed class CombiningJsonTypeInfoResolver : IJsonTypeInfoResolver + { + private readonly IJsonTypeInfoResolver[] _resolvers; + + public CombiningJsonTypeInfoResolver(IJsonTypeInfoResolver[] resolvers) + { + _resolvers = resolvers.AsSpan().ToArray(); + } + + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + foreach (IJsonTypeInfoResolver resolver in _resolvers) + { + JsonTypeInfo? typeInfo = resolver.GetTypeInfo(type, options); + if (typeInfo != null) + { + return typeInfo; + } + } + + return null; + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs index 9e3fb51b28498..d685d241628bf 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs @@ -9,7 +9,7 @@ namespace System.Text.Json.Serialization.Metadata { internal abstract class MemberAccessor { - public abstract JsonTypeInfo.ConstructorDelegate? CreateConstructor( + public abstract Func? CreateConstructor( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type classType); public abstract Func? CreateParameterizedConstructor(ConstructorInfo constructor); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.cs index d35e6e9e2cdae..2e0e5e94b2a87 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.cs @@ -23,7 +23,7 @@ internal sealed partial class ReflectionEmitCachingMemberAccessor : MemberAccess Justification = "Parent method annotation does not flow to lambda method, cf. https://github.com/dotnet/roslyn/issues/46646")] static (_) => s_sourceAccessor.CreateAddMethodDelegate()); - public override JsonTypeInfo.ConstructorDelegate? CreateConstructor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type classType) + public override Func? CreateConstructor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type classType) => s_cache.GetOrAdd((nameof(CreateConstructor), classType, null), [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2077:UnrecognizedReflectionPattern", Justification = "Cannot apply DynamicallyAccessedMembersAttribute to tuple properties.")] diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs index 6a6f612363f6d..5a5500a1cb0d6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs @@ -13,7 +13,7 @@ namespace System.Text.Json.Serialization.Metadata [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] internal sealed class ReflectionEmitMemberAccessor : MemberAccessor { - public override JsonTypeInfo.ConstructorDelegate? CreateConstructor( + public override Func? CreateConstructor( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) { Debug.Assert(type != null); @@ -59,7 +59,7 @@ internal sealed class ReflectionEmitMemberAccessor : MemberAccessor generator.Emit(OpCodes.Ret); - return (JsonTypeInfo.ConstructorDelegate)dynamicMethod.CreateDelegate(typeof(JsonTypeInfo.ConstructorDelegate)); + return (Func)dynamicMethod.CreateDelegate(typeof(Func)); } public override Func? CreateParameterizedConstructor(ConstructorInfo constructor) => diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs index 43d18ae473c6a..a5ff1fdcb795f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs @@ -19,7 +19,7 @@ internal sealed class ReflectionJsonTypeInfo : JsonTypeInfo [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] internal ReflectionJsonTypeInfo(JsonSerializerOptions options) : this( - GetConverter( + GetEffectiveConverter( typeof(T), parentClassType: null, // A TypeInfo never has a "parent" class. memberInfo: null, // A TypeInfo never has a "parent" property. @@ -40,17 +40,23 @@ internal ReflectionJsonTypeInfo(JsonConverter converter, JsonSerializerOptions o AddPropertiesAndParametersUsingReflection(); } - CreateObject = Options.MemberAccessorStrategy.CreateConstructor(typeof(T)); + Func? createObject = Options.MemberAccessorStrategy.CreateConstructor(typeof(T)); + if (converter.UsesDefaultConstructor) + { + SetCreateObject(createObject); + } + + CreateObjectForExtensionDataProperty = createObject; } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", - Justification = "The ctor is marked as RequiresUnreferencedCode")] + Justification = "The ctor is marked as RequiresUnreferencedCode")] [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", Justification = "The ctor is marked RequiresDynamicCode.")] internal override void Configure() { base.Configure(); - PropertyInfoForTypeInfo.ConverterBase.ConfigureJsonTypeInfoUsingReflection(this, Options); + Converter.ConfigureJsonTypeInfoUsingReflection(this, Options); } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] @@ -72,7 +78,7 @@ private void AddPropertiesAndParametersUsingReflection() // PropertyCache is not accessed by other threads until the current JsonTypeInfo instance // is finished initializing and added to the cache on JsonSerializerOptions. // Default 'capacity' to the common non-polymorphic + property case. - PropertyCache = new JsonPropertyDictionary(Options.PropertyNameCaseInsensitive, capacity: properties.Length); + PropertyCache = CreatePropertyCache(capacity: properties.Length); // We start from the most derived type. Type? currentType = Type; @@ -181,7 +187,13 @@ private void CacheMember( ThrowHelper.ThrowInvalidOperationException_SerializationDuplicateTypeAttribute(Type, typeof(JsonExtensionDataAttribute)); } - JsonPropertyInfo jsonPropertyInfo = AddProperty(memberInfo, memberType, declaringType, isVirtual, Options); + JsonPropertyInfo? jsonPropertyInfo = AddProperty(memberInfo, memberType, declaringType, isVirtual, Options); + if (jsonPropertyInfo == null) + { + // ignored invalid property + return; + } + Debug.Assert(jsonPropertyInfo.Name != null); if (hasExtensionAttribute) @@ -197,7 +209,7 @@ private void CacheMember( } } - private static JsonPropertyInfo AddProperty( + private JsonPropertyInfo? AddProperty( MemberInfo memberInfo, Type memberType, Type parentClassType, @@ -205,18 +217,31 @@ private static JsonPropertyInfo AddProperty( JsonSerializerOptions options) { JsonIgnoreCondition? ignoreCondition = JsonPropertyInfo.GetAttribute(memberInfo)?.Condition; - if (ignoreCondition == JsonIgnoreCondition.Always) + + if (IsInvalidForSerialization(memberType)) { - return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(memberInfo, memberType, isVirtual, options); + if (ignoreCondition == JsonIgnoreCondition.Always) + return null; + + ThrowHelper.ThrowInvalidOperationException_CannotSerializeInvalidType(memberType, parentClassType, memberInfo); } - ValidateType(memberType, parentClassType, memberInfo, options); + JsonConverter? customConverter; + JsonConverter converter; - JsonConverter converter = GetConverter( + try + { + converter = GetConverter( memberType, parentClassType, memberInfo, - options); + options, + out customConverter); + } + catch (InvalidOperationException) when (ignoreCondition == JsonIgnoreCondition.Always) + { + return null; + } return CreateProperty( declaredPropertyType: memberType, @@ -225,7 +250,8 @@ private static JsonPropertyInfo AddProperty( isVirtual, converter, options, - ignoreCondition); + ignoreCondition, + customConverter: customConverter); } private static JsonNumberHandling? GetNumberHandlingForType(Type type) @@ -236,22 +262,6 @@ private static JsonPropertyInfo AddProperty( return numberHandlingAttribute?.Handling; } - // This method gets the runtime information for a given type or property. - // The runtime information consists of the following: - // - class type, - // - element type (if the type is a collection), - // - the converter (either native or custom), if one exists. - private static JsonConverter GetConverter( - Type type, - Type? parentClassType, - MemberInfo? memberInfo, - JsonSerializerOptions options) - { - Debug.Assert(type != null); - Debug.Assert(!IsInvalidForSerialization(type), $"Type `{type.FullName}` should already be validated."); - return options.GetConverterFromMember(parentClassType, type, memberInfo); - } - private static bool PropertyIsOverridenAndIgnored( string currentMemberName, Type currentMemberType, @@ -270,7 +280,7 @@ private static bool PropertyIsOverridenAndIgnored( internal override JsonParameterInfoValues[] GetParameterInfoValues() { - ParameterInfo[] parameters = PropertyInfoForTypeInfo.ConverterBase.ConstructorInfo!.GetParameters(); + ParameterInfo[] parameters = Converter.ConstructorInfo!.GetParameters(); return GetParameterInfoArray(parameters); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs index 41527aa309366..9914033a11bed 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs @@ -22,7 +22,7 @@ public ConstructorContext([DynamicallyAccessedMembers(DynamicallyAccessedMemberT => Activator.CreateInstance(_type, nonPublic: false); } - public override JsonTypeInfo.ConstructorDelegate? CreateConstructor( + public override Func? CreateConstructor( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) { Debug.Assert(type != null); @@ -38,7 +38,7 @@ public ConstructorContext([DynamicallyAccessedMembers(DynamicallyAccessedMemberT return null; } - return new ConstructorContext(type).CreateInstance; + return new ConstructorContext(type).CreateInstance!; } public override Func? CreateParameterizedConstructor(ConstructorInfo constructor) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/SourceGenJsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/SourceGenJsonTypeInfoOfT.cs index d724c39236fef..85cfa258d8703 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/SourceGenJsonTypeInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/SourceGenJsonTypeInfoOfT.cs @@ -34,12 +34,13 @@ public SourceGenJsonTypeInfo(JsonSerializerOptions options, JsonObjectInfoValues } else { - SetCreateObjectFunc(objectInfo.ObjectCreator); + SetCreateObject(objectInfo.ObjectCreator); + CreateObjectForExtensionDataProperty = ((JsonTypeInfo)this).CreateObject; } - PropInitFunc = objectInfo.PropertyMetadataInitializer; SerializeHandler = objectInfo.SerializeHandler; + NumberHandling = objectInfo.NumberHandling; } @@ -52,7 +53,7 @@ public SourceGenJsonTypeInfo( Func> converterCreator, object? createObjectWithArgs = null, object? addFunc = null) - : base(GetConverter(collectionInfo, converterCreator), options) + : base(new JsonMetadataServicesConverter(converterCreator()), options) { if (collectionInfo is null) { @@ -60,12 +61,13 @@ public SourceGenJsonTypeInfo( } KeyTypeInfo = collectionInfo.KeyInfo; - ElementTypeInfo = collectionInfo.ElementInfo ?? throw new ArgumentNullException(nameof(collectionInfo.ElementInfo)); + ElementTypeInfo = collectionInfo.ElementInfo; + Debug.Assert(Kind != JsonTypeInfoKind.None); NumberHandling = collectionInfo.NumberHandling; SerializeHandler = collectionInfo.SerializeHandler; CreateObjectWithArgs = createObjectWithArgs; AddMethodDelegate = addFunc; - SetCreateObjectFunc(collectionInfo.ObjectCreator); + CreateObject = collectionInfo.ObjectCreator; } private static JsonConverter GetConverter(JsonObjectInfoValues objectInfo) @@ -86,12 +88,6 @@ private static JsonConverter GetConverter(JsonObjectInfoValues objectInfo) #pragma warning restore CS8714 } - private static JsonConverter GetConverter(JsonCollectionInfoValues collectionInfo, Func> converterCreator) - { - ConverterStrategy strategy = collectionInfo.KeyInfo == null ? ConverterStrategy.Enumerable : ConverterStrategy.Dictionary; - return new JsonMetadataServicesConverter(converterCreator, strategy); - } - internal override void LateAddProperties() { AddPropertiesUsingSourceGenInfo(); @@ -99,9 +95,9 @@ internal override void LateAddProperties() internal override JsonParameterInfoValues[] GetParameterInfoValues() { - JsonSerializerContext? context = Options.JsonSerializerContext; + JsonSerializerContext? context = Options.SerializerContext; JsonParameterInfoValues[] array; - if (context == null || CtorParamInitFunc == null || (array = CtorParamInitFunc()) == null) + if (CtorParamInitFunc == null || (array = CtorParamInitFunc()) == null) { ThrowHelper.ThrowInvalidOperationException_NoMetadataForTypeCtorParams(context, Type); return null!; @@ -117,22 +113,22 @@ internal void AddPropertiesUsingSourceGenInfo() return; } - JsonSerializerContext? context = Options.JsonSerializerContext; + JsonSerializerContext? context = Options.SerializerContext; JsonPropertyInfo[] array; - if (context == null || PropInitFunc == null || (array = PropInitFunc(context)) == null) + if (PropInitFunc == null || (array = PropInitFunc(context!)) == null) { if (typeof(T) == typeof(object)) { return; } - if (PropertyInfoForTypeInfo.ConverterBase.ElementType != null) + if (Converter.ElementType != null) { // Nullable<> or F# optional converter's strategy is set to element's strategy return; } - if (SerializeHandler != null && Options.JsonSerializerContext?.CanUseSerializationLogic == true) + if (SerializeHandler != null && Options.SerializerContext?.CanUseSerializationLogic == true) { ThrowOnDeserialize = true; return; @@ -143,7 +139,7 @@ internal void AddPropertiesUsingSourceGenInfo() } Dictionary? ignoredMembers = null; - JsonPropertyDictionary propertyCache = new(Options.PropertyNameCaseInsensitive, array.Length); + JsonPropertyDictionary propertyCache = CreatePropertyCache(capacity: array.Length); for (int i = 0; i < array.Length; i++) { @@ -154,7 +150,8 @@ internal void AddPropertiesUsingSourceGenInfo() { if (hasJsonInclude) { - ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(jsonPropertyInfo.ClrName!, jsonPropertyInfo.DeclaringType); + Debug.Assert(jsonPropertyInfo.ClrName != null, "ClrName is not set by source gen"); + ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(jsonPropertyInfo.ClrName, jsonPropertyInfo.DeclaringType); } continue; @@ -181,13 +178,5 @@ internal void AddPropertiesUsingSourceGenInfo() PropertyCache = propertyCache; } - - private void SetCreateObjectFunc(Func? createObjectFunc) - { - if (createObjectFunc != null) - { - CreateObject = () => createObjectFunc(); - } - } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs index 8f0e9ec9292cd..2ca7109af2886 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs @@ -226,7 +226,7 @@ public JsonConverter InitializePolymorphicReEntry(JsonTypeInfo derivedJsonTypeIn Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; SetConstructorArgumentState(); - return derivedJsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase; + return derivedJsonTypeInfo.Converter; } @@ -241,7 +241,7 @@ public JsonConverter ResumePolymorphicReEntry() // Swap out the two values as we resume the polymorphic converter (Current.JsonTypeInfo, Current.PolymorphicJsonTypeInfo) = (Current.PolymorphicJsonTypeInfo, Current.JsonTypeInfo); Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; - return Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase; + return Current.JsonTypeInfo.Converter; } /// @@ -382,20 +382,20 @@ public JsonTypeInfo GetTopJsonTypeInfoWithParameterizedConstructor() for (int i = 0; i < _count - 1; i++) { - if (_stack[i].JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.ConstructorIsParameterized) + if (_stack[i].JsonTypeInfo.Converter.ConstructorIsParameterized) { return _stack[i].JsonTypeInfo; } } - Debug.Assert(Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.ConstructorIsParameterized); + Debug.Assert(Current.JsonTypeInfo.Converter.ConstructorIsParameterized); return Current.JsonTypeInfo; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetConstructorArgumentState() { - if (Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.ConstructorIsParameterized) + if (Current.JsonTypeInfo.Converter.ConstructorIsParameterized) { // A zero index indicates a new stack frame. if (Current.CtorArgumentStateIndex == 0) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs index 81e00d7147f52..0918f09f39379 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs @@ -158,7 +158,7 @@ internal JsonConverter Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinu SupportContinuation = supportContinuation; SupportAsync = supportAsync; - return jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase; + return jsonTypeInfo.Converter; } /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs index 0e9fc8c63fbc2..98127e2370372 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs @@ -129,7 +129,7 @@ public JsonConverter InitializePolymorphicReEntry(Type runtimeType, JsonSerializ } PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; - return PolymorphicJsonTypeInfo.ConverterBase; + return PolymorphicJsonTypeInfo.EffectiveConverter; } /// @@ -141,7 +141,7 @@ public JsonConverter InitializePolymorphicReEntry(JsonTypeInfo derivedJsonTypeIn PolymorphicJsonTypeInfo = derivedJsonTypeInfo.PropertyInfoForTypeInfo; PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; - return PolymorphicJsonTypeInfo.ConverterBase; + return PolymorphicJsonTypeInfo.EffectiveConverter; } /// @@ -152,7 +152,7 @@ public JsonConverter ResumePolymorphicReEntry() Debug.Assert(PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntrySuspended); Debug.Assert(PolymorphicJsonTypeInfo is not null); PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; - return PolymorphicJsonTypeInfo.ConverterBase; + return PolymorphicJsonTypeInfo.EffectiveConverter; } /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs index 364c100282412..90bfe5a66f06d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs @@ -15,21 +15,9 @@ public static void ThrowArgumentException_NodeValueNotAllowed(string paramName) } [DoesNotReturn] - public static void ThrowArgumentException_NodeArrayTooSmall(string paramName) + public static void ThrowArgumentException_DuplicateKey(string paramName, string propertyName) { - throw new ArgumentException(SR.NodeArrayTooSmall, paramName); - } - - [DoesNotReturn] - public static void ThrowArgumentOutOfRangeException_NodeArrayIndexNegative(string paramName) - { - throw new ArgumentOutOfRangeException(paramName, SR.NodeArrayIndexNegative); - } - - [DoesNotReturn] - public static void ThrowArgumentException_DuplicateKey(string propertyName) - { - throw new ArgumentException(SR.NodeDuplicateKey, propertyName); + throw new ArgumentException(SR.Format(SR.NodeDuplicateKey, propertyName), paramName); } [DoesNotReturn] @@ -51,14 +39,14 @@ public static void ThrowInvalidOperationException_NodeElementCannotBeObjectOrArr } [DoesNotReturn] - public static void ThrowNotSupportedException_NodeCollectionIsReadOnly() + public static void ThrowNotSupportedException_CollectionIsReadOnly() { - throw GetNotSupportedException_NodeCollectionIsReadOnly(); + throw GetNotSupportedException_CollectionIsReadOnly(); } - public static NotSupportedException GetNotSupportedException_NodeCollectionIsReadOnly() + public static NotSupportedException GetNotSupportedException_CollectionIsReadOnly() { - return new NotSupportedException(SR.NodeCollectionIsReadOnly); + return new NotSupportedException(SR.CollectionIsReadOnly); } } } 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 28db4eee742b8..0f08484bdc69a 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 @@ -104,6 +104,24 @@ public static void ThrowInvalidOperationException_SerializationConverterNotCompa throw new InvalidOperationException(SR.Format(SR.SerializationConverterNotCompatible, converterType, type)); } + [DoesNotReturn] + public static void ThrowInvalidOperationException_ResolverTypeNotCompatible(Type requestedType, Type actualType) + { + throw new InvalidOperationException(SR.Format(SR.ResolverTypeNotCompatible, requestedType, actualType)); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_ResolverTypeInfoOptionsNotCompatible() + { + throw new InvalidOperationException(SR.ResolverTypeInfoOptionsNotCompatible); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_JsonTypeInfoUsedButTypeInfoResolverNotSet() + { + throw new InvalidOperationException(SR.JsonTypeInfoUsedButTypeInfoResolverNotSet); + } + [DoesNotReturn] public static void ThrowInvalidOperationException_SerializationConverterOnAttributeInvalid(Type classType, MemberInfo? memberInfo) { @@ -140,9 +158,33 @@ public static void ThrowInvalidOperationException_SerializerOptionsImmutable(Jso } [DoesNotReturn] - public static void ThrowInvalidOperationException_SerializerPropertyNameConflict(Type type, JsonPropertyInfo jsonPropertyInfo) + public static void ThrowInvalidOperationException_SerializerContextOptionsImmutable() + { + throw new InvalidOperationException(SR.SerializerContextOptionsImmutable); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_TypeInfoResolverImmutable() + { + throw new InvalidOperationException(SR.TypeInfoResolverImmutable); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_TypeInfoImmutable() { - throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameConflict, type, jsonPropertyInfo.ClrName)); + throw new InvalidOperationException(SR.TypeInfoImmutable); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_PropertyInfoImmutable() + { + throw new InvalidOperationException(SR.PropertyInfoImmutable); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_SerializerPropertyNameConflict(Type type, string propertyName) + { + throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameConflict, type, propertyName)); } [DoesNotReturn] @@ -192,9 +234,9 @@ public static void ThrowInvalidOperationException_ConstructorParameterIncomplete } [DoesNotReturn] - public static void ThrowInvalidOperationException_ExtensionDataCannotBindToCtorParam(JsonPropertyInfo jsonPropertyInfo) + public static void ThrowInvalidOperationException_ExtensionDataCannotBindToCtorParam(string propertyName, JsonPropertyInfo jsonPropertyInfo) { - throw new InvalidOperationException(SR.Format(SR.ExtensionDataCannotBindToCtorParam, jsonPropertyInfo.ClrName, jsonPropertyInfo.DeclaringType)); + throw new InvalidOperationException(SR.Format(SR.ExtensionDataCannotBindToCtorParam, propertyName, jsonPropertyInfo.DeclaringType)); } [DoesNotReturn] @@ -239,6 +281,18 @@ public static void ThrowNotSupportedException_ObjectWithParameterizedCtorRefMeta ThrowNotSupportedException(ref state, reader, ex); } + [DoesNotReturn] + public static void ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKindNone() + { + throw new InvalidOperationException(SR.JsonTypeInfoOperationNotPossibleForKindNone); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_CollectionIsReadOnly() + { + throw new InvalidOperationException(SR.CollectionIsReadOnly); + } + [DoesNotReturn] public static void ReThrowWithPath(ref ReadStack state, JsonReaderException ex) { @@ -566,6 +620,13 @@ public static void ThrowInvalidOperationException_MetadataReferenceOfTypeCannotB throw new InvalidOperationException(SR.Format(SR.MetadataReferenceOfTypeCannotBeAssignedToType, referenceId, currentType, typeToConvert)); } + [DoesNotReturn] + public static void ThrowInvalidOperationException_JsonPropertyInfoIsBoundToDifferentJsonTypeInfo(JsonPropertyInfo propertyInfo) + { + Debug.Assert(propertyInfo.ParentTypeInfo != null, "We should not throw this exception when ParentTypeInfo is null"); + throw new InvalidOperationException(SR.Format(SR.JsonPropertyInfoBoundToDifferentParent, propertyInfo.Name, propertyInfo.ParentTypeInfo.Type.FullName)); + } + [DoesNotReturn] internal static void ThrowUnexpectedMetadataException( ReadOnlySpan propertyName, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs index e7f151dc9df2b..9af92adb3808d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs @@ -35,6 +35,18 @@ public static void ThrowArgumentOutOfRangeException_CommentEnumMustBeInRange(str throw GetArgumentOutOfRangeException(parameterName, SR.CommentHandlingMustBeValid); } + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeException_ArrayIndexNegative(string paramName) + { + throw new ArgumentOutOfRangeException(paramName, SR.ArrayIndexNegative); + } + + [DoesNotReturn] + public static void ThrowArgumentException_ArrayTooSmall(string paramName) + { + throw new ArgumentException(SR.ArrayTooSmall, paramName); + } + private static ArgumentException GetArgumentException(string message) { return new ArgumentException(message); diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs index 9681105bb01ce..85a4f81ddfe16 100644 --- a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs @@ -2751,8 +2751,8 @@ public async Task SerializationMetadataNotComputedWhenMemberIgnored() #if !BUILDING_SOURCE_GENERATOR_TESTS // Without [JsonIgnore], serializer throws exceptions due to runtime-reflection-based property metadata inspection. - await Assert.ThrowsAsync(async () => await Serializer.SerializeWrapper(new TypeWith_RefStringProp())); - await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper("{}")); + await Assert.ThrowsAsync(async () => await Serializer.SerializeWrapper(new TypeWith_RefStringProp())); + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper("{}")); await Assert.ThrowsAsync(async () => await Serializer.SerializeWrapper(new TypeWith_PropWith_BadConverter())); await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper("{}")); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs index 4b3c128f2652b..6909f866b57d1 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Reflection; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Microsoft.DotNet.RemoteExecutor; using Xunit; @@ -37,25 +38,20 @@ public static void Converters_AndTypeInfoCreator_NotRooted_WhenMetadataNotPresen Assert.Contains("JsonSerializerOptions", exAsStr); // This test uses reflection to: - // - Access JsonSerializerOptions.s_defaultSimpleConverters - // - Access JsonSerializerOptions.s_defaultFactoryConverters - // - Access JsonSerializerOptions.s_typeInfoCreationFunc + // - Access DefaultJsonTypeInfoResolver.s_defaultSimpleConverters + // - Access DefaultJsonTypeInfoResolver.s_defaultFactoryConverters // // If any of them changes, this test will need to be kept in sync. // Confirm built-in converters not set. - AssertFieldNull("s_defaultSimpleConverters", optionsInstance: null); - AssertFieldNull("s_defaultFactoryConverters", optionsInstance: null); + AssertFieldNull("s_defaultSimpleConverters"); + AssertFieldNull("s_defaultFactoryConverters"); - // Confirm type info dynamic creator not set. - AssertFieldNull("s_typeInfoCreationFunc", optionsInstance: null); - - static void AssertFieldNull(string fieldName, JsonSerializerOptions? optionsInstance) + static void AssertFieldNull(string fieldName) { - BindingFlags bindingFlags = BindingFlags.NonPublic | (optionsInstance == null ? BindingFlags.Static : BindingFlags.Instance); - FieldInfo fieldInfo = typeof(JsonSerializerOptions).GetField(fieldName, bindingFlags); + FieldInfo fieldInfo = typeof(DefaultJsonTypeInfoResolver).GetField(fieldName, BindingFlags.Static | BindingFlags.NonPublic); Assert.NotNull(fieldInfo); - Assert.Null(fieldInfo.GetValue(optionsInstance)); + Assert.Null(fieldInfo.GetValue(null)); } }).Dispose(); } @@ -102,6 +98,132 @@ public static void SupportsPositionalRecords() Assert.Equal("Doe", person.LastName); } + [Fact] + public static void CombiningContexts_ResolveJsonTypeInfo() + { + IJsonTypeInfoResolver combined = JsonTypeInfoResolver.Combine(NestedContext.Default, PersonJsonContext.Default); + var options = new JsonSerializerOptions { TypeInfoResolver = combined }; + + JsonTypeInfo messageInfo = combined.GetTypeInfo(typeof(JsonMessage), options); + Assert.IsAssignableFrom>(messageInfo); + Assert.Same(options, messageInfo.Options); + + JsonTypeInfo personInfo = combined.GetTypeInfo(typeof(Person), options); + Assert.IsAssignableFrom>(personInfo); + Assert.Same(options, personInfo.Options); + } + + [Fact] + public static void CombiningContexts_ResolveJsonTypeInfo_DifferentCasing() + { + IJsonTypeInfoResolver combined = JsonTypeInfoResolver.Combine(NestedContext.Default, PersonJsonContext.Default); + var options = new JsonSerializerOptions + { + TypeInfoResolver = combined, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + Assert.NotSame(JsonNamingPolicy.CamelCase, NestedContext.Default.Options.PropertyNamingPolicy); + Assert.Same(JsonNamingPolicy.CamelCase, PersonJsonContext.Default.Options.PropertyNamingPolicy); + + JsonTypeInfo messageInfo = combined.GetTypeInfo(typeof(JsonMessage), options); + Assert.Equal(2, messageInfo.Properties.Count); + Assert.Equal("message", messageInfo.Properties[0].Name); + Assert.Equal("length", messageInfo.Properties[1].Name); + + JsonTypeInfo personInfo = combined.GetTypeInfo(typeof(Person), options); + Assert.Equal(2, personInfo.Properties.Count); + Assert.Equal("firstName", personInfo.Properties[0].Name); + Assert.Equal("lastName", personInfo.Properties[1].Name); + } + + [Theory] + [MemberData(nameof(GetCombiningContextsData))] + public static void CombiningContexts_Serialization(T value, string expectedJson) + { + IJsonTypeInfoResolver combined = JsonTypeInfoResolver.Combine(NestedContext.Default, PersonJsonContext.Default); + var options = new JsonSerializerOptions { TypeInfoResolver = combined }; + + JsonTypeInfo typeInfo = (JsonTypeInfo)combined.GetTypeInfo(typeof(T), options)!; + + string json = JsonSerializer.Serialize(value, typeInfo); + JsonTestHelper.AssertJsonEqual(expectedJson, json); + + json = JsonSerializer.Serialize(value, options); + JsonTestHelper.AssertJsonEqual(expectedJson, json); + + JsonSerializer.Deserialize(json, typeInfo); + JsonSerializer.Deserialize(json, options); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void CombiningContextWithCustomResolver_ReplacePoco() + { + TestResolver customResolver = new((type, options) => + { + if (type != typeof(TestPoco)) + return null; + + JsonTypeInfo typeInfo = JsonTypeInfo.CreateJsonTypeInfo(options); + typeInfo.CreateObject = () => new TestPoco(); + JsonPropertyInfo property = typeInfo.CreateJsonPropertyInfo(typeof(string), "test"); + property.Get = (o) => System.Runtime.CompilerServices.Unsafe.Unbox(o).IntProperty.ToString(); + property.Set = (o, val) => + { + System.Runtime.CompilerServices.Unsafe.Unbox(o).StringProperty = (string)val; + System.Runtime.CompilerServices.Unsafe.Unbox(o).IntProperty = int.Parse((string)val); + }; + + typeInfo.Properties.Add(property); + return typeInfo; + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = JsonTypeInfoResolver.Combine(customResolver, ClassWithPocoListDictionaryAndNullablePropertyContext.Default); + + // ensure we're not falling back to reflection serialization + Assert.Throws(() => JsonSerializer.Serialize(new Person("a", "b"), o)); + Assert.Throws(() => JsonSerializer.Serialize((byte)1, o)); + + ClassWithPocoListDictionaryAndNullable obj = new() + { + UIntProperty = 13, + ListOfPocoProperty = new List() { new TestPoco() { IntProperty = 4 }, new TestPoco() { IntProperty = 5 } }, + DictionaryPocoValueProperty = new Dictionary() { ['c'] = new TestPoco() { IntProperty = 6 }, ['d'] = new TestPoco() { IntProperty = 7 } }, + NullablePocoProperty = new TestPoco() { IntProperty = 8 }, + PocoProperty = new TestPoco() { IntProperty = 9 }, + }; + + string json = JsonSerializer.Serialize(obj, o); + Assert.Equal("""{"UIntProperty":13,"ListOfPocoProperty":[{"test":"4"},{"test":"5"}],"DictionaryPocoValueProperty":{"c":{"test":"6"},"d":{"test":"7"}},"NullablePocoProperty":{"test":"8"},"PocoProperty":{"test":"9"}}""", json); + + ClassWithPocoListDictionaryAndNullable deserialized = JsonSerializer.Deserialize(json, o); + Assert.Equal(obj.UIntProperty, deserialized.UIntProperty); + Assert.Equal(obj.ListOfPocoProperty.Count, deserialized.ListOfPocoProperty.Count); + Assert.Equal(2, obj.ListOfPocoProperty.Count); + Assert.Equal(obj.ListOfPocoProperty[0].IntProperty.ToString(), deserialized.ListOfPocoProperty[0].StringProperty); + Assert.Equal(obj.ListOfPocoProperty[0].IntProperty, deserialized.ListOfPocoProperty[0].IntProperty); + Assert.Equal(obj.ListOfPocoProperty[1].IntProperty.ToString(), deserialized.ListOfPocoProperty[1].StringProperty); + Assert.Equal(obj.ListOfPocoProperty[1].IntProperty, deserialized.ListOfPocoProperty[1].IntProperty); + Assert.Equal(obj.DictionaryPocoValueProperty.Count, deserialized.DictionaryPocoValueProperty.Count); + Assert.Equal(2, obj.DictionaryPocoValueProperty.Count); + Assert.Equal(obj.DictionaryPocoValueProperty['c'].IntProperty.ToString(), deserialized.DictionaryPocoValueProperty['c'].StringProperty); + Assert.Equal(obj.DictionaryPocoValueProperty['c'].IntProperty, deserialized.DictionaryPocoValueProperty['c'].IntProperty); + Assert.Equal(obj.DictionaryPocoValueProperty['d'].IntProperty.ToString(), deserialized.DictionaryPocoValueProperty['d'].StringProperty); + Assert.Equal(obj.DictionaryPocoValueProperty['d'].IntProperty, deserialized.DictionaryPocoValueProperty['d'].IntProperty); + Assert.Equal(obj.NullablePocoProperty.Value.IntProperty.ToString(), deserialized.NullablePocoProperty.Value.StringProperty); + Assert.Equal(obj.NullablePocoProperty.Value.IntProperty, deserialized.NullablePocoProperty.Value.IntProperty); + Assert.Equal(obj.PocoProperty.IntProperty.ToString(), deserialized.PocoProperty.StringProperty); + Assert.Equal(obj.PocoProperty.IntProperty, deserialized.PocoProperty.IntProperty); + } + + public static IEnumerable GetCombiningContextsData() + { + yield return WrapArgs(new JsonMessage { Message = "Hi" }, """{ "Message" : "Hi", "Length" : 2 }"""); + yield return WrapArgs(new Person("John", "Doe"), """{ "FirstName" : "John", "LastName" : "Doe" }"""); + static object[] WrapArgs(T value, string expectedJson) => new object[] { value, expectedJson }; + } + [JsonSerializable(typeof(JsonMessage))] internal partial class NestedContext : JsonSerializerContext { } @@ -182,5 +304,38 @@ public enum TestEnum internal partial class GenericParameterWithCustomConverterFactoryContext : JsonSerializerContext { } + + [JsonSerializable(typeof(ClassWithPocoListDictionaryAndNullable))] + internal partial class ClassWithPocoListDictionaryAndNullablePropertyContext : JsonSerializerContext + { + + } + + internal class ClassWithPocoListDictionaryAndNullable + { + public uint UIntProperty { get; set; } + public List ListOfPocoProperty { get; set; } + public Dictionary DictionaryPocoValueProperty { get; set; } + public TestPoco? NullablePocoProperty { get; set; } + public TestPoco PocoProperty { get; set; } + } + + internal struct TestPoco + { + public string StringProperty { get; set; } + public int IntProperty { get; set; } + } + + internal class TestResolver : IJsonTypeInfoResolver + { + private Func _getTypeInfo; + + public TestResolver(Func getTypeInfo) + { + _getTypeInfo = getTypeInfo; + } + + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) => _getTypeInfo(type, options); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs index c16738ce5056d..e53b33c3b4dda 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs @@ -371,6 +371,7 @@ public static void JsonSerializerOptions_EqualityComparer_ChangingAnySettingShou yield return (GetProp(nameof(JsonSerializerOptions.UnknownTypeHandling)), JsonUnknownTypeHandling.JsonNode); yield return (GetProp(nameof(JsonSerializerOptions.WriteIndented)), true); yield return (GetProp(nameof(JsonSerializerOptions.ReferenceHandler)), ReferenceHandler.Preserve); + yield return (GetProp(nameof(JsonSerializerOptions.TypeInfoResolver)), new DefaultJsonTypeInfoResolver()); static PropertyInfo GetProp(string name) { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.cs index e7055c2db1a67..1bcbef4ff58ae 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.cs @@ -180,7 +180,7 @@ void RunTest() } [ActiveIssue("https://github.com/dotnet/runtime/issues/66232", TargetFrameworkMonikers.NetFramework)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/66371", typeof(PlatformDetection), nameof(PlatformDetection.IsMonoInterpreter))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/66371", typeof(PlatformDetection), nameof(PlatformDetection.IsMonoInterpreter))] [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public static void GetConverter_Poco_WriteThrowsNotSupportedException() { @@ -196,20 +196,20 @@ public static void GetConverter_Poco_WriteThrowsNotSupportedException() // for reflection-based serialization should throw NotSupportedException // since it can't resolve reflection-based metadata. Assert.Throws(() => converter.Write(writer, value, options)); - Debug.Assert(writer.BytesCommitted + writer.BytesPending == 0); + Assert.Equal(0, writer.BytesCommitted + writer.BytesPending); JsonSerializer.Serialize(42, options); // Same operation should succeed when instance has been primed. converter.Write(writer, value, options); - Debug.Assert(writer.BytesCommitted + writer.BytesPending > 0); + Assert.NotEqual(0, writer.BytesCommitted + writer.BytesPending); writer.Reset(); // State change should not leak into unrelated options instances. var options2 = new JsonSerializerOptions(); options2.AddContext(); Assert.Throws(() => converter.Write(writer, value, options2)); - Debug.Assert(writer.BytesCommitted + writer.BytesPending == 0); + Assert.Equal(0, writer.BytesCommitted + writer.BytesPending); }).Dispose(); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs index 2e3912a7572e9..7f6b4ebd56dd8 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs @@ -572,7 +572,6 @@ private static class JsonSerializerOptionsSmallBufferMapper { private static readonly ConditionalWeakTable s_smallBufferMap = new(); private static readonly JsonSerializerOptions s_DefaultOptionsWithSmallBuffer = new JsonSerializerOptions { DefaultBufferSize = 1 }; - private static readonly FieldInfo s_optionsContextField = typeof(JsonSerializerOptions).GetField("_serializerContext", BindingFlags.NonPublic | BindingFlags.Instance); public static JsonSerializerOptions ResolveOptionsInstanceWithSmallBuffer(JsonSerializerOptions? options) { @@ -592,20 +591,15 @@ public static JsonSerializerOptions ResolveOptionsInstanceWithSmallBuffer(JsonSe return resolvedValue; } - JsonSerializerOptions smallBufferCopy = new JsonSerializerOptions(options) { DefaultBufferSize = 1 }; - CopyJsonSerializerContext(options, smallBufferCopy); + JsonSerializerOptions smallBufferCopy = new JsonSerializerOptions(options) + { + // Copy the resolver explicitly until https://github.com/dotnet/aspnetcore/issues/38720 is resolved. + TypeInfoResolver = options.TypeInfoResolver, + DefaultBufferSize = 1, + }; s_smallBufferMap.Add(options, smallBufferCopy); return smallBufferCopy; } - - private static void CopyJsonSerializerContext(JsonSerializerOptions source, JsonSerializerOptions target) - { - JsonSerializerContext context = (JsonSerializerContext)s_optionsContextField.GetValue(source); - if (context != null) - { - s_optionsContextField.SetValue(target, context); - } - } } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverMultiContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverMultiContextTests.cs new file mode 100644 index 0000000000000..a7dded985278c --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverMultiContextTests.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public partial class DefaultJsonTypeInfoResolverMultiContextTests : SerializerTests + { + public DefaultJsonTypeInfoResolverMultiContextTests() + : base(JsonSerializerWrapper.StringSerializer) + { + } + + [Fact] + public async Task TypeInfoWithNullCreateObjectFailsDeserialization() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add(ti => + { + if (ti.Type == typeof(Poco)) + { + ti.CreateObject = null; + } + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = resolver; + + string json = """{"StringProperty":"test"}"""; + await TestMultiContextDeserialization(json, new Poco() { StringProperty = "test" }); + await TestMultiContextDeserialization(json, options: o, expectedExceptionType: typeof(NotSupportedException)); + + Assert.Throws(() => resolver.Modifiers.Add(ti => { })); + } + + [Theory] + [MemberData(nameof(JsonSerializerSerializeWithTypeInfoOfT_TestData))] + public async Task JsonSerializerSerializeWithTypeInfoOfT(T testObj, string expectedJson) + { + DefaultJsonTypeInfoResolver r = new(); + JsonSerializerOptions o = new(); + o.TypeInfoResolver = r; + JsonTypeInfo typeInfo = (JsonTypeInfo)r.GetTypeInfo(typeof(T), o); + string json = await Serializer.SerializeWrapper(testObj, typeInfo); + Assert.Equal(expectedJson, json); + } + + [Fact] + public async Task SerializationWithJsonTypeInfoWithoutSettingTypeInfoResolverThrows() + { + JsonSerializerOptions o = new(); + DefaultJsonTypeInfoResolver r = new(); + // note: TypeInfoResolver not set + JsonTypeInfo ti = (JsonTypeInfo)r.GetTypeInfo(typeof(SomeClass), o); + SomeClass obj = new() + { + ObjProp = "test", + IntProp = 42, + }; + + // TODO: reassess if this is expected behavior + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(obj, ti)); + } + + [Fact] + public async Task DeserializationWithJsonTypeInfoWithoutSettingTypeInfoResolverThrows() + { + JsonSerializerOptions o = new(); + DefaultJsonTypeInfoResolver r = new(); + // note: TypeInfoResolver not set + JsonTypeInfo ti = (JsonTypeInfo)r.GetTypeInfo(typeof(SomeClass), o); + + // TODO: reassess if this is expected behavior + string json = """{"ObjProp":"test","IntProp":42}"""; + await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, ti)); + } + + [Fact] + public async Task SerializationWithJsonTypeInfoWhenTypeInfoResolverSetIsPossible() + { + JsonSerializerOptions o = new(); + DefaultJsonTypeInfoResolver r = new(); + o.TypeInfoResolver = r; + JsonTypeInfo ti = (JsonTypeInfo)r.GetTypeInfo(typeof(SomeClass), o); + SomeClass obj = new() + { + ObjProp = "test", + IntProp = 42, + }; + + string json = await Serializer.SerializeWrapper(obj, ti); + Assert.Equal("""{"ObjProp":"test","IntProp":42}""", json); + } + + [Fact] + public async Task DeserializationWithJsonTypeInfoWhenTypeInfoResolverSetIsPossible() + { + JsonSerializerOptions o = new(); + DefaultJsonTypeInfoResolver r = new(); + o.TypeInfoResolver = r; + JsonTypeInfo ti = (JsonTypeInfo)r.GetTypeInfo(typeof(SomeClass), o); + string json = """{"ObjProp":"test","IntProp":42}"""; + SomeClass deserialized = await Serializer.DeserializeWrapper(json, ti); + Assert.IsType(deserialized.ObjProp); + Assert.Equal("test", ((JsonElement)deserialized.ObjProp).GetString()); + Assert.Equal(42, deserialized.IntProp); + } + + public static IEnumerable JsonSerializerSerializeWithTypeInfoOfT_TestData() + { + yield return new object[] { "value", @"""value""" }; + yield return new object[] { 5, @"5" }; + yield return new object[] { new SomeClass() { IntProp = 15, ObjProp = 17m }, @"{""ObjProp"":17,""IntProp"":15}" }; + } + + private class Poco + { + public string StringProperty { get; set; } + } + + private class SomeClass + { + public object ObjProp { get; set; } + public int IntProp { get; set; } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonPropertyInfo.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonPropertyInfo.cs new file mode 100644 index 0000000000000..e6442017184b7 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonPropertyInfo.cs @@ -0,0 +1,1153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json.Tests; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class DefaultJsonTypeInfoResolverTests + { + [Fact] + public static void JsonPropertyInfoOptionsAreSet() + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo = JsonTypeInfo.CreateJsonTypeInfo(typeof(MyClass), options); + CreatePropertyAndCheckOptions(options, typeInfo); + + typeInfo = JsonTypeInfo.CreateJsonTypeInfo(options); + CreatePropertyAndCheckOptions(options, typeInfo); + + typeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(MyClass), options); + CreatePropertyAndCheckOptions(options, typeInfo); + + static void CreatePropertyAndCheckOptions(JsonSerializerOptions expectedOptions, JsonTypeInfo typeInfo) + { + JsonPropertyInfo propertyInfo = typeInfo.CreateJsonPropertyInfo(typeof(string), "test"); + Assert.Same(expectedOptions, propertyInfo.Options); + } + } + + [Theory] + [InlineData(typeof(string))] + [InlineData(typeof(int))] + [InlineData(typeof(MyClass))] + public static void JsonPropertyInfoPropertyTypeIsSetWhenUsingCreateJsonPropertyInfo(Type propertyType) + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo = JsonTypeInfo.CreateJsonTypeInfo(typeof(MyClass), options); + JsonPropertyInfo propertyInfo = typeInfo.CreateJsonPropertyInfo(propertyType, "test"); + + Assert.Equal(propertyType, propertyInfo.PropertyType); + } + + [Fact] + public static void JsonPropertyInfoPropertyTypeIsSet() + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(MyClass), options); + Assert.Equal(2, typeInfo.Properties.Count); + JsonPropertyInfo propertyInfo = typeInfo.Properties[0]; + Assert.Equal(typeof(string), propertyInfo.PropertyType); + } + + [Theory] + [InlineData(typeof(string))] + [InlineData(typeof(int))] + [InlineData(typeof(MyClass))] + public static void JsonPropertyInfoNameIsSetAndIsMutableWhenUsingCreateJsonPropertyInfo(Type propertyType) + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo = JsonTypeInfo.CreateJsonTypeInfo(typeof(MyClass), options); + JsonPropertyInfo propertyInfo = typeInfo.CreateJsonPropertyInfo(propertyType, "test"); + + Assert.Equal("test", propertyInfo.Name); + + propertyInfo.Name = "foo"; + Assert.Equal("foo", propertyInfo.Name); + + Assert.Throws(() => propertyInfo.Name = null); + } + + [Fact] + public static void JsonPropertyInfoNameIsSetAndIsMutableForDefaultResolver() + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(MyClass), options); + Assert.Equal(2, typeInfo.Properties.Count); + JsonPropertyInfo propertyInfo = typeInfo.Properties[0]; + + Assert.Equal(nameof(MyClass.Value), propertyInfo.Name); + + propertyInfo.Name = "foo"; + Assert.Equal("foo", propertyInfo.Name); + + Assert.Throws(() => propertyInfo.Name = null); + } + + [Fact] + public static void JsonPropertyInfoForDefaultResolverHasNamingPoliciesRulesApplied() + { + JsonSerializerOptions options = new(); + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + JsonTypeInfo typeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(MyClass), options); + Assert.Equal(2, typeInfo.Properties.Count); + JsonPropertyInfo propertyInfo = typeInfo.Properties[0]; + + Assert.Equal(nameof(MyClass.Value).ToLowerInvariant(), propertyInfo.Name); + + // explicitly setting does not change casing + propertyInfo.Name = "Foo"; + Assert.Equal("Foo", propertyInfo.Name); + } + + [Fact] + public static void JsonPropertyInfoCustomConverterIsNullWhenUsingCreateJsonPropertyInfo() + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(TestClassWithCustomConverterOnProperty), options); + JsonPropertyInfo propertyInfo = typeInfo.CreateJsonPropertyInfo(typeof(MyClass), "test"); + + Assert.Null(propertyInfo.CustomConverter); + } + + [Fact] + public static void JsonPropertyInfoCustomConverterIsNotNullForPropertyWithCustomConverter() + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(TestClassWithCustomConverterOnProperty), options); + Assert.Equal(1, typeInfo.Properties.Count); + JsonPropertyInfo propertyInfo = typeInfo.Properties[0]; + + Assert.NotNull(propertyInfo.CustomConverter); + Assert.IsType(propertyInfo.CustomConverter); + } + + [Fact] + public static void JsonPropertyInfoCustomConverterSetToNullIsRespected() + { + JsonSerializerOptions options = new(); + DefaultJsonTypeInfoResolver r = new(); + r.Modifiers.Add(ti => + { + if (ti.Type == typeof(TestClassWithCustomConverterOnProperty)) + { + Assert.Equal(1, ti.Properties.Count); + JsonPropertyInfo propertyInfo = ti.Properties[0]; + Assert.NotNull(propertyInfo.CustomConverter); + Assert.IsType(propertyInfo.CustomConverter); + propertyInfo.CustomConverter = null; + } + }); + + options.TypeInfoResolver = r; + + TestClassWithCustomConverterOnProperty obj = new() + { + MyClassProperty = new MyClass() { Value = "SomeValue" }, + }; + + string json = JsonSerializer.Serialize(obj, options); + Assert.Equal("""{"MyClassProperty":{"Value":"SomeValue","Thing":null}}""", json); + + TestClassWithCustomConverterOnProperty deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(obj.MyClassProperty.Value, deserialized.MyClassProperty.Value); + } + + [Fact] + public static void JsonPropertyInfoCustomConverterIsRespected() + { + JsonSerializerOptions options = new(); + DefaultJsonTypeInfoResolver r = new(); + r.Modifiers.Add(ti => + { + if (ti.Type == typeof(TestClassWithCustomConverterOnProperty)) + { + JsonPropertyInfo propertyInfo = ti.Properties[0]; + Assert.NotNull(propertyInfo.CustomConverter); + Assert.IsType(propertyInfo.CustomConverter); + propertyInfo.CustomConverter = new MyClassCustomConverter("test_"); + } + }); + + options.TypeInfoResolver = r; + + TestClassWithCustomConverterOnProperty obj = new() + { + MyClassProperty = new MyClass() { Value = "SomeValue" }, + }; + + string json = JsonSerializer.Serialize(obj, options); + Assert.Equal("""{"MyClassProperty":"test_SomeValue"}""", json); + + TestClassWithCustomConverterOnProperty deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(obj.MyClassProperty.Value, deserialized.MyClassProperty.Value); + } + + [Fact] + public static void JsonPropertyInfoCustomConverterFactoryIsNotExpanded() + { + JsonSerializerOptions options = new(); + DefaultJsonTypeInfoResolver r = new(); + JsonConverter? expectedConverter = null; + r.Modifiers.Add(ti => + { + if (ti.Type == typeof(TestClassWithCustomConverterFactoryOnProperty)) + { + JsonPropertyInfo propertyInfo = ti.Properties[0]; + Assert.NotNull(propertyInfo.CustomConverter); + Assert.IsType(propertyInfo.CustomConverter); + expectedConverter = ((MyClassCustomConverterFactory)propertyInfo.CustomConverter).ConverterInstance; + } + }); + + options.TypeInfoResolver = r; + + TestClassWithCustomConverterFactoryOnProperty obj = new() + { + MyClassProperty = new MyClass() { Value = "SomeValue" }, + }; + + string json = JsonSerializer.Serialize(obj, options); + Assert.Equal("""{"MyClassProperty":"test_SomeValue"}""", json); + Assert.NotNull(expectedConverter); + Assert.IsType(expectedConverter); + + TestClassWithCustomConverterFactoryOnProperty deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(obj.MyClassProperty.Value, deserialized.MyClassProperty.Value); + } + + [Fact] + public static void JsonPropertyInfoCustomConverterFactoryIsNotExpandedWhenSetInResolver() + { + JsonSerializerOptions options = new(); + DefaultJsonTypeInfoResolver r = new(); + MyClassCustomConverterFactory converterFactory = new(); + r.Modifiers.Add(ti => + { + if (ti.Type == typeof(TestClassWithProperty)) + { + JsonPropertyInfo propertyInfo = ti.Properties[0]; + Assert.Null(propertyInfo.CustomConverter); + propertyInfo.CustomConverter = converterFactory; + Assert.Same(converterFactory, propertyInfo.CustomConverter); + } + }); + + options.TypeInfoResolver = r; + + TestClassWithProperty obj = new() + { + MyClassProperty = new MyClass() { Value = "SomeValue" }, + }; + + string json = JsonSerializer.Serialize(obj, options); + Assert.Equal("""{"MyClassProperty":"test_SomeValue"}""", json); + + TestClassWithProperty deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(obj.MyClassProperty.Value, deserialized.MyClassProperty.Value); + } + + + [Fact] + public static void JsonPropertyInfoGetIsNullAndMutableWhenUsingCreateJsonPropertyInfo() + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(TestClassWithCustomConverterOnProperty), options); + JsonPropertyInfo propertyInfo = typeInfo.CreateJsonPropertyInfo(typeof(MyClass), "test"); + Assert.Null(propertyInfo.Get); + Func get = (obj) => + { + throw new NotImplementedException(); + }; + + propertyInfo.Get = get; + Assert.Same(get, propertyInfo.Get); + } + + [Fact] + public static void JsonPropertyInfoGetIsNotNullForDefaultResolver() + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(TestClassWithCustomConverterOnProperty), options); + JsonPropertyInfo propertyInfo = typeInfo.Properties[0]; + + Assert.NotNull(propertyInfo.Get); + + TestClassWithCustomConverterOnProperty obj = new(); + + Assert.Null(propertyInfo.Get(obj)); + + obj.MyClassProperty = new MyClass(); + Assert.Same(obj.MyClassProperty, propertyInfo.Get(obj)); + + MyClass sentinel = new(); + Func get = (obj) => sentinel; + propertyInfo.Get = get; + Assert.Same(get, propertyInfo.Get); + Assert.Same(sentinel, propertyInfo.Get(obj)); + } + + [Fact] + public static void JsonPropertyInfoGetPropertyNotSerializableButDeserializableWhenNull() + { + JsonSerializerOptions options = new(); + DefaultJsonTypeInfoResolver r = new(); + r.Modifiers.Add(ti => + { + if (ti.Type == typeof(TestClassWithCustomConverterOnProperty)) + { + Assert.Equal(1, ti.Properties.Count); + JsonPropertyInfo propertyInfo = ti.Properties[0]; + propertyInfo.Get = null; + } + }); + + options.TypeInfoResolver = r; + + TestClassWithCustomConverterOnProperty obj = new() + { + MyClassProperty = new MyClass() { Value = "SomeValue" }, + }; + + string json = JsonSerializer.Serialize(obj, options); + Assert.Equal("{}", json); + + json = """{"MyClassProperty":"SomeValue"}"""; + TestClassWithCustomConverterOnProperty deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(obj.MyClassProperty.Value, deserialized.MyClassProperty.Value); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void JsonPropertyInfoGetIsRespected(bool useCustomConverter) + { + TestClassWithCustomConverterOnProperty obj = new() + { + MyClassProperty = new MyClass() { Value = "SomeValue" }, + }; + + MyClass substitutedValue = new MyClass() { Value = "SomeOtherValue" }; + + bool getterCalled = false; + + JsonSerializerOptions options = new(); + DefaultJsonTypeInfoResolver r = new(); + r.Modifiers.Add(ti => + { + if (ti.Type == typeof(TestClassWithCustomConverterOnProperty)) + { + Assert.Equal(1, ti.Properties.Count); + JsonPropertyInfo propertyInfo = ti.Properties[0]; + if (!useCustomConverter) + { + propertyInfo.CustomConverter = null; + } + + propertyInfo.Get = (o) => + { + Assert.Same(obj, o); + Assert.False(getterCalled); + getterCalled = true; + return substitutedValue; + }; + } + }); + + options.TypeInfoResolver = r; + + string json = JsonSerializer.Serialize(obj, options); + if (useCustomConverter) + { + Assert.Equal("""{"MyClassProperty":"SomeOtherValue"}""", json); + } + else + { + Assert.Equal("""{"MyClassProperty":{"Value":"SomeOtherValue","Thing":null}}""", json); + } + + TestClassWithCustomConverterOnProperty deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(substitutedValue.Value, deserialized.MyClassProperty.Value); + + Assert.True(getterCalled); + } + + [Fact] + public static void JsonPropertyInfoSetIsNullAndMutableWhenUsingCreateJsonPropertyInfo() + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(TestClassWithCustomConverterOnProperty), options); + JsonPropertyInfo propertyInfo = typeInfo.CreateJsonPropertyInfo(typeof(MyClass), "test"); + Assert.Null(propertyInfo.Set); + Action set = (obj, val) => + { + throw new NotImplementedException(); + }; + + propertyInfo.Set = set; + Assert.Same(set, propertyInfo.Set); + } + + [Fact] + public static void JsonPropertyInfoSetIsNotNullForDefaultResolver() + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(TestClassWithCustomConverterOnProperty), options); + Assert.Equal(1, typeInfo.Properties.Count); + JsonPropertyInfo propertyInfo = typeInfo.Properties[0]; + + Assert.NotNull(propertyInfo.Set); + + TestClassWithCustomConverterOnProperty obj = new(); + + MyClass value = new MyClass(); + propertyInfo.Set(obj, value); + Assert.Same(value, obj.MyClassProperty); + + MyClass sentinel = new(); + Action set = (o, value) => + { + Assert.Same(obj, o); + Assert.Same(sentinel, value); + obj.MyClassProperty = sentinel; + }; + + propertyInfo.Set = set; + Assert.Same(set, propertyInfo.Set); + + propertyInfo.Set(obj, sentinel); + Assert.Same(obj.MyClassProperty, sentinel); + } + + [Fact] + public static void JsonPropertyInfoSetPropertyDeserializableButNotSerializableWhenNull() + { + JsonSerializerOptions options = new(); + DefaultJsonTypeInfoResolver r = new(); + r.Modifiers.Add(ti => + { + if (ti.Type == typeof(TestClassWithCustomConverterOnProperty)) + { + Assert.Equal(1, ti.Properties.Count); + JsonPropertyInfo propertyInfo = ti.Properties[0]; + Assert.NotNull(propertyInfo.Set); + propertyInfo.Set = null; + Assert.Null(propertyInfo.Set); + } + }); + + options.TypeInfoResolver = r; + + TestClassWithCustomConverterOnProperty obj = new() + { + MyClassProperty = new MyClass() { Value = "SomeValue" }, + }; + + string json = JsonSerializer.Serialize(obj, options); + Assert.Equal("""{"MyClassProperty":"SomeValue"}""", json); + + TestClassWithCustomConverterOnProperty deserialized = JsonSerializer.Deserialize(json, options); + Assert.Null(deserialized.MyClassProperty); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void JsonPropertyInfoSetIsRespected(bool useCustomConverter) + { + TestClassWithCustomConverterOnProperty obj = new() + { + MyClassProperty = new MyClass() { Value = "SomeValue" }, + }; + + MyClass substitutedValue = new MyClass() { Value = "SomeOtherValue" }; + bool setterCalled = false; + + JsonSerializerOptions options = new(); + DefaultJsonTypeInfoResolver r = new(); + r.Modifiers.Add(ti => + { + if (ti.Type == typeof(TestClassWithCustomConverterOnProperty)) + { + Assert.Equal(1, ti.Properties.Count); + JsonPropertyInfo propertyInfo = ti.Properties[0]; + if (!useCustomConverter) + { + propertyInfo.CustomConverter = null; + } + + Assert.NotNull(propertyInfo.Set); + + Action setter = (o, val) => + { + var testClass = (TestClassWithCustomConverterOnProperty)o; + Assert.IsType(val); + MyClass myClass = (MyClass)val; + Assert.Equal(obj.MyClassProperty.Value, myClass.Value); + + testClass.MyClassProperty = substitutedValue; + Assert.False(setterCalled); + setterCalled = true; + }; + + propertyInfo.Set = setter; + Assert.Same(setter, propertyInfo.Set); + } + }); + + options.TypeInfoResolver = r; + + string json = JsonSerializer.Serialize(obj, options); + if (useCustomConverter) + { + Assert.Equal("""{"MyClassProperty":"SomeValue"}""", json); + } + else + { + Assert.Equal("""{"MyClassProperty":{"Value":"SomeValue","Thing":null}}""", json); + } + + TestClassWithCustomConverterOnProperty deserialized = JsonSerializer.Deserialize(json, options); + Assert.Same(substitutedValue, deserialized.MyClassProperty); + Assert.True(setterCalled); + } + + [Fact] + public static void AddingNumberHandlingToPropertyIsRespected() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClassWithNumber)) + { + Assert.Equal(1, ti.Properties.Count); + Assert.Null(ti.Properties[0].NumberHandling); + ti.Properties[0].NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString; + Assert.Equal(JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString, ti.Properties[0].NumberHandling); + } + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = resolver; + + TestClassWithNumber obj = new() + { + IntProperty = 37, + }; + + string json = JsonSerializer.Serialize(obj, o); + Assert.Equal("""{"IntProperty":"37"}""", json); + + TestClassWithNumber deserialized = JsonSerializer.Deserialize(json, o); + Assert.Equal(obj.IntProperty, deserialized.IntProperty); + } + + private class TestClassWithNumber + { + public int IntProperty { get; set; } + } + + [Theory] + [InlineData(null)] + [InlineData(JsonNumberHandling.Strict)] + public static void RemovingOrChangingNumberHandlingFromPropertyIsRespected(JsonNumberHandling? numberHandling) + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClassWithNumberHandlingOnProperty)) + { + Assert.Equal(1, ti.Properties.Count); + Assert.Equal(JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString, ti.Properties[0].NumberHandling); + ti.Properties[0].NumberHandling = numberHandling; + Assert.Equal(numberHandling, ti.Properties[0].NumberHandling); + } + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = resolver; + + TestClassWithNumberHandlingOnProperty obj = new() + { + IntProperty = 37, + }; + + string json = JsonSerializer.Serialize(obj, o); + Assert.Equal("""{"IntProperty":37}""", json); + + TestClassWithNumberHandlingOnProperty deserialized = JsonSerializer.Deserialize(json, o); + Assert.Equal(obj.IntProperty, deserialized.IntProperty); + } + + private class TestClassWithNumberHandlingOnProperty + { + [JsonNumberHandling(JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString)] + public int IntProperty { get; set; } + } + + [Fact] + public static void NumberHandlingFromTypeDoesntFlowToPropertyAndOverrideIsRespected() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClassWithNumberHandling)) + { + Assert.Equal(1, ti.Properties.Count); + Assert.Null(ti.Properties[0].NumberHandling); + ti.Properties[0].NumberHandling = JsonNumberHandling.Strict; + Assert.Equal(JsonNumberHandling.Strict, ti.Properties[0].NumberHandling); + } + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = resolver; + + TestClassWithNumberHandling obj = new() + { + IntProperty = 37, + }; + + string json = JsonSerializer.Serialize(obj, o); + Assert.Equal("""{"IntProperty":37}""", json); + + TestClassWithNumberHandling deserialized = JsonSerializer.Deserialize(json, o); + Assert.Equal(obj.IntProperty, deserialized.IntProperty); + } + + [JsonNumberHandling(JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString)] + private class TestClassWithNumberHandling + { + public int IntProperty { get; set; } + } + + [Fact] + public static void NumberHandlingFromOptionsDoesntFlowToPropertyAndOverrideIsRespected() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClassWithNumber)) + { + Assert.Null(ti.Properties[0].NumberHandling); + ti.Properties[0].NumberHandling = JsonNumberHandling.Strict; + Assert.Equal(JsonNumberHandling.Strict, ti.Properties[0].NumberHandling); + } + }); + + JsonSerializerOptions o = new(); + o.NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString; + o.TypeInfoResolver = resolver; + + TestClassWithNumber obj = new() + { + IntProperty = 37, + }; + + string json = JsonSerializer.Serialize(obj, o); + Assert.Equal("""{"IntProperty":37}""", json); + + TestClassWithNumber deserialized = JsonSerializer.Deserialize(json, o); + Assert.Equal(obj.IntProperty, deserialized.IntProperty); + } + + [Fact] + public static void ShouldSerializeShouldReportBackAssignedValue() + { + JsonSerializerOptions o = new(); + + JsonTypeInfo ti = JsonTypeInfo.CreateJsonTypeInfo(typeof(MyClass), o); + JsonPropertyInfo pi = ti.CreateJsonPropertyInfo(typeof(string), "test"); + + Assert.Null(pi.ShouldSerialize); + + Func value = (o, val) => throw new NotImplementedException(); + pi.ShouldSerialize = value; + Assert.Same(value, pi.ShouldSerialize); + + pi.ShouldSerialize = null; + Assert.Null(pi.ShouldSerialize); + } + + [Fact] + public static void AddingShouldSerializeToPropertyIsRespected() + { + TestClassWithNumber obj = new() + { + IntProperty = 3, + }; + + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClassWithNumber)) + { + Assert.Equal(1, ti.Properties.Count); + Assert.Null(ti.Properties[0].ShouldSerialize); + ti.Properties[0].ShouldSerialize = (o, val) => + { + Assert.Same(obj, o); + int intValue = (int)val; + Assert.Equal(obj.IntProperty, intValue); + return intValue != 3; + }; + } + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = resolver; + + string json = JsonSerializer.Serialize(obj, o); + Assert.Equal("{}", json); + + obj.IntProperty = 37; + json = JsonSerializer.Serialize(obj, o); + Assert.Equal("""{"IntProperty":37}""", json); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void RemovingOrChangingShouldSerializeFromPropertyWithIgnoreConditionIsRespected(bool removeShouldSerialize) + { + TestClassWithNumberAndIgnoreConditionOnProperty obj = new() + { + IntProperty = 37, + }; + + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClassWithNumberAndIgnoreConditionOnProperty)) + { + Assert.Equal(1, ti.Properties.Count); + Assert.NotNull(ti.Properties[0].ShouldSerialize); + Assert.False(ti.Properties[0].ShouldSerialize(null, 0)); + Assert.True(ti.Properties[0].ShouldSerialize(null, 1)); + Assert.True(ti.Properties[0].ShouldSerialize(null, -1)); + Assert.True(ti.Properties[0].ShouldSerialize(null, 3)); + + if (removeShouldSerialize) + { + ti.Properties[0].ShouldSerialize = null; + } + else + { + ti.Properties[0].ShouldSerialize = (o, val) => + { + Assert.Same(obj, o); + int intValue = (int)val; + Assert.Equal(obj.IntProperty, intValue); + return intValue != 3; + }; + } + } + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = resolver; + + string json = JsonSerializer.Serialize(obj, o); + Assert.Equal("""{"IntProperty":37}""", json); + + obj.IntProperty = default; + json = JsonSerializer.Serialize(obj, o); + Assert.Equal("""{"IntProperty":0}""", json); + + obj.IntProperty = 3; + json = JsonSerializer.Serialize(obj, o); + if (removeShouldSerialize) + { + Assert.Equal("""{"IntProperty":3}""", json); + } + else + { + Assert.Equal("{}", json); + } + } + + private class TestClassWithNumberAndIgnoreConditionOnProperty + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int IntProperty { get; set; } + } + + [Fact] + public static void DefaultIgnoreConditionFromOptionsDoesntFlowToShouldSerializePropertyAndOverrideIsRespected() + { + TestClassWithNumber obj = new() + { + IntProperty = 37, + }; + + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClassWithNumber)) + { + Assert.Equal(1, ti.Properties.Count); + Assert.Null(ti.Properties[0].ShouldSerialize); + ti.Properties[0].ShouldSerialize = (o, val) => + { + Assert.Same(obj, o); + int intValue = (int)val; + Assert.Equal(obj.IntProperty, intValue); + return intValue != 3; + }; + } + }); + + JsonSerializerOptions o = new(); + o.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault; + o.TypeInfoResolver = resolver; + + string json = JsonSerializer.Serialize(obj, o); + Assert.Equal("""{"IntProperty":37}""", json); + + obj.IntProperty = default; + json = JsonSerializer.Serialize(obj, o); + Assert.Equal("""{"IntProperty":0}""", json); + + obj.IntProperty = 3; + json = JsonSerializer.Serialize(obj, o); + Assert.Equal("{}", json); + } + + [Fact] + public static void DefaultIgnoreConditionFromOptionsIsRespectedWhenShouldSerializePropertyIsAssignedAndCleared() + { + TestClassWithNumberAndIgnoreConditionOnProperty obj = new() + { + IntProperty = 37, + }; + + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClassWithNumber)) + { + Assert.Equal(1, ti.Properties.Count); + Assert.Null(ti.Properties[0].ShouldSerialize); + ti.Properties[0].ShouldSerialize = (o, val) => + { + Assert.Same(obj, o); + int intValue = (int)val; + Assert.Equal(obj.IntProperty, intValue); + return intValue != 3; + }; + + ti.Properties[0].ShouldSerialize = null; + } + }); + + JsonSerializerOptions o = new(); + o.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault; + o.TypeInfoResolver = resolver; + + string json = JsonSerializer.Serialize(obj, o); + Assert.Equal("""{"IntProperty":37}""", json); + + obj.IntProperty = default; + json = JsonSerializer.Serialize(obj, o); + Assert.Equal("{}", json); + + obj.IntProperty = 3; + json = JsonSerializer.Serialize(obj, o); + Assert.Equal("""{"IntProperty":3}""", json); + } + + public enum ModifyJsonIgnore + { + DontModify, + NeverSerialize, + AlwaysSerialize, + DontSerializeNumber3OrStringAsd, + } + + [Theory] + [InlineData(JsonIgnoreCondition.WhenWritingDefault, ModifyJsonIgnore.DontModify)] + [InlineData(JsonIgnoreCondition.WhenWritingNull, ModifyJsonIgnore.DontModify)] + [InlineData(JsonIgnoreCondition.Never, ModifyJsonIgnore.DontModify)] + [InlineData(JsonIgnoreCondition.WhenWritingDefault, ModifyJsonIgnore.NeverSerialize)] + [InlineData(JsonIgnoreCondition.WhenWritingNull, ModifyJsonIgnore.NeverSerialize)] + [InlineData(JsonIgnoreCondition.Never, ModifyJsonIgnore.NeverSerialize)] + [InlineData(JsonIgnoreCondition.WhenWritingDefault, ModifyJsonIgnore.AlwaysSerialize)] + [InlineData(JsonIgnoreCondition.WhenWritingNull, ModifyJsonIgnore.AlwaysSerialize)] + [InlineData(JsonIgnoreCondition.Never, ModifyJsonIgnore.AlwaysSerialize)] + [InlineData(JsonIgnoreCondition.WhenWritingDefault, ModifyJsonIgnore.DontSerializeNumber3OrStringAsd)] + [InlineData(JsonIgnoreCondition.WhenWritingNull, ModifyJsonIgnore.DontSerializeNumber3OrStringAsd)] + [InlineData(JsonIgnoreCondition.Never, ModifyJsonIgnore.DontSerializeNumber3OrStringAsd)] + public static void JsonIgnoreConditionIsCorrectlyTranslatedToShouldSerializeDelegateAndChangingShouldSerializeIsRespected(JsonIgnoreCondition defaultIgnoreCondition, ModifyJsonIgnore modify) + { + TestClassWithEveryPossibleJsonIgnore obj = new() + { + AlwaysProperty = "Always", + WhenWritingDefaultProperty = 37, + WhenWritingNullProperty = "WhenWritingNull", + NeverProperty = "Never", + Property = "None", + }; + + // sanity check + bool modifierTestRun = false; + + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add(ti => + { + if (ti.Type != typeof(TestClassWithEveryPossibleJsonIgnore)) + return; + + Assert.Equal(5, ti.Properties.Count); + Assert.False(modifierTestRun); + modifierTestRun = true; + foreach (var property in ti.Properties) + { + string jsonIgnoreValue = property.Name.Substring(0, property.Name.Length - "Property".Length); + JsonIgnoreCondition? ignoreConditionOnProperty = string.IsNullOrEmpty(jsonIgnoreValue) ? null : (JsonIgnoreCondition)Enum.Parse(typeof(JsonIgnoreCondition), jsonIgnoreValue); + TestJsonIgnoreConditionDelegate(defaultIgnoreCondition, ignoreConditionOnProperty, property, modify); + } + }); + + JsonSerializerOptions options = new(); + options.TypeInfoResolver = resolver; + options.DefaultIgnoreCondition = defaultIgnoreCondition; + + + // - delegate correctly returns value + // - nulling out delegate removes behavior + // - check every options default + string json = JsonSerializer.Serialize(obj, options); + Assert.True(modifierTestRun); + + switch (modify) + { + case ModifyJsonIgnore.DontModify: + Assert.Equal("""{"WhenWritingDefaultProperty":37,"WhenWritingNullProperty":"WhenWritingNull","NeverProperty":"Never","Property":"None"}""", json); + break; + case ModifyJsonIgnore.NeverSerialize: + Assert.Equal("{}", json); + break; + case ModifyJsonIgnore.AlwaysSerialize: + case ModifyJsonIgnore.DontSerializeNumber3OrStringAsd: + Assert.Equal("""{"AlwaysProperty":"Always","WhenWritingDefaultProperty":37,"WhenWritingNullProperty":"WhenWritingNull","NeverProperty":"Never","Property":"None"}""", json); + break; + } + + obj.AlwaysProperty = default; + obj.WhenWritingDefaultProperty = default; + obj.WhenWritingNullProperty = default; + obj.NeverProperty = default; + obj.Property = default; + + json = JsonSerializer.Serialize(obj, options); + + switch (modify) + { + case ModifyJsonIgnore.DontModify: + { + string noJsonIgnoreProperty = defaultIgnoreCondition == JsonIgnoreCondition.Never ? @",""Property"":null" : null; + Assert.Equal($@"{{""NeverProperty"":null{noJsonIgnoreProperty}}}", json); + break; + } + case ModifyJsonIgnore.NeverSerialize: + Assert.Equal("{}", json); + break; + case ModifyJsonIgnore.AlwaysSerialize: + case ModifyJsonIgnore.DontSerializeNumber3OrStringAsd: + Assert.Equal("""{"AlwaysProperty":null,"WhenWritingDefaultProperty":0,"WhenWritingNullProperty":null,"NeverProperty":null,"Property":null}""", json); + break; + } + + obj.AlwaysProperty = "asd"; + obj.WhenWritingDefaultProperty = 3; + obj.WhenWritingNullProperty = "asd"; + obj.NeverProperty = "asd"; + obj.Property = "asd"; + + json = JsonSerializer.Serialize(obj, options); + + switch (modify) + { + case ModifyJsonIgnore.DontModify: + Assert.Equal("""{"WhenWritingDefaultProperty":3,"WhenWritingNullProperty":"asd","NeverProperty":"asd","Property":"asd"}""", json); + break; + case ModifyJsonIgnore.AlwaysSerialize: + Assert.Equal("""{"AlwaysProperty":"asd","WhenWritingDefaultProperty":3,"WhenWritingNullProperty":"asd","NeverProperty":"asd","Property":"asd"}""", json); + break; + case ModifyJsonIgnore.NeverSerialize: + case ModifyJsonIgnore.DontSerializeNumber3OrStringAsd: + Assert.Equal("{}", json); + break; + } + + static void TestJsonIgnoreConditionDelegate(JsonIgnoreCondition defaultIgnoreCondition, JsonIgnoreCondition? ignoreConditionOnProperty, JsonPropertyInfo property, ModifyJsonIgnore modify) + { + // defaultIgnoreCondition is not taken into accound, we might expect null if defaultIgnoreCondition == ignoreConditionOnProperty + switch (ignoreConditionOnProperty) + { + case null: + Assert.Null(property.ShouldSerialize); + break; + case JsonIgnoreCondition.Always: + Assert.NotNull(property.ShouldSerialize); + Assert.False(property.ShouldSerialize(null, null)); + Assert.False(property.ShouldSerialize(null, "")); + Assert.False(property.ShouldSerialize(null, "asd")); + + Assert.Null(property.Get); + Assert.Null(property.Set); + break; + case JsonIgnoreCondition.WhenWritingDefault: + Assert.NotNull(property.ShouldSerialize); + Assert.False(property.ShouldSerialize(null, 0)); + Assert.True(property.ShouldSerialize(null, 1)); + Assert.True(property.ShouldSerialize(null, -1)); + break; + case JsonIgnoreCondition.WhenWritingNull: + Assert.NotNull(property.ShouldSerialize); + Assert.False(property.ShouldSerialize(null, null)); + Assert.True(property.ShouldSerialize(null, "")); + Assert.True(property.ShouldSerialize(null, "asd")); + break; + case JsonIgnoreCondition.Never: + Assert.NotNull(property.ShouldSerialize); + Assert.True(property.ShouldSerialize(null, null)); + Assert.True(property.ShouldSerialize(null, "")); + Assert.True(property.ShouldSerialize(null, "asd")); + break; + } + + if (modify != ModifyJsonIgnore.DontModify && ignoreConditionOnProperty == JsonIgnoreCondition.Always) + { + property.Get = (o) => ((TestClassWithEveryPossibleJsonIgnore)o).AlwaysProperty; + } + + switch (modify) + { + case ModifyJsonIgnore.AlwaysSerialize: + property.ShouldSerialize = (o, v) => true; + break; + case ModifyJsonIgnore.NeverSerialize: + property.ShouldSerialize = (o, v) => false; + break; + case ModifyJsonIgnore.DontSerializeNumber3OrStringAsd: + property.ShouldSerialize = (o, v) => + { + if (v is null) + { + return true; + } + else if (v is int intVal) + { + return intVal != 3; + } + else if (v is string stringVal) + { + return stringVal != "asd"; + } + + Assert.Fail("ShouldSerialize set for value which is not int or string"); + return false; + }; + break; + } + } + } + + private class TestClassWithEveryPossibleJsonIgnore + { + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public string AlwaysProperty { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int WhenWritingDefaultProperty { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string WhenWritingNullProperty { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string NeverProperty { get; set; } + + public string Property { get; set; } + } + + private class TestClassWithProperty + { + public MyClass MyClassProperty { get; set; } + } + + private class TestClassWithCustomConverterOnProperty + { + [JsonConverter(typeof(MyClassConverterOriginal))] + public MyClass MyClassProperty { get; set; } + } + + private class TestClassWithCustomConverterFactoryOnProperty + { + [JsonConverter(typeof(MyClassCustomConverterFactory))] + public MyClass MyClassProperty { get; set; } + } + + private class MyClassConverterOriginal : JsonConverter + { + public override MyClass? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + throw new InvalidOperationException($"Wrong token type: {reader.TokenType}"); + + MyClass myClass = new MyClass(); + myClass.Value = reader.GetString(); + return myClass; + } + + public override void Write(Utf8JsonWriter writer, MyClass value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + } + + private class MyClassCustomConverter : JsonConverter + { + private string _prefix; + + public MyClassCustomConverter(string prefix) + { + _prefix = prefix; + } + + public override MyClass? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + throw new InvalidOperationException($"Wrong token type: {reader.TokenType}"); + + MyClass myClass = new MyClass(); + myClass.Value = reader.GetString().Substring(_prefix.Length); + return myClass; + } + + public override void Write(Utf8JsonWriter writer, MyClass value, JsonSerializerOptions options) + { + writer.WriteStringValue(_prefix + value.Value); + } + } + + private class MyClassCustomConverterFactory : JsonConverterFactory + { + internal JsonConverter ConverterInstance { get; } = new MyClassCustomConverter("test_"); + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(MyClass); + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Assert.Equal(typeof(MyClass), typeToConvert); + return ConverterInstance; + } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs new file mode 100644 index 0000000000000..d46978ee314fb --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs @@ -0,0 +1,830 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json.Tests; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class DefaultJsonTypeInfoResolverTests + { + [Theory] + [InlineData(typeof(object))] + [InlineData(typeof(int))] + [InlineData(typeof(string))] + [InlineData(typeof(SomeClass))] + [InlineData(typeof(StructWithFourArgs))] + [InlineData(typeof(Dictionary))] + [InlineData(typeof(DictionaryWrapper))] + [InlineData(typeof(List))] + [InlineData(typeof(ListWrapper))] + public static void TypeInfoPropertiesDefaults(Type type) + { + bool usingParametrizedConstructor = type.GetConstructors() + .FirstOrDefault(ctor => ctor.GetParameters().Length != 0 && ctor.GetCustomAttribute() != null) != null; + + DefaultJsonTypeInfoResolver r = new(); + JsonSerializerOptions o = new(); + o.Converters.Add(new CustomThrowingConverter()); + + JsonTypeInfo ti = r.GetTypeInfo(type, o); + + Assert.Same(o, ti.Options); + Assert.NotNull(ti.Properties); + + if (ti.Kind == JsonTypeInfoKind.Object && usingParametrizedConstructor) + { + Assert.Null(ti.CreateObject); + Func createObj = () => Activator.CreateInstance(type); + ti.CreateObject = createObj; + Assert.Same(createObj, ti.CreateObject); + } + else if (ti.Kind == JsonTypeInfoKind.None) + { + Assert.Null(ti.CreateObject); + Assert.Throws(() => ti.CreateObject = () => Activator.CreateInstance(type)); + } + else + { + Assert.NotNull(ti.CreateObject); + Func createObj = () => Activator.CreateInstance(type); + ti.CreateObject = createObj; + Assert.Same(createObj, ti.CreateObject); + } + + JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), "foo"); + Assert.NotNull(property); + + if (ti.Kind == JsonTypeInfoKind.Object) + { + Assert.InRange(ti.Properties.Count, 1, 10); + Assert.False(ti.Properties.IsReadOnly); + ti.Properties.Add(property); + ti.Properties.Remove(property); + } + else + { + Assert.Equal(0, ti.Properties.Count); + Assert.True(ti.Properties.IsReadOnly); + Assert.Throws(() => ti.Properties.Add(property)); + Assert.Throws(() => ti.Properties.Insert(0, property)); + Assert.Throws(() => ti.Properties.Clear()); + } + + Assert.Null(ti.NumberHandling); + JsonNumberHandling numberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString; + ti.NumberHandling = numberHandling; + Assert.Equal(numberHandling, ti.NumberHandling); + + InvokeGeneric(type, nameof(TypeInfoPropertiesDefaults_Generic), ti); + } + + private static void TypeInfoPropertiesDefaults_Generic(JsonTypeInfo ti) + { + if (ti.Kind == JsonTypeInfoKind.None) + { + Assert.Null(ti.CreateObject); + Assert.Throws(() => ti.CreateObject = () => (T)Activator.CreateInstance(typeof(T))); + } + else + { + bool createObjCalled = false; + Assert.NotNull(ti.CreateObject); + Func createObj = () => + { + createObjCalled = true; + return default(T); + }; + + ti.CreateObject = createObj; + Assert.Same(createObj, ti.CreateObject); + + JsonTypeInfo untyped = ti; + if (typeof(T).IsValueType) + { + Assert.NotSame(createObj, untyped.CreateObject); + } + else + { + Assert.Same(createObj, untyped.CreateObject); + } + + Assert.Same(untyped.CreateObject, untyped.CreateObject); + Assert.Same(createObj, ti.CreateObject); + untyped.CreateObject(); + Assert.True(createObjCalled); + + ti.CreateObject = null; + Assert.Null(ti.CreateObject); + Assert.Null(untyped.CreateObject); + + bool untypedCreateObjCalled = false; + Func untypedCreateObj = () => + { + untypedCreateObjCalled = true; + return default(T); + }; + untyped.CreateObject = untypedCreateObj; + Assert.Same(untypedCreateObj, untyped.CreateObject); + Assert.Same(ti.CreateObject, ti.CreateObject); + Assert.NotSame(untypedCreateObj, ti.CreateObject); + + ti.CreateObject(); + Assert.True(untypedCreateObjCalled); + + untyped.CreateObject = null; + Assert.Null(ti.CreateObject); + Assert.Null(untyped.CreateObject); + } + } + + [Fact] + public static void TypeInfoKindNoneNumberHandlingDirect() + { + DefaultJsonTypeInfoResolver r = new(); + r.Modifiers.Add((ti) => + { + if (ti.Type == typeof(int)) + { + ti.NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString; + } + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = r; + + string json = JsonSerializer.Serialize(13, o); + Assert.Equal(@"""13""", json); + + var deserialized = JsonSerializer.Deserialize(json, o); + Assert.Equal(13, deserialized); + } + + [Fact] + public static void TypeInfoKindNoneNumberHandlingDirectThroughObject() + { + DefaultJsonTypeInfoResolver r = new(); + r.Modifiers.Add((ti) => + { + if (ti.Type == typeof(int)) + { + ti.NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString; + } + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = r; + + string json = JsonSerializer.Serialize(13, o); + Assert.Equal(@"""13""", json); + + var deserialized = JsonSerializer.Deserialize(json, o); + Assert.Equal("13", ((JsonElement)deserialized).GetString()); + } + + [Fact] + public static void TypeInfoKindNoneNumberHandling() + { + DefaultJsonTypeInfoResolver r = new(); + r.Modifiers.Add((ti) => + { + if (ti.Type == typeof(int) || ti.Type == typeof(object)) + { + ti.NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString; + } + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = r; + + SomeClass testObj = new SomeClass() + { + ObjProp = 45, + IntProp = 13, + }; + + string json = JsonSerializer.Serialize(testObj, o); + Assert.Equal(@"{""ObjProp"":""45"",""IntProp"":""13""}", json); + + var deserialized = JsonSerializer.Deserialize(json, o); + Assert.Equal(testObj.ObjProp.ToString(), ((JsonElement)deserialized.ObjProp).GetString()); + Assert.Equal(testObj.IntProp, deserialized.IntProp); + } + + [Fact] + public static void RecursiveTypeNumberHandling() + { + DefaultJsonTypeInfoResolver r = new(); + r.Modifiers.Add((ti) => + { + if (ti.Type == typeof(SomeRecursiveClass)) + { + ti.NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString; + } + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = r; + + SomeRecursiveClass testObj = new SomeRecursiveClass() + { + IntProp = 13, + RecursiveProperty = new SomeRecursiveClass() + { + IntProp = 14, + }, + }; + + string json = JsonSerializer.Serialize(testObj, o); + Assert.Equal(@"{""IntProp"":""13"",""RecursiveProperty"":{""IntProp"":""14"",""RecursiveProperty"":null}}", json); + + var deserialized = JsonSerializer.Deserialize(json, o); + Assert.Equal(testObj.IntProp, deserialized.IntProp); + Assert.NotNull(testObj.RecursiveProperty); + Assert.Equal(testObj.RecursiveProperty.IntProp, deserialized.RecursiveProperty.IntProp); + Assert.Null(testObj.RecursiveProperty.RecursiveProperty); + } + + [Theory] + [InlineData(typeof(SomeClass), typeof(object))] + [InlineData(typeof(object), typeof(string))] + [InlineData(typeof(object), typeof(int))] + [InlineData(typeof(string), typeof(int))] + [InlineData(typeof(int), typeof(string))] + [InlineData(typeof(int), typeof(double))] + public static void TypeInfoOfWrongTypeOnObject(Type expectedType, Type actualType) + { + DefaultJsonTypeInfoResolver dr = new(); + TestResolver r = new((type, options) => + { + if (type == expectedType) + { + return dr.GetTypeInfo(actualType, options); + } + + return dr.GetTypeInfo(type, options); + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = r; + + SomeClass testObj = new() + { + ObjProp = "test", + }; + + Assert.Throws(() => JsonSerializer.Serialize(testObj, o)); + } + + [Fact] + public static void TypeInfoOfWrongOptions() + { + JsonSerializerOptions wrongOptions = new(); + DefaultJsonTypeInfoResolver dr = new(); + TestResolver r = new((type, options) => + { + if (type == typeof(int)) + { + return dr.GetTypeInfo(type, wrongOptions); + } + + return dr.GetTypeInfo(type, options); + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = r; + + SomeClass testObj = new() + { + IntProp = 17, + }; + + Assert.Throws(() => JsonSerializer.Serialize(testObj, o)); + } + + [Theory] + [InlineData(typeof(SomeClass), typeof(object))] + [InlineData(typeof(object), typeof(string))] + [InlineData(typeof(object), typeof(int))] + [InlineData(typeof(int), typeof(string))] + [InlineData(typeof(int), typeof(double))] + public static void TypeInfoOfWrongTypeDirectCall(Type expectedType, Type actualType) + { + DefaultJsonTypeInfoResolver dr = new(); + TestResolver r = new((type, options) => + { + if (type == expectedType) + { + return dr.GetTypeInfo(actualType, options); + } + + return dr.GetTypeInfo(type, options); + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = r; + + object testObj = Activator.CreateInstance(expectedType); + + Assert.Throws(() => JsonSerializer.Serialize(testObj, expectedType, o)); + } + + [Theory] + [MemberData(nameof(GetTypeInfoTestData))] + public static void TypeInfoIsImmutableAfterFirstUsage(T testObj) + { + JsonTypeInfo untyped = null; + DefaultJsonTypeInfoResolver dr = new(); + TestResolver r = new((typeToResolve, options) => + { + var ret = dr.GetTypeInfo(typeToResolve, options); + if (typeToResolve == typeof(T)) + { + Assert.Null(untyped); + untyped = ret; + } + + return ret; + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = r; + + Assert.NotNull(JsonSerializer.Serialize(testObj, typeof(T), o)); + Assert.NotNull(untyped); + + JsonTypeInfo typeInfo = (JsonTypeInfo)untyped; + + if (typeInfo.Kind == JsonTypeInfoKind.None) + { + Assert.Null(typeInfo.CreateObject); + Assert.Null(untyped.CreateObject); + } + else + { + Assert.NotNull(typeInfo.CreateObject); + Assert.NotNull(untyped.CreateObject); + } + + Assert.Null(typeInfo.NumberHandling); + + TestTypeInfoImmutability(typeInfo); + } + + private static void TestTypeInfoImmutability(JsonTypeInfo typeInfo) + { + JsonTypeInfo untyped = typeInfo; + Assert.Equal(typeof(T), typeInfo.Type); + Assert.True(typeInfo.Converter.CanConvert(typeof(T))); + + JsonPropertyInfo prop = typeInfo.CreateJsonPropertyInfo(typeof(string), "foo"); + Assert.Throws(() => untyped.CreateObject = untyped.CreateObject); + Assert.Throws(() => typeInfo.CreateObject = typeInfo.CreateObject); + Assert.Throws(() => typeInfo.NumberHandling = typeInfo.NumberHandling); + Assert.Throws(() => typeInfo.Properties.Clear()); + Assert.Throws(() => typeInfo.Properties.Add(prop)); + Assert.Throws(() => typeInfo.Properties.Insert(0, prop)); + + foreach (var property in typeInfo.Properties) + { + Assert.NotNull(property.PropertyType); + Assert.Null(property.CustomConverter); + Assert.NotNull(property.Name); + Assert.NotNull(property.Get); + Assert.NotNull(property.Set); + Assert.Null(property.ShouldSerialize); + Assert.Null(typeInfo.NumberHandling); + + Assert.Throws(() => property.CustomConverter = property.CustomConverter); + Assert.Throws(() => property.Name = property.Name); + Assert.Throws(() => property.Get = property.Get); + Assert.Throws(() => property.Set = property.Set); + Assert.Throws(() => property.ShouldSerialize = property.ShouldSerialize); + Assert.Throws(() => property.NumberHandling = property.NumberHandling); + } + } + + [Theory] + [InlineData(typeof(object), JsonTypeInfoKind.None)] + [InlineData(typeof(string), JsonTypeInfoKind.None)] + [InlineData(typeof(int), JsonTypeInfoKind.None)] + [InlineData(typeof(SomeRecursiveClass) /* custom converter */, JsonTypeInfoKind.None)] + [InlineData(typeof(SomeClass), JsonTypeInfoKind.Object)] + [InlineData(typeof(DefaultJsonTypeInfoResolverTests), JsonTypeInfoKind.Object)] + [InlineData(typeof(StructWithFourArgs), JsonTypeInfoKind.Object)] + [InlineData(typeof(Dictionary), JsonTypeInfoKind.Dictionary)] + [InlineData(typeof(DictionaryWrapper), JsonTypeInfoKind.Dictionary)] + [InlineData(typeof(List), JsonTypeInfoKind.Enumerable)] + [InlineData(typeof(ListWrapper), JsonTypeInfoKind.Enumerable)] + [InlineData(typeof(int[]), JsonTypeInfoKind.Enumerable)] + public static void JsonTypeInfoKindIsReportedCorrectly(Type type, JsonTypeInfoKind expectedJsonTypeInfoKind) + { + InvokeGeneric(type, nameof(JsonTypeInfoKindIsReportedCorrectly_Generic), expectedJsonTypeInfoKind); + } + + private static void JsonTypeInfoKindIsReportedCorrectly_Generic(JsonTypeInfoKind expectedJsonTypeInfoKind) + { + DefaultJsonTypeInfoResolver r = new(); + JsonSerializerOptions o = new(); + o.Converters.Add(new CustomThrowingConverter()); + JsonTypeInfo ti = r.GetTypeInfo(typeof(T), o); + Assert.Equal(expectedJsonTypeInfoKind, ti.Kind); + + ti = JsonTypeInfo.CreateJsonTypeInfo(typeof(T), o); + Assert.Equal(expectedJsonTypeInfoKind, ti.Kind); + + ti = JsonTypeInfo.CreateJsonTypeInfo(o); + Assert.Equal(expectedJsonTypeInfoKind, ti.Kind); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void JsonTypeInfoAddDuplicatedPropertyNames(bool ignoreDuplicatedProperty) + { + DefaultJsonTypeInfoResolver r = new(); + r.Modifiers.Add((ti) => + { + if (ti.Type == typeof(MyClass)) + { + JsonPropertyInfo prop = ti.CreateJsonPropertyInfo(typeof(uint), ti.Properties[0].Name); + uint valueHolder = 7; + + if (!ignoreDuplicatedProperty) + { + prop.Get = (o) => valueHolder; + prop.Set = (o, val) => valueHolder = (uint)val; + } + + ti.Properties.Add(prop); + } + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = r; + + MyClass obj = new() + { + Value = "foo", + }; + + Assert.Throws(() => JsonSerializer.Serialize(obj, o)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void JsonTypeInfoRenameToDuplicatePropertyNames(bool ignoreDuplicatedProperty) + { + DefaultJsonTypeInfoResolver r = new(); + r.Modifiers.Add((ti) => + { + if (ti.Type == typeof(MyClass)) + { + if (ignoreDuplicatedProperty) + { + ti.Properties[1].Get = null; + ti.Properties[1].Set = null; + } + + ti.Properties[1].Name = ti.Properties[0].Name; + } + }); + + JsonSerializerOptions o = new(); + o.TypeInfoResolver = r; + + MyClass obj = new() + { + Value = "foo", + }; + + Assert.Throws(() => JsonSerializer.Serialize(obj, o)); + } + + [Fact] + public static void AddJsonPropertyInfoCreatedFromDifferentJsonTypeInfoInstance() + { + DefaultJsonTypeInfoResolver resolver = new(); + JsonSerializerOptions options = new(); + JsonTypeInfo[] typeInfos = new[] + { + // we add double so that we check between instances of the same internal type as well + JsonTypeInfo.CreateJsonTypeInfo(options), + JsonTypeInfo.CreateJsonTypeInfo(options), + JsonTypeInfo.CreateJsonTypeInfo(options), + resolver.GetTypeInfo(typeof(SomeClass), options), + resolver.GetTypeInfo(typeof(SomeClass), options), + resolver.GetTypeInfo(typeof(SomeOtherClass), options), + ((IJsonTypeInfoResolver)new SomeClassContext()).GetTypeInfo(typeof(SomeClass), options), + ((IJsonTypeInfoResolver)new SomeClassContext()).GetTypeInfo(typeof(SomeClass), options), + ((IJsonTypeInfoResolver)new SomeClassContext()).GetTypeInfo(typeof(SomeOtherClass), options), + new SomeClassContext(options).SomeClass // this binds to options and therefore we cannot add more of these + }; + + foreach (var typeInfo1 in typeInfos) + { + foreach (var typeInfo2 in typeInfos) + { + if (ReferenceEquals(typeInfo1, typeInfo2)) + continue; + + Assert.Throws(() => typeInfo1.Properties.Add(typeInfo2.CreateJsonPropertyInfo(typeof(int), "test"))); + } + } + } + + [Fact] + public static void AddJsonPropertyInfoFromMetadataServices() + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo1 = JsonTypeInfo.CreateJsonTypeInfo(options); + JsonTypeInfo typeInfo2 = JsonTypeInfo.CreateJsonTypeInfo(options); + + JsonPropertyInfo propertyInfo = JsonMetadataServices.CreatePropertyInfo( + options, + new JsonPropertyInfoValues() + { + DeclaringType = typeof(SomeClass), + PropertyName = "test", + }); + + typeInfo1.Properties.Add(propertyInfo); + Assert.Equal(1, typeInfo1.Properties.Count); + Assert.Same(propertyInfo, typeInfo1.Properties[0]); + + Assert.Throws(() => typeInfo2.Properties.Add(propertyInfo)); + Assert.Equal(0, typeInfo2.Properties.Count); + } + + [Fact] + public static void AddingNullJsonPropertyInfoIsNotPossible() + { + JsonSerializerOptions options = new(); + JsonTypeInfo typeInfo = JsonTypeInfo.CreateJsonTypeInfo(options); + Assert.Throws(() => typeInfo.Properties.Add(null)); + Assert.Empty(typeInfo.Properties); + Assert.Throws(() => typeInfo.Properties.Insert(0, null)); + Assert.Empty(typeInfo.Properties); + + typeInfo.Properties.Add(typeInfo.CreateJsonPropertyInfo(typeof(int), "test")); + Assert.Throws(() => typeInfo.Properties[0] = null); + Assert.Equal(1, typeInfo.Properties.Count); + Assert.NotNull(typeInfo.Properties[0]); + } + + [Theory] + [InlineData(typeof(object))] + [InlineData(typeof(string))] + [InlineData(typeof(int))] + [InlineData(typeof(SomeRecursiveClass))] + [InlineData(typeof(SomeClass))] + [InlineData(typeof(DefaultJsonTypeInfoResolverTests))] + [InlineData(typeof(StructWithFourArgs))] + [InlineData(typeof(Dictionary))] + [InlineData(typeof(DictionaryWrapper))] + [InlineData(typeof(List))] + [InlineData(typeof(ListWrapper))] + [InlineData(typeof(int[]))] + public static void CreateJsonTypeInfo(Type type) + { + InvokeGeneric(type, nameof(CreateJsonTypeInfo_Generic)); + } + + private static void CreateJsonTypeInfo_Generic() + { + TestCreateJsonTypeInfo((o) => (JsonTypeInfo)JsonTypeInfo.CreateJsonTypeInfo(typeof(T), o)); + TestCreateJsonTypeInfo((o) => JsonTypeInfo.CreateJsonTypeInfo(o)); + + static void TestCreateJsonTypeInfo(Func> getTypeInfo) + { + JsonSerializerOptions o = new() { TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; + TestCreateJsonTypeInfoInstance(o, getTypeInfo(o)); + + o = new JsonSerializerOptions() { TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; + var conv = new DummyConverter(); + o.Converters.Add(conv); + JsonTypeInfo ti = getTypeInfo(o); + Assert.Same(conv, ti.Converter); + Assert.Equal(JsonTypeInfoKind.None, ti.Kind); + TestCreateJsonTypeInfoInstance(o, ti); + } + + static void TestCreateJsonTypeInfoInstance(JsonSerializerOptions o, JsonTypeInfo ti) + { + Assert.Equal(typeof(T), ti.Type); + Assert.NotNull(ti.Converter); + Assert.True(ti.Converter.CanConvert(typeof(T))); + + JsonSerializer.Serialize(default(T), ti); + + JsonTypeInfo untyped = ti; + Assert.Null(ti.CreateObject); + Assert.Null(untyped.CreateObject); + + TestTypeInfoImmutability(ti); + } + } + + public static IEnumerable GetTypeInfoTestData() + { + yield return new object[] { "test" }; + yield return new object[] { 13 }; + yield return new object[] { new SomeClass { IntProp = 17 } }; + yield return new object[] { new SomeRecursiveClass() }; + } + + [Fact] + public static void JsonConstructorAttributeIsOverriddenWhenCreateObjectIsSet() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add(ti => + { + if (ti.Type == typeof(ClassWithParametrizedConstructorAndReadOnlyProperties)) + { + Assert.Null(ti.CreateObject); + ti.CreateObject = () => new ClassWithParametrizedConstructorAndReadOnlyProperties(1, "test", dummyParam: true); + } + }); + + JsonSerializerOptions o = new() { TypeInfoResolver = resolver }; + string json = """{"A":2,"B":"foo"}"""; + var deserialized = JsonSerializer.Deserialize(json, o); + + Assert.NotNull(deserialized); + Assert.Equal(1, deserialized.A); + Assert.Equal("test", deserialized.B); + } + + private class ClassWithParametrizedConstructorAndReadOnlyProperties + { + public int A { get; } + public string B { get; } + + public ClassWithParametrizedConstructorAndReadOnlyProperties(int a, string b, bool dummyParam) + { + A = a; + B = b; + } + + [JsonConstructor] + public ClassWithParametrizedConstructorAndReadOnlyProperties(int a, string b) + { + Assert.Fail("this ctor should not be used"); + } + } + + [Fact] + public static void JsonConstructorAttributeIsOverridenAndPropertiesAreSetWhenCreateObjectIsSet() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add(ti => + { + if (ti.Type == typeof(ClassWithParametrizedConstructorAndWritableProperties)) + { + Assert.Null(ti.CreateObject); + ti.CreateObject = () => new ClassWithParametrizedConstructorAndWritableProperties(); + } + }); + + JsonSerializerOptions o = new() { TypeInfoResolver = resolver }; + + string json = """{"A":2,"B":"foo","C":"bar"}"""; + var deserialized = JsonSerializer.Deserialize(json, o); + + Assert.NotNull(deserialized); + Assert.Equal(2, deserialized.A); + Assert.Equal("foo", deserialized.B); + Assert.Equal("bar", deserialized.C); + } + + private class ClassWithParametrizedConstructorAndWritableProperties + { + public int A { get; set; } + public string B { get; set; } + public string C { get; set; } + + public ClassWithParametrizedConstructorAndWritableProperties() { } + + [JsonConstructor] + public ClassWithParametrizedConstructorAndWritableProperties(int a, string b) + { + Assert.Fail("this ctor should not be used"); + } + } + + [Fact] + public static void SerializingTypeWithCustomNonSerializablePropertyAndJsonConstructorWorksCorrectly() + { + var resolver = new DefaultJsonTypeInfoResolver { Modifiers = { ContractModifier } }; + var options = new JsonSerializerOptions { TypeInfoResolver = resolver }; + string json = JsonSerializer.Serialize(new PocoWithConstructor("str"), options); + Assert.Equal("{}", json); + + static void ContractModifier(JsonTypeInfo jti) + { + if (jti.Type == typeof(PocoWithConstructor)) + { + jti.Properties.Add(jti.CreateJsonPropertyInfo(typeof(string), "someOtherName")); + } + } + } + + [Fact] + public static void SerializingTypeWithCustomSerializablePropertyAndJsonConstructorWorksCorrectly() + { + var resolver = new DefaultJsonTypeInfoResolver { Modifiers = { ContractModifier } }; + var options = new JsonSerializerOptions { TypeInfoResolver = resolver }; + string json = JsonSerializer.Serialize(new PocoWithConstructor("str"), options); + Assert.Equal("""{"test":"asd"}""", json); + + static void ContractModifier(JsonTypeInfo jti) + { + if (jti.Type == typeof(PocoWithConstructor)) + { + JsonPropertyInfo pi = jti.CreateJsonPropertyInfo(typeof(string), "test"); + pi.Get = (o) => "asd"; + jti.Properties.Add(pi); + } + } + } + + [Fact] + public static void SerializingTypeWithCustomPropertyAndJsonConstructorBindsParameter() + { + var resolver = new DefaultJsonTypeInfoResolver { Modifiers = { ContractModifier } }; + var options = new JsonSerializerOptions { TypeInfoResolver = resolver }; + string json = """{"parameter":"asd"}"""; + PocoWithConstructor deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal("asd", deserialized.ParameterValue); + + static void ContractModifier(JsonTypeInfo jti) + { + if (jti.Type == typeof(PocoWithConstructor)) + { + jti.Properties.Add(jti.CreateJsonPropertyInfo(typeof(string), "parameter")); + } + } + } + + private class PocoWithConstructor + { + internal string ParameterValue { get; set; } + + public PocoWithConstructor(string parameter) + { + ParameterValue = parameter; + } + } + + [Fact] + public static void JsonConstructorAttributeIsOverridenAndPropertiesAreSetWhenCreateObjectIsSet_LargeConstructor() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add(ti => + { + if (ti.Type == typeof(ClassWithLargeParameterizedConstructor)) + { + Assert.Null(ti.CreateObject); + ti.CreateObject = () => new ClassWithLargeParameterizedConstructor(); + } + }); + + JsonSerializerOptions o = new() { TypeInfoResolver = resolver }; + + string json = """{"A":2,"B":"foo","C":"bar","E":true}"""; + var deserialized = JsonSerializer.Deserialize(json, o); + + Assert.NotNull(deserialized); + Assert.Equal(2, deserialized.A); + Assert.Equal("foo", deserialized.B); + Assert.Equal("bar", deserialized.C); + Assert.True(deserialized.E); + } + + private class ClassWithLargeParameterizedConstructor + { + public int A { get; set; } + public string B { get; set; } + public string C { get; set; } + public string D { get; set; } + public bool E { get; set; } + public int F { get; set; } + + public ClassWithLargeParameterizedConstructor() { } + + [JsonConstructor] + public ClassWithLargeParameterizedConstructor(int a, string b, string c, string d, bool e, int f) + { + Assert.Fail("this ctor should not be used"); + } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.cs new file mode 100644 index 0000000000000..a1b1b3d2cc038 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.cs @@ -0,0 +1,225 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class DefaultJsonTypeInfoResolverTests + { + [Fact] + public static void GetTypeInfoNullArguments() + { + DefaultJsonTypeInfoResolver r = new(); + Assert.Throws(() => r.GetTypeInfo(null, null)); + Assert.Throws(() => r.GetTypeInfo(null, new JsonSerializerOptions())); + Assert.Throws(() => r.GetTypeInfo(typeof(string), null)); + } + + [Fact] + public static void ModifiersIsEmptyNonCastableIList() + { + DefaultJsonTypeInfoResolver r = new(); + Assert.NotNull(r.Modifiers); + Assert.Null(r.Modifiers as List>); + Assert.False(r.Modifiers.GetType().IsPublic); + Assert.Empty(r.Modifiers); + Assert.Equal(0, r.Modifiers.Count); + } + + [Fact] + public static void ModifiersAreMutableAndInterfaceIsImplementedCorrectly() + { + DefaultJsonTypeInfoResolver r = new(); + Assert.Same(r.Modifiers, r.Modifiers); + var mods = r.Modifiers; + + Action el0 = (ti) => { }; + Action el1 = (ti) => { }; + Action el2 = (ti) => { }; + Assert.NotSame(el0, el1); + Assert.NotSame(el1, el2); + IEnumerator> enumerator; + + Assert.Equal(0, mods.Count); + Assert.False(mods.IsReadOnly); + Assert.Throws(() => mods[-1]); + Assert.Throws(() => mods[0]); + Assert.Throws(() => mods[1]); + + using (enumerator = mods.GetEnumerator()) + { + Assert.False(enumerator.MoveNext()); + } + + mods.Add(el0); + Assert.Equal(1, mods.Count); + Assert.Throws(() => mods[-1]); + Assert.Same(el0, mods[0]); + Assert.Throws(() => mods[1]); + + using (enumerator = mods.GetEnumerator()) + { + Assert.True(enumerator.MoveNext()); + Assert.Same(el0, enumerator.Current); + Assert.False(enumerator.MoveNext()); + } + + mods.Clear(); + Assert.Equal(0, mods.Count); + + using (enumerator = mods.GetEnumerator()) + { + Assert.False(enumerator.MoveNext()); + } + + mods.Insert(0, el0); + Assert.Equal(1, mods.Count); + Assert.Same(el0, mods[0]); + Assert.True(mods.Remove(el0)); + Assert.Equal(0, mods.Count); + + mods.Insert(0, el1); + mods.Insert(0, el0); + Assert.Equal(2, mods.Count); + Assert.Same(el0, mods[0]); + Assert.Same(el1, mods[1]); + Assert.False(mods.Remove(el2)); + mods.RemoveAt(1); + Assert.Equal(1, mods.Count); + Assert.Same(el0, mods[0]); + } + + [Fact] + public static void EmptyModifiersAreImmutableAfterFirstUsage() + { + DefaultJsonTypeInfoResolver r = new(); + IList> mods = r.Modifiers; + + Assert.NotNull(r.GetTypeInfo(typeof(string), new JsonSerializerOptions())); + + Assert.True(mods.IsReadOnly); + Assert.Same(mods, r.Modifiers); + Assert.Equal(0, mods.Count); + + Assert.Throws(() => mods.Add((ti) => { })); + Assert.Throws(() => mods.Insert(0, (ti) => { })); + } + + [Fact] + public static void NonEmptyModifiersAreImmutableAfterFirstUsage() + { + DefaultJsonTypeInfoResolver r = new(); + IList> mods = r.Modifiers; + Action el0 = (ti) => { }; + Action el1 = (ti) => { }; + mods.Add(el0); + mods.Add(el1); + + Assert.NotNull(r.GetTypeInfo(typeof(string), new JsonSerializerOptions())); + + Assert.True(mods.IsReadOnly); + Assert.Same(mods, r.Modifiers); + Assert.Equal(2, mods.Count); + + Assert.Throws(() => mods.Add((ti) => { })); + Assert.Throws(() => mods.Insert(0, (ti) => { })); + Assert.Throws(() => mods.Remove(el0)); + Assert.Throws(() => mods.RemoveAt(0)); + } + + [Fact] + public static void ModifiersAreCalledAndModifyTypeInfos() + { + DefaultJsonTypeInfoResolver r = new(); + JsonTypeInfo storedTypeInfo = null; + bool createObjectCalled = false; + bool secondModifierCalled = false; + r.Modifiers.Add((ti) => + { + Assert.Null(storedTypeInfo); + storedTypeInfo = ti; + + // marker that test has modified something + ti.CreateObject = () => + { + Assert.False(createObjectCalled); + createObjectCalled = true; + + // we don't care what's returned as it won't be used by deserialization + return null; + }; + }); + + r.Modifiers.Add((ti) => + { + // this proves we've been called after first modifier + Assert.NotNull(storedTypeInfo); + Assert.Same(storedTypeInfo, ti); + secondModifierCalled = true; + }); + + JsonTypeInfo returnedTypeInfo = r.GetTypeInfo(typeof(InvalidOperationException), new JsonSerializerOptions()); + + Assert.NotNull(storedTypeInfo); + Assert.Same(storedTypeInfo, returnedTypeInfo); + + Assert.False(createObjectCalled); + // we call our previously set marker + storedTypeInfo.CreateObject(); + + Assert.True(createObjectCalled); + Assert.True(secondModifierCalled); + } + + private static void InvokeGeneric(Type type, string methodName, params object[] args) + { + typeof(DefaultJsonTypeInfoResolverTests) + .GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static) + .MakeGenericMethod(type) + .Invoke(null, args); + } + + private class SomeClass + { + public object ObjProp { get; set; } + public int IntProp { get; set; } + } + + private class SomeOtherClass + { + public object ObjProp { get; set; } + public int IntProp { get; set; } + } + + [JsonSerializable(typeof(SomeClass))] + [JsonSerializable(typeof(SomeOtherClass))] + private partial class SomeClassContext : JsonSerializerContext + { + } + + private class CustomThrowingConverter : JsonConverter + { + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => throw new NotImplementedException(); + } + + private class DummyConverter : JsonConverter + { + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => default(T); + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { } + } + + private class SomeRecursiveClass + { + public int IntProp { get; set; } + public SomeRecursiveClass RecursiveProperty { get; set; } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonTypeInfoResolverTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonTypeInfoResolverTests.cs new file mode 100644 index 0000000000000..8b533b742fbef --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonTypeInfoResolverTests.cs @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class JsonTypeInfoResolverTests + { + [Fact] + public static void GetTypeInfoNullArguments() + { + IJsonTypeInfoResolver[] resolvers = null; + Assert.Throws(() => JsonTypeInfoResolver.Combine(resolvers)); + + DefaultJsonTypeInfoResolver nonNullResolver1 = new(); + DefaultJsonTypeInfoResolver nonNullResolver2 = new(); + Assert.Throws(() => JsonTypeInfoResolver.Combine(null)); + Assert.Throws(() => JsonTypeInfoResolver.Combine(null, null)); + Assert.Throws(() => JsonTypeInfoResolver.Combine(nonNullResolver1, null)); + Assert.Throws(() => JsonTypeInfoResolver.Combine(nonNullResolver1, nonNullResolver2, null)); + Assert.Throws(() => JsonTypeInfoResolver.Combine(nonNullResolver1, null, nonNullResolver2)); + } + + [Fact] + public static void CombiningZeroResolversProducesValidResolver() + { + IJsonTypeInfoResolver resolver = JsonTypeInfoResolver.Combine(); + Assert.NotNull(resolver); + + // calling twice to make sure we get the same answer + Assert.Null(resolver.GetTypeInfo(null, null)); + Assert.Null(resolver.GetTypeInfo(null, null)); + } + + [Fact] + public static void CombiningSingleResolverProducesSameAnswersAsInputResolver() + { + JsonSerializerOptions options = new(); + JsonTypeInfo t1 = JsonTypeInfo.CreateJsonTypeInfo(typeof(int), options); + JsonTypeInfo t2 = JsonTypeInfo.CreateJsonTypeInfo(typeof(uint), options); + JsonTypeInfo t3 = JsonTypeInfo.CreateJsonTypeInfo(typeof(string), options); + + // we return same instance for easier comparison + TestResolver resolver = new((t, o) => + { + Assert.Same(o, options); + if (t == typeof(int)) return t1; + if (t == typeof(uint)) return t2; + if (t == typeof(string)) return t3; + return null; + }); + + IJsonTypeInfoResolver combined = JsonTypeInfoResolver.Combine(resolver); + + Assert.Same(t1, combined.GetTypeInfo(typeof(int), options)); + Assert.Same(t2, combined.GetTypeInfo(typeof(uint), options)); + Assert.Same(t3, combined.GetTypeInfo(typeof(string), options)); + Assert.Null(combined.GetTypeInfo(typeof(char), options)); + Assert.Null(combined.GetTypeInfo(typeof(StringBuilder), options)); + } + + [Fact] + public static void CombiningUsesAndRespectsAllResolversInOrder() + { + JsonSerializerOptions options = new(); + JsonTypeInfo t1 = JsonTypeInfo.CreateJsonTypeInfo(typeof(int), options); + JsonTypeInfo t2 = JsonTypeInfo.CreateJsonTypeInfo(typeof(uint), options); + JsonTypeInfo t3 = JsonTypeInfo.CreateJsonTypeInfo(typeof(string), options); + + int resolverId = 1; + + // we return same instance for easier comparison + TestResolver r1 = new((t, o) => + { + Assert.Equal(1, resolverId); + Assert.Same(o, options); + if (t == typeof(int)) return t1; + resolverId++; + return null; + }); + + TestResolver r2 = new((t, o) => + { + Assert.Equal(2, resolverId); + Assert.Same(o, options); + if (t == typeof(uint)) return t2; + resolverId++; + return null; + }); + + TestResolver r3 = new((t, o) => + { + Assert.Equal(3, resolverId); + Assert.Same(o, options); + if (t == typeof(string)) return t3; + resolverId++; + return null; + }); + + IJsonTypeInfoResolver combined = JsonTypeInfoResolver.Combine(r1, r2, r3); + + resolverId = 1; + Assert.Same(t1, combined.GetTypeInfo(typeof(int), options)); + Assert.Equal(1, resolverId); + + resolverId = 1; + Assert.Same(t2, combined.GetTypeInfo(typeof(uint), options)); + Assert.Equal(2, resolverId); + + resolverId = 1; + Assert.Same(t3, combined.GetTypeInfo(typeof(string), options)); + Assert.Equal(3, resolverId); + + resolverId = 1; + Assert.Null(combined.GetTypeInfo(typeof(char), options)); + Assert.Equal(4, resolverId); + + resolverId = 1; + Assert.Null(combined.GetTypeInfo(typeof(StringBuilder), options)); + Assert.Equal(4, resolverId); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.Options.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.Options.cs index 1a092379ee891..c1f53298ac5df 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.Options.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.Options.cs @@ -55,9 +55,9 @@ public void AddContextOverwritesOptionsForFreshContext() // Those options are overwritten when context is binded via options.AddContext(); JsonSerializerOptions options = new(); options.AddContext(); // No error. - FieldInfo contextField = typeof(JsonSerializerOptions).GetField("_serializerContext", BindingFlags.NonPublic | BindingFlags.Instance); - Assert.NotNull(contextField); - Assert.Same(options, ((JsonSerializerContext)contextField.GetValue(options)).Options); + FieldInfo resolverField = typeof(JsonSerializerOptions).GetField("_typeInfoResolver", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(resolverField); + Assert.Same(options, ((JsonSerializerContext)resolverField.GetValue(options)).Options); } [Fact] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/TestResolver.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/TestResolver.cs new file mode 100644 index 0000000000000..6ea4e77dfaed9 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/TestResolver.cs @@ -0,0 +1,22 @@ +// 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; + +namespace System.Text.Json.Serialization.Tests +{ + internal class TestResolver : IJsonTypeInfoResolver + { + private Func _getTypeInfo; + + public TestResolver(Func getTypeInfo) + { + _getTypeInfo = getTypeInfo; + } + + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + return _getTypeInfo(type, options); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs index 064f5d9c94894..951441d8b482c 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Reflection; using System.Text.Encodings.Web; +using System.Text.Json.Serialization.Metadata; using System.Text.Unicode; using Xunit; @@ -31,21 +32,28 @@ public static void SetOptionsFail() { var options = new JsonSerializerOptions(); - // Verify these do not throw. - options.Converters.Clear(); - TestConverter tc = new TestConverter(); - options.Converters.Add(tc); - options.Converters.Insert(0, new TestConverter()); - options.Converters.Remove(tc); - options.Converters.RemoveAt(0); + TestIListNonThrowingOperationsWhenMutable(options.Converters, () => new TestConverter()); + + // Verify TypeInfoResolver throws on null resolver + Assert.Throws(() => options.TypeInfoResolver = null); + + // Verify default TypeInfoResolver throws + Action tiModifier = (ti) => { }; + Assert.Throws(() => (options.TypeInfoResolver as DefaultJsonTypeInfoResolver).Modifiers.Clear()); + Assert.Throws(() => (options.TypeInfoResolver as DefaultJsonTypeInfoResolver).Modifiers.Add(tiModifier)); + Assert.Throws(() => (options.TypeInfoResolver as DefaultJsonTypeInfoResolver).Modifiers.Insert(0, tiModifier)); + + // Now set DefaultTypeInfoResolver + options.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + TestIListNonThrowingOperationsWhenMutable((options.TypeInfoResolver as DefaultJsonTypeInfoResolver).Modifiers, () => (ti) => { }); // Add one item for later. + TestConverter tc = new TestConverter(); options.Converters.Add(tc); + (options.TypeInfoResolver as DefaultJsonTypeInfoResolver).Modifiers.Add(tiModifier); - // Verify converter collection throws on null adds. - Assert.Throws(() => options.Converters.Add(null)); - Assert.Throws(() => options.Converters.Insert(0, null)); - Assert.Throws(() => options.Converters[0] = null); + TestIListThrowingOperationsWhenMutable(options.Converters); + TestIListThrowingOperationsWhenMutable((options.TypeInfoResolver as DefaultJsonTypeInfoResolver).Modifiers); // Perform serialization. JsonSerializer.Deserialize("1", options); @@ -62,14 +70,8 @@ public static void SetOptionsFail() Assert.Equal(JsonCommentHandling.Disallow, options.ReadCommentHandling); Assert.False(options.WriteIndented); - Assert.Equal(tc, options.Converters[0]); - Assert.True(options.Converters.Contains(tc)); - options.Converters.CopyTo(new JsonConverter[1] { null }, 0); - Assert.Equal(1, options.Converters.Count); - Assert.False(options.Converters.Equals(tc)); - Assert.NotNull(options.Converters.GetEnumerator()); - Assert.Equal(0, options.Converters.IndexOf(tc)); - Assert.False(options.Converters.IsReadOnly); + TestIListNonThrowingOperationsWhenImmutable(options.Converters, tc); + TestIListNonThrowingOperationsWhenImmutable((options.TypeInfoResolver as DefaultJsonTypeInfoResolver).Modifiers, tiModifier); // Setters should always throw; we don't check to see if the value is the same or not. Assert.Throws(() => options.AllowTrailingCommas = options.AllowTrailingCommas); @@ -82,13 +84,102 @@ public static void SetOptionsFail() Assert.Throws(() => options.PropertyNamingPolicy = options.PropertyNamingPolicy); Assert.Throws(() => options.ReadCommentHandling = options.ReadCommentHandling); Assert.Throws(() => options.WriteIndented = options.WriteIndented); + Assert.Throws(() => options.TypeInfoResolver = options.TypeInfoResolver); - Assert.Throws(() => options.Converters[0] = tc); - Assert.Throws(() => options.Converters.Clear()); - Assert.Throws(() => options.Converters.Add(tc)); - Assert.Throws(() => options.Converters.Insert(0, new TestConverter())); - Assert.Throws(() => options.Converters.Remove(tc)); - Assert.Throws(() => options.Converters.RemoveAt(0)); + TestIListThrowingOperationsWhenImmutable(options.Converters, tc); + TestIListThrowingOperationsWhenImmutable((options.TypeInfoResolver as DefaultJsonTypeInfoResolver).Modifiers, tiModifier); + + static void TestIListNonThrowingOperationsWhenMutable(IList list, Func newT) + { + list.Clear(); + T el = newT(); + list.Add(el); + Assert.Equal(1, list.Count); + list.Insert(0, newT()); + Assert.Equal(2, list.Count); + list.Remove(el); + Assert.Equal(1, list.Count); + list.RemoveAt(0); + Assert.Equal(0, list.Count); + Assert.False(list.IsReadOnly, "List should not be read-only"); + } + + static void TestIListThrowingOperationsWhenMutable(IList list) where T : class + { + // Verify collection throws on null adds. + Assert.Throws(() => list.Add(null)); + Assert.Throws(() => list.Insert(0, null)); + Assert.Throws(() => list[0] = null); + } + + static void TestIListNonThrowingOperationsWhenImmutable(IList list, T onlyElement) + { + Assert.Equal(onlyElement, list[0]); + Assert.True(list.Contains(onlyElement)); + list.CopyTo(new T[1] { default(T) }, 0); + Assert.Equal(1, list.Count); + Assert.False(list.Equals(onlyElement)); + Assert.NotNull(list.GetEnumerator()); + Assert.Equal(0, list.IndexOf(onlyElement)); + Assert.True(list.IsReadOnly, "List should be read-only"); + } + + static void TestIListThrowingOperationsWhenImmutable(IList list, T firstElement) + { + Assert.Throws(() => list[0] = firstElement); + Assert.Throws(() => list.Clear()); + Assert.Throws(() => list.Add(firstElement)); + Assert.Throws(() => list.Insert(0, firstElement)); + Assert.Throws(() => list.Remove(firstElement)); + Assert.Throws(() => list.RemoveAt(0)); + } + } + + [Fact] + public static void TypeInfoResolverIsNotNullAndCorrectType() + { + var options = new JsonSerializerOptions(); + Assert.NotNull(options.TypeInfoResolver); + Assert.IsType(options.TypeInfoResolver); + Assert.Same(options.TypeInfoResolver, options.TypeInfoResolver); + } + + [Fact] + public static void TypeInfoResolverCannotBeSetAfterAddingContext() + { + var options = new JsonSerializerOptions(); + options.AddContext(); + Assert.IsType(options.TypeInfoResolver); + Assert.Throws(() => options.TypeInfoResolver = new DefaultJsonTypeInfoResolver()); + } + + [Fact] + public static void TypeInfoResolverCannotBeSetOnOptionsCreatedFromContext() + { + var context = new JsonContext(); + var options = context.Options; + Assert.Same(context, options.TypeInfoResolver); + Assert.Throws(() => options.TypeInfoResolver = new DefaultJsonTypeInfoResolver()); + } + + [Fact] + public static void WhenAddingContextTypeInfoResolverAsContextOptionsAreSameAsOptions() + { + var options = new JsonSerializerOptions(); + options.AddContext(); + Assert.Same(options, (options.TypeInfoResolver as JsonContext).Options); + } + + [Fact] + public static void TypeInfoResolverCannotBeSetAfterContextIsSetThroughTypeInfoResolver() + { + var options = new JsonSerializerOptions(); + IJsonTypeInfoResolver resolver = new JsonContext(); + options.TypeInfoResolver = resolver; + Assert.Same(resolver, options.TypeInfoResolver); + + resolver = new DefaultJsonTypeInfoResolver(); + Assert.Throws(() => options.TypeInfoResolver = resolver); } [Fact] @@ -607,6 +698,10 @@ private static JsonSerializerOptions GetFullyPopulatedOptionsInstance() { options.ReferenceHandler = ReferenceHandler.Preserve; } + else if (propertyType == typeof(IJsonTypeInfoResolver)) + { + options.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); + } else if (propertyType.IsValueType) { options.ReadCommentHandling = JsonCommentHandling.Disallow; diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.DeserializeAsyncEnumerable.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.DeserializeAsyncEnumerable.cs index f50d0f85f3bb9..38f3122dd6994 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.DeserializeAsyncEnumerable.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.DeserializeAsyncEnumerable.cs @@ -64,7 +64,8 @@ public static async Task DeserializeAsyncEnumerable_ReadSourceAsync(IE { JsonSerializerOptions options = new JsonSerializerOptions { - DefaultBufferSize = bufferSize + DefaultBufferSize = bufferSize, + TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; byte[] data = JsonSerializer.SerializeToUtf8Bytes(source); @@ -82,7 +83,12 @@ public static async Task DeserializeAsyncEnumerable_ShouldStreamPartialData(bool string json = JsonSerializer.Serialize(Enumerable.Range(0, 100)); using var stream = new Utf8MemoryStream(json); - IAsyncEnumerable asyncEnumerable = DeserializeAsyncEnumerableWrapper(stream, new JsonSerializerOptions { DefaultBufferSize = 1 }, useJsonTypeInfoOverload: useJsonTypeInfoOverload); + JsonSerializerOptions options = new JsonSerializerOptions + { + DefaultBufferSize = 1 + }; + + IAsyncEnumerable asyncEnumerable = DeserializeAsyncEnumerableWrapper(stream, options, useJsonTypeInfoOverload: useJsonTypeInfoOverload); await using IAsyncEnumerator asyncEnumerator = asyncEnumerable.GetAsyncEnumerator(); for (int i = 0; i < 20; i++) @@ -181,7 +187,7 @@ public static async Task DeserializeAsyncEnumerable_CancellationToken_ThrowsOnCa { JsonSerializerOptions options = new JsonSerializerOptions { - DefaultBufferSize = 1 + DefaultBufferSize = 1, }; byte[] data = JsonSerializer.SerializeToUtf8Bytes(Enumerable.Range(1, 100)); @@ -246,10 +252,9 @@ private static IAsyncEnumerable DeserializeAsyncEnumerableWrapper(Stream s private static JsonTypeInfo ResolveJsonTypeInfo(JsonSerializerOptions? options = null) { - // TODO replace with contract resolver once implemented -- only works with value converters. options ??= JsonSerializerOptions.Default; - JsonConverter converter = (JsonConverter)options.GetConverter(typeof(T)); - return JsonMetadataServices.CreateValueInfo(options, converter); + JsonSerializer.Serialize(42, options); // Lock the options instance before initializing metadata + return (JsonTypeInfo)options.TypeInfoResolver.GetTypeInfo(typeof(T), options); } private static async Task> ToListAsync(this IAsyncEnumerable source) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TypeInfoResolverFunctionalTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TypeInfoResolverFunctionalTests.cs new file mode 100644 index 0000000000000..ebece0937fa46 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TypeInfoResolverFunctionalTests.cs @@ -0,0 +1,703 @@ +// 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.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text.Json.Serialization.Metadata; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static class TypeInfoResolverFunctionalTests + { + [Fact] + public static void AddPrefixToEveryPropertyOfClass() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClass)) + { + Assert.Equal(JsonTypeInfoKind.Object, ti.Kind); + foreach (var prop in ti.Properties) + { + prop.Name = "renamed_" + prop.Name; + } + } + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = resolver; + + TestClass originalObj = new TestClass() + { + TestField = "test value", + TestProperty = 42, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""renamed_TestProperty"":42,""renamed_TestField"":""test value""}", json); + + TestClass deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(originalObj.TestProperty, deserialized.TestProperty); + } + + [Fact] + public static void AppendCharacterWhenSerializingField() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClass)) + { + Assert.Equal(JsonTypeInfoKind.Object, ti.Kind); + // Because IncludeFields is false + Assert.Equal(1, ti.Properties.Count); + JsonPropertyInfo field = ti.CreateJsonPropertyInfo(typeof(string), "TestField"); + field.Get = (o) => + { + var obj = (TestClass)o; + return obj.TestField + "X"; + }; + field.Set = (o, val) => + { + var obj = (TestClass)o; + var value = (string)val; + // We append 'X' on serialization + // therefore on deserialization we remove last character + obj.TestField = value.Substring(0, value.Length - 1); + }; + ti.Properties.Add(field); + } + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.TypeInfoResolver = resolver; + + TestClass originalObj = new TestClass() + { + TestField = "test value", + TestProperty = 42, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""TestProperty"":42,""TestField"":""test valueX""}", json); + + TestClass deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(originalObj.TestProperty, deserialized.TestProperty); + } + + [Fact] + public static void DoNotSerializeValue42() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClass)) + { + Assert.Equal(JsonTypeInfoKind.Object, ti.Kind); + foreach (var prop in ti.Properties) + { + if (prop.PropertyType == typeof(int)) + { + prop.ShouldSerialize = (o, val) => + { + return (int)val != 42; + }; + } + } + } + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = resolver; + + TestClass originalObj = new TestClass() + { + TestField = "test value", + TestProperty = 43, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""TestProperty"":43,""TestField"":""test value""}", json); + + originalObj.TestProperty = 42; + json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""TestField"":""test value""}", json); + + TestClass deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(0, deserialized.TestProperty); + } + + [Fact] + public static void DoNotSerializePropertyWithNameButDeserializeIt() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClass)) + { + Assert.Equal(JsonTypeInfoKind.Object, ti.Kind); + foreach (var prop in ti.Properties) + { + if (prop.Name == nameof(TestClass.TestProperty)) + { + prop.Get = null; + } + } + } + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = resolver; + + TestClass originalObj = new TestClass() + { + TestField = "test value", + TestProperty = 42, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""TestField"":""test value""}", json); + + json = @"{""TestProperty"":42,""TestField"":""test value""}"; + + TestClass deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(originalObj.TestProperty, deserialized.TestProperty); + } + + [Fact] + public static void DoNotDeserializePropertyWithNameButSerializeIt() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClass)) + { + Assert.Equal(JsonTypeInfoKind.Object, ti.Kind); + foreach (var prop in ti.Properties) + { + if (prop.Name == nameof(TestClass.TestProperty)) + { + prop.Set = null; + } + } + } + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = resolver; + + TestClass originalObj = new TestClass() + { + TestField = "test value", + TestProperty = 42, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""TestProperty"":42,""TestField"":""test value""}", json); + + TestClass deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(0, deserialized.TestProperty); + } + + [Fact] + public static void SetCustomNumberHandlingForAProperty() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClass)) + { + Assert.Equal(JsonTypeInfoKind.Object, ti.Kind); + foreach (var prop in ti.Properties) + { + if (prop.Name == nameof(TestClass.TestProperty)) + { + prop.NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString; + } + } + } + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = resolver; + + TestClass originalObj = new TestClass() + { + TestField = "test value", + TestProperty = 42, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""TestProperty"":""42"",""TestField"":""test value""}", json); + + TestClass deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(originalObj.TestProperty, deserialized.TestProperty); + } + + [Fact] + public static void SetCustomConverterForAProperty() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClass)) + { + Assert.Equal(JsonTypeInfoKind.Object, ti.Kind); + foreach (var prop in ti.Properties) + { + if (prop.Name == nameof(TestClass.TestProperty)) + { + prop.CustomConverter = new PlusOneConverter(); + } + } + } + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = resolver; + + TestClass originalObj = new TestClass() + { + TestField = "test value", + TestProperty = 42, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""TestProperty"":43,""TestField"":""test value""}", json); + + TestClass deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(originalObj.TestProperty, deserialized.TestProperty); + } + + [Fact] + public static void UntypedCreateObjectWithDefaults() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClass)) + { + ti.CreateObject = () => + { + return new TestClass() + { + TestField = "test value", + TestProperty = 42, + }; + }; + } + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = resolver; + + TestClass originalObj = new TestClass() + { + TestField = "test value 2", + TestProperty = 45, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""TestProperty"":45,""TestField"":""test value 2""}", json); + + TestClass deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(originalObj.TestProperty, deserialized.TestProperty); + + json = @"{}"; + deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal("test value", deserialized.TestField); + Assert.Equal(42, deserialized.TestProperty); + + json = @"{""TestField"":""test value 2""}"; + deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(42, deserialized.TestProperty); + } + + [Fact] + public static void TypedCreateObjectWithDefaults() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClass)) + { + JsonTypeInfo typedTi = ti as JsonTypeInfo; + Assert.NotNull(typedTi); + typedTi.CreateObject = () => + { + return new TestClass() + { + TestField = "test value", + TestProperty = 42, + }; + }; + } + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = resolver; + + TestClass originalObj = new TestClass() + { + TestField = "test value 2", + TestProperty = 45, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""TestProperty"":45,""TestField"":""test value 2""}", json); + + TestClass deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(originalObj.TestProperty, deserialized.TestProperty); + + json = @"{}"; + deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal("test value", deserialized.TestField); + Assert.Equal(42, deserialized.TestProperty); + + json = @"{""TestField"":""test value 2""}"; + deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(42, deserialized.TestProperty); + } + + [Fact] + public static void SetCustomNumberHandlingForAType() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClass)) + { + Assert.Equal(JsonTypeInfoKind.Object, ti.Kind); + ti.NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString; + } + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = resolver; + + TestClass originalObj = new TestClass() + { + TestField = "test value", + TestProperty = 42, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""TestProperty"":""42"",""TestField"":""test value""}", json); + + TestClass deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(originalObj.TestProperty, deserialized.TestProperty); + } + + [Fact] + public static void CombineCustomResolverWithDefault() + { + TestResolver resolver = new TestResolver((Type type, JsonSerializerOptions options) => + { + if (type != typeof(TestClass)) + return null; + + JsonTypeInfo ti = JsonTypeInfo.CreateJsonTypeInfo(options); + ti.CreateObject = () => new TestClass() + { + TestField = string.Empty, + TestProperty = 42, + }; + + JsonPropertyInfo field = ti.CreateJsonPropertyInfo(typeof(string), "MyTestField"); + field.Get = (o) => + { + TestClass obj = (TestClass)o; + return obj.TestField ?? string.Empty; + }; + + field.Set = (o, val) => + { + TestClass obj = (TestClass)o; + string value = (string?)val ?? string.Empty; + obj.TestField = value; + }; + + field.ShouldSerialize = (o, val) => (string)val != string.Empty; + + JsonPropertyInfo prop = ti.CreateJsonPropertyInfo(typeof(int), "MyTestProperty"); + prop.Get = (o) => + { + TestClass obj = (TestClass)o; + return obj.TestProperty; + }; + + prop.Set = (o, val) => + { + TestClass obj = (TestClass)o; + obj.TestProperty = (int)val; + }; + + prop.ShouldSerialize = (o, val) => (int)val != 42; + + ti.Properties.Add(field); + ti.Properties.Add(prop); + return ti; + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = JsonTypeInfoResolver.Combine(resolver, options.TypeInfoResolver); + + TestClass originalObj = new TestClass() + { + TestField = "test value", + TestProperty = 45, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""MyTestField"":""test value"",""MyTestProperty"":45}", json); + + TestClass deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(originalObj.TestProperty, deserialized.TestProperty); + + originalObj.TestField = null; + json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""MyTestProperty"":45}", json); + + originalObj.TestField = string.Empty; + json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""MyTestProperty"":45}", json); + + deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(originalObj.TestProperty, deserialized.TestProperty); + + originalObj.TestField = "test value"; + originalObj.TestProperty = 42; + json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal(@"{""MyTestField"":""test value""}", json); + deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.TestField, deserialized.TestField); + Assert.Equal(originalObj.TestProperty, deserialized.TestProperty); + } + + [Fact] + public static void DataContractResolverScenario() + { + var options = new JsonSerializerOptions { TypeInfoResolver = new DataContractResolver() }; + + var value = new DataContractResolver.TestClass { String = "str", Boolean = true, Int = 42, Ignored = "ignored" }; + string json = JsonSerializer.Serialize(value, options); + Assert.Equal("""{"intValue":42,"boolValue":true,"stringValue":"str"}""", json); + + DataContractResolver.TestClass result = JsonSerializer.Deserialize(json, options); + Assert.Equal("str", result.String); + Assert.Equal(42, result.Int); + Assert.True(result.Boolean); + } + + internal class DataContractResolver : DefaultJsonTypeInfoResolver + { + [DataContract] + public class TestClass + { + [JsonIgnore] // ignored by the custom resolver + [DataMember(Name = "stringValue", Order = 2)] + public string String { get; set; } + + [JsonPropertyName("BOOL_VALUE")] // ignored by the custom resolver + [DataMember(Name = "boolValue", Order = 1)] + public bool Boolean { get; set; } + + [JsonPropertyOrder(int.MaxValue)] // ignored by the custom resolver + [DataMember(Name = "intValue", Order = 0)] + public int Int { get; set; } + + [IgnoreDataMember] + public string Ignored { get; set; } + } + + public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options); + + if (jsonTypeInfo.Kind == JsonTypeInfoKind.Object && + type.GetCustomAttribute() is not null) + { + jsonTypeInfo.Properties.Clear(); // TODO should not require clearing + + IEnumerable<(PropertyInfo propInfo, DataMemberAttribute attr)> properties = type + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(propInfo => propInfo.GetCustomAttribute() is null) + .Select(propInfo => (propInfo, attr: propInfo.GetCustomAttribute())) + .OrderBy(entry => entry.attr?.Order ?? 0); + + foreach ((PropertyInfo propertyInfo, DataMemberAttribute? attr) in properties) + { + JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(propertyInfo.PropertyType, attr?.Name ?? propertyInfo.Name); + jsonPropertyInfo.Get = + propertyInfo.CanRead + ? propertyInfo.GetValue + : null; + + jsonPropertyInfo.Set = propertyInfo.CanWrite + ? propertyInfo.SetValue + : null; + + jsonTypeInfo.Properties.Add(jsonPropertyInfo); + } + } + + return jsonTypeInfo; + } + } + + [Fact] + public static void SpecifiedContractResolverScenario() + { + var options = new JsonSerializerOptions { TypeInfoResolver = new SpecifiedContractResolver() }; + + var value = new SpecifiedContractResolver.TestClass { String = "str", Int = 42 }; + string json = JsonSerializer.Serialize(value, options); + Assert.Equal("""{}""", json); + + value.IntSpecified = true; + json = JsonSerializer.Serialize(value, options); + Assert.Equal("""{"Int":42}""", json); + + value.StringSpecified = true; + json = JsonSerializer.Serialize(value, options); + Assert.Equal("""{"String":"str","Int":42}""", json); + } + + internal class SpecifiedContractResolver : DefaultJsonTypeInfoResolver + { + public class TestClass + { + public string String { get; set; } + [JsonIgnore] + public bool StringSpecified { get; set; } + + public int Int { get; set; } + [JsonIgnore] + public bool IntSpecified { get; set; } + } + public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options); + + foreach (JsonPropertyInfo property in jsonTypeInfo.Properties) + { + PropertyInfo? specifiedProperty = type.GetProperty(property.Name + "Specified", BindingFlags.Instance | BindingFlags.Public); + + if (specifiedProperty != null && specifiedProperty.CanRead && specifiedProperty.PropertyType == typeof(bool)) + { + property.ShouldSerialize = (obj, _) => (bool)specifiedProperty.GetValue(obj); + } + } + + return jsonTypeInfo; + } + } + + [Fact] + public static void FieldContractResolverScenario() + { + var options = new JsonSerializerOptions { TypeInfoResolver = new FieldContractResolver() }; + + var value = FieldContractResolver.TestClass.Create("str", 42, true); + string json = JsonSerializer.Serialize(value, options); + Assert.Equal("""{"_string":"str","_int":42,"_bool":true}""", json); + + FieldContractResolver.TestClass result = JsonSerializer.Deserialize(json, options); + Assert.Equal(value, result); + } + + internal class FieldContractResolver : DefaultJsonTypeInfoResolver + { + public class TestClass + { + private string _string; + private int _int; + private bool _bool; + + public static TestClass Create(string @string, int @int, bool @bool) + => new TestClass { _string = @string, _int = @int, _bool = @bool }; + + // Should be ignored by the serializer + public bool Boolean + { + get => _bool; + set => throw new NotSupportedException(); + } + + public override int GetHashCode() => (_string, _int, _bool).GetHashCode(); + public override bool Equals(object? other) + => other is TestClass tc && (_string, _int, _bool) == (tc._string, tc._int, tc._bool); + } + + public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options); + + if (jsonTypeInfo.Kind == JsonTypeInfoKind.Object) + { + jsonTypeInfo.Properties.Clear(); + + foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(field.FieldType, field.Name); + jsonPropertyInfo.Get = field.GetValue; + jsonPropertyInfo.Set = field.SetValue; + + jsonTypeInfo.Properties.Add(jsonPropertyInfo); + } + } + + return jsonTypeInfo; + } + } + + internal class TestClass + { + public int TestProperty { get; set; } + public string TestField; + } + + // adds one on write, subtracts one on read + internal class PlusOneConverter : JsonConverter + { + public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Assert.Equal(typeof(int), typeToConvert); + return reader.GetInt32() - 1; + } + + public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value + 1); + } + } + } +} 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 f775496500b1a..0f6efb07c8520 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,6 +1,7 @@ $(NetCoreAppCurrent);$(NetFrameworkMinimum) + true true $(NoWarn);SYSLIB0020 @@ -170,9 +171,15 @@ + + + + + + @@ -196,6 +203,7 @@ + diff --git a/src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.txt b/src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.txt index 30e8d88d4acff..ff2c3ef870c04 100644 --- a/src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.txt +++ b/src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.txt @@ -178,4 +178,6 @@ CannotRemoveAttribute : Attribute 'System.Runtime.Versioning.UnsupportedOSPlatfo CannotRemoveAttribute : Attribute 'System.Runtime.Versioning.UnsupportedOSPlatformAttribute' exists on 'System.Security.Cryptography.TripleDES' in the contract but not the implementation. Compat issues with assembly System.Security.Cryptography.X509Certificates: CannotChangeAttribute : Attribute 'System.Runtime.Versioning.UnsupportedOSPlatformAttribute' on 'System.Security.Cryptography.X509Certificates.PublicKey.GetDSAPublicKey()' changed from '[UnsupportedOSPlatformAttribute("ios")]' in the contract to '[UnsupportedOSPlatformAttribute("browser")]' in the implementation. -Total Issues: 167 +Compat issues with assembly System.Text.Json: +CannotMakeTypeAbstract : Type 'System.Text.Json.Serialization.Metadata.JsonTypeInfo' is abstract in the implementation but is not abstract in the contract. +Total Issues: 168