diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index d0f8b2e121df8e..85953b92aeecb4 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,8 +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 ConditionalWeakTableTypeRef = "global::System.Runtime.CompilerServices.ConditionalWeakTable"; 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"; @@ -64,7 +63,6 @@ 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"; @@ -315,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); } @@ -323,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()); } @@ -366,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); @@ -387,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); } @@ -397,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); } @@ -410,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)}"; @@ -483,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; @@ -492,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: @@ -524,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); @@ -645,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) { @@ -671,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"; @@ -699,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 {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerContextTypeRef}? context) +private static {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}) {{ - {contextTypeRef} {JsonContextVarName} = ({contextTypeRef}?)context ?? this; - {JsonSerializerOptionsTypeRef} options = {JsonContextVarName}.Options; - {JsonPropertyInfoTypeRef}[] {PropVarName} = {propertyArrayInstantiationValue}; "); @@ -724,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"; @@ -775,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}, @@ -787,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(@$" @@ -1133,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() { @@ -1174,11 +1115,7 @@ private string GetRootJsonContextImplementation() {{ }} -public {contextTypeName}({JsonSerializerOptionsTypeRef} options) : base(options) -{{ -}} - -private {contextTypeName}({JsonSerializerOptionsTypeRef} options, bool bindOptionsToContext) : base(options, bindOptionsToContext) +public {contextTypeName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}) : base({OptionsLocalVariableName}) {{ }} @@ -1220,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++) {{ @@ -1232,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())); @@ -1251,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); }}"; } @@ -1261,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())); @@ -1280,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) @@ -1302,22 +1238,34 @@ private string GetGetTypeInfoImplementation() }"); // Explicit IJsonTypeInfoResolver implementation - string contextTypeName = _currentContext.ContextType.Name; - sb.AppendLine(); - sb.Append(@$"{JsonTypeInfoTypeRef}? {JsonTypeInfoResolverTypeRef}.GetTypeInfo({TypeTypeRef} type, {JsonSerializerOptionsTypeRef} options) + sb.Append(@$"{JsonTypeInfoTypeRef}? {JsonTypeInfoResolverTypeRef}.GetTypeInfo({TypeTypeRef} type, {JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}) {{ - {contextTypeName} context = this; - - if (options != null && {OptionsInstanceVariableName} != options) + if ({OptionsInstanceVariableName} == {OptionsLocalVariableName}) {{ - context = (s_resolverCache ??= new()).GetValue(options, static options => new {contextTypeName}(options, bindOptionsToContext: false)); + 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}); + }} +"); + } + } - return context.GetTypeInfo(type); + sb.Append($@" + return null; + }} }} - -private {ConditionalWeakTableTypeRef}<{JsonSerializerOptionsTypeRef},{contextTypeName}>? s_resolverCache;"); +"); 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 58803478320930..7cd0fca234ca5d 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -1465,11 +1465,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 98cc904cdf4346..770e45d395b235 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 3c2466038a5d88..86e3c7011e176c 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -960,7 +960,6 @@ public JsonSerializableAttribute(System.Type type) { } public abstract partial class JsonSerializerContext : System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver { protected JsonSerializerContext(System.Text.Json.JsonSerializerOptions? options) { } - protected JsonSerializerContext(System.Text.Json.JsonSerializerOptions? options, bool bindOptionsToContext) { } 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); @@ -1139,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 @@ -1206,6 +1206,8 @@ internal JsonTypeInfo() { } 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.")] 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 f42adbc31fe409..19a3d3c74c3487 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 a0efc0a884331f..bec6fddd9ef790 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); 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 6c0a095061fac9..d077627720bda4 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/Value/NullableConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs index cbc89a4fb7bba6..36f74f39bf8d02 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/JsonSerializerContext.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs index 6d87444a463c28..7e05b6347d1b6b 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 @@ -81,32 +81,11 @@ internal bool CanUseSerializationLogic /// or until is called, where a new options instance is created and bound. /// protected JsonSerializerContext(JsonSerializerOptions? options) - : this(options, bindOptionsToContext: true) - { - } - - /// - /// Creates an instance of and optionally binds it with the indicated . - /// - /// The run time provided options for the context instance. - /// Specify whether the options instance should be bound to the new context. - /// - /// If no instance options are passed, then no options are set until the context is bound using , - /// or until is called, where a new options instance is created and bound. - /// - protected JsonSerializerContext(JsonSerializerOptions? options, bool bindOptionsToContext) { if (options != null) { - if (bindOptionsToContext) - { - options.TypeInfoResolver = this; - Debug.Assert(_options == options, "options.TypeInfoResolver setter did not assign options"); - } - else - { - _options = options; - } + options.TypeInfoResolver = this; + Debug.Assert(_options == options, "options.TypeInfoResolver setter did not assign options"); } } 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 a0a2de2adaded2..a1f67039f0b803 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,6 +23,9 @@ 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) @@ -30,7 +33,17 @@ internal JsonTypeInfo GetOrAddJsonTypeInfo(Type type) InitializeCachingContext(); } - 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) @@ -85,7 +98,6 @@ private void InitializeCachingContext() /// internal sealed class CachingContext { - private readonly ConcurrentDictionary _converterCache = new(); private readonly ConcurrentDictionary _jsonTypeInfoCache = new(); public CachingContext(JsonSerializerOptions options) @@ -96,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(); } } 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 a9c4c257c5f9b3..e92ebb0408288b 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 @@ -55,9 +55,12 @@ public sealed partial class JsonSerializerOptions return converter; } + /// + /// Gets converter for type but does not use TypeInfoResolver + /// internal JsonConverter GetConverterForType(Type typeToConvert) { - JsonConverter converter = GetConverterInternal(typeToConvert); + JsonConverter converter = GetConverterFromOptionsOrReflectionConverter(typeToConvert); Debug.Assert(converter != null); converter = ExpandFactoryConverter(converter, typeToConvert); @@ -122,39 +125,67 @@ public JsonConverter GetConverter(Type typeToConvert) } DefaultJsonTypeInfoResolver.RootDefaultInstance(); - return GetConverterInternal(typeToConvert); + 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; - internal 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)?.Converter; + if (converter == null) + { + ThrowHelper.ThrowNotSupportedException_BuiltInConvertersNotRooted(typeToConvert); + return null!; + } + + return converter; + } - // 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. + 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?) @@ -166,7 +197,7 @@ internal JsonConverter GetConverterFromType(Type typeToConvert) } } - // Priority 4: Attempt to get built-in converter. + // Priority 3: Attempt to get built-in converter. if (converter == null) { converter = DefaultJsonTypeInfoResolver.GetDefaultConverter(typeToConvert); 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 45ae91d6abd886..63c9afc8207b4c 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 @@ -652,28 +652,24 @@ internal void InitializeForReflectionSerializer() IsInitializedForReflectionSerializer = true; } - private JsonTypeInfo GetJsonTypeInfoFromContextOrCreate(Type type) + private JsonTypeInfo? GetTypeInfoInternal(Type type) { IJsonTypeInfoResolver? resolver = _effectiveJsonTypeInfoResolver ?? _typeInfoResolver; JsonTypeInfo? info = resolver?.GetTypeInfo(type, this); - if (info == null) + if (info != null) { - ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type); - return null!; - } - - if (info.Type != type) - { - ThrowHelper.ThrowInvalidOperationException_ResolverTypeNotCompatible(type, info.Type); - } + if (info.Type != type) + { + ThrowHelper.ThrowInvalidOperationException_ResolverTypeNotCompatible(type, info.Type); + } - if (info.Options != this) - { - ThrowHelper.ThrowInvalidOperationException_ResolverTypeInfoOptionsNotCompatible(); + if (info.Options != this) + { + ThrowHelper.ThrowInvalidOperationException_ResolverTypeInfoOptionsNotCompatible(); + } } - info.EnsureConfigured(); return info; } 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 index db4761754ba50c..874815923eacb2 100644 --- 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 @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json.Serialization.Converters; @@ -17,14 +18,24 @@ 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(GetEffectiveConverter( + : 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), - options) - { + options); } internal override JsonParameterInfoValues[] GetParameterInfoValues() 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 e39751e1603e5e..879c1884a8a898 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.Converter 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 b2954c11e4af27..92691bc989eb75 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,12 +37,6 @@ 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) { @@ -90,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/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index a08048bd0d2d37..9403842fc23100 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; @@ -153,6 +154,7 @@ internal virtual void Configure(JsonTypeInfo typeInfo) } SetEffectiveConverter(); + ConverterStrategy = EffectiveConverter.ConverterStrategy; if (IsForTypeInfo) { @@ -574,7 +576,7 @@ JsonConverter GetDictionaryValueConverter(Type dictionaryValueType) // 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); @@ -596,7 +598,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. @@ -612,6 +614,7 @@ internal bool ReadJsonExtensionDataValue(ref ReadStack state, ref Utf8JsonReader internal MemberInfo? MemberInfo { get; set; } + [AllowNull] internal JsonTypeInfo JsonTypeInfo { get diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index c4dd22e9fe688b..47571ed0ae01f7 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 @@ -225,19 +225,14 @@ internal void InitializeForSourceGen(JsonSerializerOptions options, JsonProperty _propertyTypeEqualsTypeToConvert = true; PropertyTypeCanBeNull = PropertyType.CanBeNull(); - JsonTypeInfo propertyTypeInfo = propertyInfo.PropertyTypeInfo; + JsonTypeInfo? propertyTypeInfo = propertyInfo.PropertyTypeInfo; Type declaringType = propertyInfo.DeclaringType; JsonConverter? typedCustomConverter = propertyInfo.Converter; CustomConverter = typedCustomConverter; - JsonConverter? typedNonCustomConverter = propertyTypeInfo.Converter as JsonConverter; + JsonConverter? typedNonCustomConverter = propertyTypeInfo?.Converter as JsonConverter; NonCustomConverter = typedNonCustomConverter; - JsonConverter? typedEffectiveConverter = typedCustomConverter ?? typedNonCustomConverter; - if (typedEffectiveConverter == null) - { - throw new InvalidOperationException(SR.Format(SR.ConverterForPropertyMustBeValid, declaringType, ClrName, typeof(T))); - } IsIgnored = propertyInfo.IgnoreCondition == JsonIgnoreCondition.Always; if (!IsIgnored) @@ -250,7 +245,6 @@ internal void InitializeForSourceGen(JsonSerializerOptions options, JsonProperty DeclaringType = declaringType; IgnoreCondition = propertyInfo.IgnoreCondition; MemberType = propertyInfo.IsProperty ? MemberTypes.Property : MemberTypes.Field; - ConverterStrategy = typedEffectiveConverter.ConverterStrategy; NumberHandling = propertyInfo.NumberHandling; if (IgnoreCondition != null) @@ -278,7 +272,7 @@ internal override void SetEffectiveConverter() JsonSerializerOptions.CheckConverterNullabilityIsSameAsPropertyType(customConverter, PropertyType); } - JsonConverter effectiveConverter = customConverter ?? NonCustomConverter ?? Options.GetConverterForType(PropertyType); + JsonConverter effectiveConverter = customConverter ?? NonCustomConverter ?? Options.GetConverterFromTypeInfo(PropertyType); if (effectiveConverter.TypeToConvert == PropertyType) { EffectiveConverter = effectiveConverter; 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 232c6d559a9c99..cbc23adf63eee9 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 @@ -403,6 +403,8 @@ internal virtual void LateAddProperties() { } /// 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); @@ -663,7 +665,7 @@ 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) 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 de77085ffc52fb..fdba40e285149c 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 @@ -53,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) { @@ -61,7 +61,7 @@ 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; @@ -88,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(); 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 bf56fd47918050..d08c2ce4eefab9 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 @@ -128,6 +128,67 @@ public static void CombiningContexts_Serialization(T value, string expectedJs 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 }"""); @@ -192,5 +253,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/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index 5c9c3119d42187..0c5b56a211f825 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