From 897b2a3b8d04922e8fc7b0719aaf1fca4b1478b2 Mon Sep 17 00:00:00 2001 From: Krzysztof Wicher Date: Wed, 22 Jun 2022 21:45:52 +0200 Subject: [PATCH] JSON contract customization (#70435) * Initial implementation for contract customization fix build errors Move converter rooting to DefaultJsonTypeInfoResolver so that it can be used standalone Fix ConfigurationList.IsReadOnly Minor refactorings (#1) * Makes the following changes: * Move singleton initialization for DefaultTypeInfoResolver behind a static property. * Consolidate JsonSerializerContext & IJsonTypeInfoResolver values to a single field. * Move reflection fallback logic away from JsonSerializerContext and into JsonSerializerOptions * Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs * remove testing of removed field Simplify the JsonTypeInfo.CreateObject implemenetation (#2) * Simplify the JsonTypeInfo.CreateObject implemenetation * Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs * Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs Co-authored-by: Krzysztof Wicher Co-authored-by: Krzysztof Wicher Tests and fixes for JsonTypeInfoKind.None TypeInfo type mismatch tests Allow setting NumberHandling on JsonTypeInfoKind.None test resolver returning wrong type of options JsonTypeInfo/JsonPropertyInfo mutability tests rename test file Move default converter rooting responsibility behind DefaultJsonTypeInfoResolver (#3) * Move default converter rooting responsibility behind DefaultJsonTypeInfoResolver * address feedback Add simple test for using JsonTypeInfo with APIs directly taking it fix and tests for untyped/typed CreateObject uncomment test cases, remove todo More tests and tiny fixes Add a JsonTypeInfoResolver.Combine test for JsonSerializerContext (#4) * Fix JsonTypeInfoResolver.Combine for JsonSerializerContext * Break up failing test Fix simple scenarios for combining contexts (#6) * Fix simple scenarios for combining contexts * feedback JsonSerializerContext combine test with different camel casing Remove unneeded virtual calls & branching when accessing Get & Set delegates (#7) JsonPropertyInfo tests everything minus ShouldSerialize & NumberHandling Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs throw InvalidOperationException rather than ArgumentNullException for source gen when PropertyInfo.Name is assigned through JsonPropertyInfoValues tests for duplicated property names and JsonPropertyInfo.NumberHandling Add tests for NumberHandling and failing tests for ShouldSerialize disable the failing test and add extra checks disable remainder of the failing ShouldSerialize tests, fix working one Fix ShouldSerialize and IgnoreCondition interop Add failing tests for CreateObject + parametrized constructors Fix CreateObject support for JsonConstructor types (#10) * Fix CreateObject support for JsonConstructor types * address feedback Make contexts more combinator friendly (#9) * Make contexts more combinator friendly * remove converter cache * redesign test to account for JsonConstructorAttribute * Combine unit tests * address feedback * Add acceptance tests for DataContract attributes & Specified pattern (#11) * Add private field serialization acceptance test (#13) * tests, PR feedback (#14) * PR feedback and extra tests * Shorten class name, remove incorrect check (not true for polimorphic cases) * Make parameter matching for custom properties map property Name with parameter (#16) * Test static initialization with JsonTypeInfo (#17) * Fix test failures and proper fix this time (#18) * Fix test failures and proper fix this time * reinstate ActiveIssueAttribute * PR feedback and adjust couple of tests which don't set TypeInfoResolver * fix IAsyncEnumerable tests * Lock JsonSerializerOptions in JsonTypeInfo.EnsureConfigured() Co-authored-by: Eirik Tsarpalis Co-authored-by: Eirik Tsarpalis --- .../gen/JsonSourceGenerator.Emitter.cs | 246 ++-- .../gen/JsonSourceGenerator.Parser.cs | 4 +- .../gen/TypeGenerationSpec.cs | 5 + .../System.Text.Json/ref/System.Text.Json.cs | 65 +- .../src/Resources/Strings.resx | 87 +- .../src/System.Text.Json.csproj | 19 +- .../JsonPropertyDictionary.KeyCollection.cs | 10 +- .../JsonPropertyDictionary.ValueCollection.cs | 10 +- .../Text/Json/JsonPropertyDictionary.cs | 26 +- .../JsonPropertyInfoDictionaryValueList.cs | 206 +++ .../Json/Serialization/ConfigurationList.cs | 41 +- .../Converters/CastingConverter.cs | 70 + .../Collection/JsonCollectionConverter.cs | 6 +- .../Collection/JsonDictionaryConverter.cs | 2 +- .../Collection/StackOrQueueConverter.cs | 2 +- .../FSharp/FSharpTypeConverterFactory.cs | 4 +- .../JsonMetadataServicesConverter.cs | 12 +- .../Converters/Object/ObjectConverter.cs | 2 +- .../Object/ObjectDefaultConverter.cs | 20 +- ...ctWithParameterizedConstructorConverter.cs | 15 +- .../Value/NullableConverterFactory.cs | 2 +- .../Text/Json/Serialization/JsonConverter.cs | 19 +- .../Serialization/JsonConverterFactory.cs | 8 +- .../Json/Serialization/JsonConverterOfT.cs | 38 +- .../JsonSerializer.Read.HandlePropertyName.cs | 10 +- .../JsonSerializer.Read.Helpers.cs | 2 +- .../JsonSerializer.Read.Stream.cs | 6 +- .../JsonSerializer.Read.Utf8JsonReader.cs | 2 +- .../JsonSerializer.Write.Helpers.cs | 8 +- .../Serialization/JsonSerializerContext.cs | 22 +- .../JsonSerializerOptions.Caching.cs | 61 +- .../JsonSerializerOptions.Converters.cs | 275 ++-- .../Serialization/JsonSerializerOptions.cs | 180 ++- .../Metadata/CustomJsonTypeInfoOfT.cs | 47 + .../DefaultJsonTypeInfoResolver.Converters.cs | 126 ++ .../Metadata/DefaultJsonTypeInfoResolver.cs | 138 ++ .../Metadata/IJsonTypeInfoResolver.cs | 24 + .../JsonMetadataServices.Converters.cs | 32 +- .../Metadata/JsonMetadataServices.cs | 27 +- .../Metadata/JsonParameterInfo.cs | 2 +- .../Metadata/JsonPropertyInfo.cs | 259 +++- .../Metadata/JsonPropertyInfoOfT.cs | 357 +++-- .../Metadata/JsonTypeInfo.Cache.cs | 17 +- .../Serialization/Metadata/JsonTypeInfo.cs | 294 ++++- .../Metadata/JsonTypeInfoKind.cs | 28 + .../Serialization/Metadata/JsonTypeInfoOfT.cs | 54 +- .../Metadata/JsonTypeInfoResolver.cs | 67 + .../Serialization/Metadata/MemberAccessor.cs | 2 +- .../ReflectionEmitCachingMemberAccessor.cs | 2 +- .../Metadata/ReflectionEmitMemberAccessor.cs | 4 +- .../Metadata/ReflectionJsonTypeInfoOfT.cs | 70 +- .../Metadata/ReflectionMemberAccessor.cs | 4 +- .../Metadata/SourceGenJsonTypeInfoOfT.cs | 43 +- .../Text/Json/Serialization/ReadStack.cs | 10 +- .../Text/Json/Serialization/WriteStack.cs | 2 +- .../Json/Serialization/WriteStackFrame.cs | 6 +- .../src/System/Text/Json/ThrowHelper.Node.cs | 24 +- .../Text/Json/ThrowHelper.Serialization.cs | 69 +- .../src/System/Text/Json/ThrowHelper.cs | 12 + .../tests/Common/PropertyVisibilityTests.cs | 4 +- .../JsonSerializerContextTests.cs | 179 ++- .../Serialization/CacheTests.cs | 1 + .../CustomConverterTests.cs | 8 +- .../JsonSerializerWrapper.Reflection.cs | 18 +- ...ltJsonTypeInfoResolverMultiContextTests.cs | 134 ++ ...nTypeInfoResolverTests.JsonPropertyInfo.cs | 1153 +++++++++++++++++ ...tJsonTypeInfoResolverTests.JsonTypeInfo.cs | 830 ++++++++++++ .../DefaultJsonTypeInfoResolverTests.cs | 225 ++++ .../JsonTypeInfoResolverTests.cs | 130 ++ .../MetadataTests/MetadataTests.Options.cs | 6 +- .../MetadataTests/TestResolver.cs | 22 + .../Serialization/OptionsTests.cs | 145 ++- .../Stream.DeserializeAsyncEnumerable.cs | 17 +- .../TypeInfoResolverFunctionalTests.cs | 703 ++++++++++ .../System.Text.Json.Tests.csproj | 8 + ...iCompatBaseline.NetCoreAppLatestStable.txt | 4 +- 76 files changed, 5865 insertions(+), 927 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyInfoDictionaryValueList.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/CustomJsonTypeInfoOfT.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/IJsonTypeInfoResolver.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoKind.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolver.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverMultiContextTests.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonPropertyInfo.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/JsonTypeInfoResolverTests.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/TestResolver.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TypeInfoResolverFunctionalTests.cs 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