diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 77f8e7861cfb2..b2b3488b03b3b 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -19,17 +19,19 @@ public sealed partial class JsonSourceGenerator private sealed partial class Emitter { // Literals in generated source - private const string RuntimeCustomConverterFetchingMethodName = "GetRuntimeProvidedCustomConverter"; - private const string OptionsInstanceVariableName = "Options"; - private const string PropInitMethodNameSuffix = "PropInit"; - private const string CtorParamInitMethodNameSuffix = "CtorParamInit"; - private const string SerializeMethodNameSuffix = "Serialize"; private const string CreateValueInfoMethodName = "CreateValueInfo"; + private const string CtorParamInitMethodNameSuffix = "CtorParamInit"; private const string DefaultOptionsStaticVarName = "s_defaultOptions"; private const string DefaultContextBackingStaticVarName = "s_defaultContext"; - private const string WriterVarName = "writer"; - private const string ValueVarName = "value"; + internal const string GetConverterFromFactoryMethodName = "GetConverterFromFactory"; private const string JsonSerializerContextName = "JsonSerializerContext"; + internal const string JsonContextVarName = "jsonContext"; + private const string OptionsInstanceVariableName = "Options"; + private const string PropInitMethodNameSuffix = "PropInit"; + private const string RuntimeCustomConverterFetchingMethodName = "GetRuntimeProvidedCustomConverter"; + private const string SerializeMethodNameSuffix = "Serialize"; + private const string ValueVarName = "value"; + private const string WriterVarName = "writer"; private static AssemblyName _assemblyName = typeof(Emitter).Assembly.GetName(); private static readonly string s_generatedCodeAttributeSource = $@" @@ -99,15 +101,24 @@ public void Emit() { _currentContext = contextGenerationSpec; + bool generateGetConverterMethodForTypes = false; + bool generateGetConverterMethodForProperties = false; + foreach (TypeGenerationSpec typeGenerationSpec in _currentContext.RootSerializableTypes) { GenerateTypeInfo(typeGenerationSpec); + + generateGetConverterMethodForTypes |= typeGenerationSpec.HasTypeFactoryConverter; + generateGetConverterMethodForProperties |= typeGenerationSpec.HasPropertyFactoryConverters; } string contextName = _currentContext.ContextType.Name; // Add root context implementation. - AddSource($"{contextName}.g.cs", GetRootJsonContextImplementation(), isRootContextDef: true); + AddSource( + $"{contextName}.g.cs", + GetRootJsonContextImplementation(generateGetConverterMethodForTypes, generateGetConverterMethodForProperties), + isRootContextDef: true); // Add GetJsonTypeInfo override implementation. AddSource($"{contextName}.GetJsonTypeInfo.g.cs", GetGetTypeInfoImplementation()); @@ -283,54 +294,42 @@ private string GenerateForTypeWithUnknownConverter(TypeGenerationSpec typeMetada string typeCompilableName = typeMetadata.TypeRef; string typeFriendlyName = typeMetadata.TypeInfoPropertyName; - string metadataInitSource; - // 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});"); + if (typeMetadata.IsValueType) { - metadataInitSource = $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic}; - {TypeTypeRef} typeToConvert = typeof({typeCompilableName}); - if (!converter.CanConvert(typeToConvert)) - {{ - {TypeTypeRef}? underlyingType = {NullableTypeRef}.GetUnderlyingType(typeToConvert); - if (underlyingType != null && converter.CanConvert(underlyingType)) + metadataInitSource.Append($@" + if (!converter.CanConvert(typeToConvert)) {{ - {JsonConverterTypeRef}? actualConverter = converter; - - if (converter is {JsonConverterFactoryTypeRef} converterFactory) + {TypeTypeRef}? underlyingType = {NullableTypeRef}.GetUnderlyingType(typeToConvert); + if (underlyingType != null && converter.CanConvert(underlyingType)) {{ - actualConverter = converterFactory.CreateConverter(underlyingType, {OptionsInstanceVariableName}); - - if (actualConverter == null || actualConverter is {JsonConverterFactoryTypeRef}) - {{ - throw new {InvalidOperationExceptionTypeRef}($""JsonConverterFactory '{{converter}} cannot return a 'null' or 'JsonConverterFactory' value.""); - }} + // Allow nullable handling to forward to the underlying type's converter. + converter = {JsonMetadataServicesTypeRef}.GetNullableConverter<{typeCompilableName}>(this.{typeFriendlyName})!; + converter = (({ JsonConverterFactoryTypeRef })converter).CreateConverter(typeToConvert, { OptionsInstanceVariableName })!; }} - - // Allow nullable handling to forward to the underlying type's converter. - converter = {JsonMetadataServicesTypeRef}.GetNullableConverter<{typeCompilableName}>(this.{typeFriendlyName}); - }} - else - {{ - throw new {InvalidOperationExceptionTypeRef}($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'.""); - }} - }} - - _{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, converter);"; + else + {{ + throw new {InvalidOperationExceptionTypeRef}($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'.""); + }} + }}"); } else { - metadataInitSource = $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic}; - {TypeTypeRef} typeToConvert = typeof({typeCompilableName}); - if (!converter.CanConvert(typeToConvert)) - {{ - throw new {InvalidOperationExceptionTypeRef}($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'.""); - }} - - _{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, converter);"; + metadataInitSource.Append($@" + if (!converter.CanConvert(typeToConvert)) + {{ + throw new {InvalidOperationExceptionTypeRef}($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'.""); + }}"); } - return GenerateForType(typeMetadata, metadataInitSource); + metadataInitSource.Append($@" + _{typeFriendlyName} = { JsonMetadataServicesTypeRef }.{ GetCreateValueInfoMethodRef(typeCompilableName)} ({ OptionsInstanceVariableName}, converter); "); + + return GenerateForType(typeMetadata, metadataInitSource.ToString()); } private string GenerateForNullable(TypeGenerationSpec typeMetadata) @@ -484,7 +483,7 @@ private string GenerateFastPathFuncForEnumerable(TypeGenerationSpec typeGenerati Type elementType = valueTypeGenerationSpec.Type; string? writerMethodToCall = GetWriterMethod(elementType); - + string iterationLogic; string valueToWrite; @@ -638,7 +637,6 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata) private string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenerationSpec) { const string PropVarName = "properties"; - const string JsonContextVarName = "jsonContext"; List properties = typeGenerationSpec.PropertyGenSpecList!; @@ -1093,7 +1091,9 @@ private string WrapWithCheckForCustomConverterIfRequired(string source, string t }}"; } - private string GetRootJsonContextImplementation() + private string GetRootJsonContextImplementation( + bool generateGetConverterMethodForTypes, + bool generateGetConverterMethodForProperties) { string contextTypeRef = _currentContext.ContextTypeRef; string contextTypeName = _currentContext.ContextType.Name; @@ -1115,6 +1115,16 @@ private string GetRootJsonContextImplementation() {GetFetchLogicForRuntimeSpecifiedCustomConverter()}"); + if (generateGetConverterMethodForProperties) + { + sb.Append(GetFetchLogicForGetCustomConverter_PropertiesWithFactories()); + } + + if (generateGetConverterMethodForProperties || generateGetConverterMethodForTypes) + { + sb.Append(GetFetchLogicForGetCustomConverter_TypesWithFactories()); + } + return sb.ToString(); } @@ -1173,6 +1183,32 @@ private string GetFetchLogicForRuntimeSpecifiedCustomConverter() }}"; } + private string GetFetchLogicForGetCustomConverter_PropertiesWithFactories() + { + return @$" + +private {JsonConverterTypeRef} {GetConverterFromFactoryMethodName}({JsonConverterFactoryTypeRef} factory) +{{ + return ({JsonConverterTypeRef}) {GetConverterFromFactoryMethodName}(typeof(T), factory); +}}"; + } + + private string GetFetchLogicForGetCustomConverter_TypesWithFactories() + { + return @$" + +private {JsonConverterTypeRef} {GetConverterFromFactoryMethodName}({TypeTypeRef} type, {JsonConverterFactoryTypeRef} factory) +{{ + {JsonConverterTypeRef}? converter = factory.CreateConverter(type, {Emitter.OptionsInstanceVariableName}); + if (converter == null || converter is {JsonConverterFactoryTypeRef}) + {{ + throw new {InvalidOperationExceptionTypeRef}($""The converter '{{factory.GetType()}}' cannot return null or a JsonConverterFactory instance.""); + }} + + return converter; +}}"; + } + private string GetGetTypeInfoImplementation() { StringBuilder sb = new(); diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index cd47bcae0c075..fecdb3792b8c1 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -25,6 +25,7 @@ private sealed class Parser private const string SystemTextJsonNamespace = "System.Text.Json"; private const string JsonConverterAttributeFullName = "System.Text.Json.Serialization.JsonConverterAttribute"; private const string JsonElementFullName = "System.Text.Json.JsonElement"; + private const string JsonConverterFactoryFullName = "System.Text.Json.Serialization.JsonConverterFactory"; private const string JsonIgnoreAttributeFullName = "System.Text.Json.Serialization.JsonIgnoreAttribute"; private const string JsonIgnoreConditionFullName = "System.Text.Json.Serialization.JsonIgnoreCondition"; private const string JsonIncludeAttributeFullName = "System.Text.Json.Serialization.JsonIncludeAttribute"; @@ -571,6 +572,8 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener string? converterInstatiationLogic = null; bool implementsIJsonOnSerialized = false; bool implementsIJsonOnSerializing = false; + bool hasTypeFactoryConverter = false; + bool hasPropertyFactoryConverters = false; IList attributeDataList = CustomAttributeData.GetCustomAttributes(type); foreach (CustomAttributeData attributeData in attributeDataList) @@ -585,7 +588,11 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener else if (!foundDesignTimeCustomConverter && attributeType.GetCompatibleBaseClass(JsonConverterAttributeFullName) != null) { foundDesignTimeCustomConverter = true; - converterInstatiationLogic = GetConverterInstantiationLogic(attributeData); + converterInstatiationLogic = GetConverterInstantiationLogic( + type, + attributeData, + forType: true, + ref hasTypeFactoryConverter); } } @@ -816,6 +823,8 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener for (Type? currentType = type; currentType != null; currentType = currentType.BaseType) { + PropertyGenerationSpec spec; + foreach (PropertyInfo propertyInfo in currentType.GetProperties(bindingFlags)) { bool isVirtual = propertyInfo.IsVirtual(); @@ -826,9 +835,8 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener continue; } - PropertyGenerationSpec spec = GetPropertyGenerationSpec(propertyInfo, isVirtual, generationMode); - CacheMember(spec, ref propGenSpecList, ref ignoredMembers); - propertyOrderSpecified |= spec.Order != 0; + spec = GetPropertyGenerationSpec(propertyInfo, isVirtual, generationMode); + CacheMemberHelper(); } foreach (FieldInfo fieldInfo in currentType.GetFields(bindingFlags)) @@ -838,9 +846,19 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener continue; } - PropertyGenerationSpec spec = GetPropertyGenerationSpec(fieldInfo, isVirtual: false, generationMode); + spec = GetPropertyGenerationSpec(fieldInfo, isVirtual: false, generationMode); + CacheMemberHelper(); + } + + void CacheMemberHelper() + { CacheMember(spec, ref propGenSpecList, ref ignoredMembers); + propertyOrderSpecified |= spec.Order != 0; + hasPropertyFactoryConverters |= spec.HasFactoryConverter; + + // The property type may be implicitly in the context, so add that as well. + hasTypeFactoryConverter |= spec.TypeGenerationSpec.HasTypeFactoryConverter; } } @@ -864,8 +882,10 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener constructionStrategy, nullableUnderlyingTypeMetadata: nullableUnderlyingTypeGenSpec, converterInstatiationLogic, - implementsIJsonOnSerialized, - implementsIJsonOnSerializing); + implementsIJsonOnSerialized : implementsIJsonOnSerialized, + implementsIJsonOnSerializing : implementsIJsonOnSerializing, + hasTypeFactoryConverter : hasTypeFactoryConverter, + hasPropertyFactoryConverters : hasPropertyFactoryConverters); return typeMetadata; } @@ -905,16 +925,95 @@ private static bool PropertyIsOverridenAndIgnored( private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, bool isVirtual, JsonSourceGenerationMode generationMode) { + Type memberCLRType = GetMemberClrType(memberInfo); IList attributeDataList = CustomAttributeData.GetCustomAttributes(memberInfo); - bool hasJsonInclude = false; - JsonIgnoreCondition? ignoreCondition = null; - JsonNumberHandling? numberHandling = null; - string? jsonPropertyName = null; + ProcessCustomAttributes( + attributeDataList, + memberCLRType, + out bool hasJsonInclude, + out string? jsonPropertyName, + out JsonIgnoreCondition? ignoreCondition, + out JsonNumberHandling? numberHandling, + out string? converterInstantiationLogic, + out int order, + out bool hasFactoryConverter); + + ProcessMember( + memberInfo, + memberCLRType, + hasJsonInclude, + out bool isReadOnly, + out bool isPublic, + out bool canUseGetter, + out bool canUseSetter, + out bool getterIsVirtual, + out bool setterIsVirtual); + + string clrName = memberInfo.Name; + string runtimePropertyName = DetermineRuntimePropName(clrName, jsonPropertyName, _currentContextNamingPolicy); + string propertyNameVarName = DeterminePropNameIdentifier(runtimePropertyName); + + return new PropertyGenerationSpec + { + ClrName = clrName, + IsProperty = memberInfo.MemberType == MemberTypes.Property, + IsPublic = isPublic, + IsVirtual = isVirtual, + JsonPropertyName = jsonPropertyName, + RuntimePropertyName = runtimePropertyName, + PropertyNameVarName = propertyNameVarName, + IsReadOnly = isReadOnly, + CanUseGetter = canUseGetter, + CanUseSetter = canUseSetter, + GetterIsVirtual = getterIsVirtual, + SetterIsVirtual = setterIsVirtual, + DefaultIgnoreCondition = ignoreCondition, + NumberHandling = numberHandling, + Order = order, + HasJsonInclude = hasJsonInclude, + TypeGenerationSpec = GetOrAddTypeGenerationSpec(memberCLRType, generationMode), + DeclaringTypeRef = memberInfo.DeclaringType.GetCompilableName(), + ConverterInstantiationLogic = converterInstantiationLogic, + HasFactoryConverter = hasFactoryConverter + }; + } + + private Type GetMemberClrType(MemberInfo memberInfo) + { + if (memberInfo is PropertyInfo propertyInfo) + { + return propertyInfo.PropertyType; + } + + if (memberInfo is FieldInfo fieldInfo) + { + return fieldInfo.FieldType; + } + + throw new InvalidOperationException(); + } + + private void ProcessCustomAttributes( + IList attributeDataList, + Type memberCLRType, + out bool hasJsonInclude, + out string? jsonPropertyName, + out JsonIgnoreCondition? ignoreCondition, + out JsonNumberHandling? numberHandling, + out string? converterInstantiationLogic, + out int order, + out bool hasFactoryConverter) + { + hasJsonInclude = false; + jsonPropertyName = null; + ignoreCondition = default; + numberHandling = default; + converterInstantiationLogic = null; + order = 0; bool foundDesignTimeCustomConverter = false; - string? converterInstantiationLogic = null; - int order = 0; + hasFactoryConverter = false; foreach (CustomAttributeData attributeData in attributeDataList) { @@ -923,7 +1022,11 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, if (!foundDesignTimeCustomConverter && attributeType.GetCompatibleBaseClass(JsonConverterAttributeFullName) != null) { foundDesignTimeCustomConverter = true; - converterInstantiationLogic = GetConverterInstantiationLogic(attributeData); + converterInstantiationLogic = GetConverterInstantiationLogic( + memberCLRType, + attributeData, + forType: false, + ref hasFactoryConverter); } else if (attributeType.Assembly.FullName == SystemTextJsonNamespace) { @@ -974,21 +1077,29 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, } } } + } - Type memberCLRType; - bool isReadOnly; - bool isPublic = false; - bool canUseGetter = false; - bool canUseSetter = false; - bool getterIsVirtual = false; - bool setterIsVirtual = false; + private static void ProcessMember( + MemberInfo memberInfo, + Type memberClrType, + bool hasJsonInclude, + out bool isReadOnly, + out bool isPublic, + out bool canUseGetter, + out bool canUseSetter, + out bool getterIsVirtual, + out bool setterIsVirtual) + { + isPublic = false; + canUseGetter = false; + canUseSetter = false; + getterIsVirtual = false; + setterIsVirtual = false; switch (memberInfo) { case PropertyInfo propertyInfo: { - memberCLRType = propertyInfo.PropertyType; - MethodInfo? getMethod = propertyInfo.GetMethod; MethodInfo? setMethod = propertyInfo.SetMethod; @@ -1031,7 +1142,6 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, break; case FieldInfo fieldInfo: { - memberCLRType = fieldInfo.FieldType; isPublic = fieldInfo.IsPublic; isReadOnly = fieldInfo.IsInitOnly; @@ -1045,39 +1155,15 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, default: throw new InvalidOperationException(); } - - string clrName = memberInfo.Name; - string runtimePropertyName = DetermineRuntimePropName(clrName, jsonPropertyName, _currentContextNamingPolicy); - string propertyNameVarName = DeterminePropNameIdentifier(runtimePropertyName); - - return new PropertyGenerationSpec - { - ClrName = clrName, - IsProperty = memberInfo.MemberType == MemberTypes.Property, - IsPublic = isPublic, - IsVirtual = isVirtual, - JsonPropertyName = jsonPropertyName, - RuntimePropertyName = runtimePropertyName, - PropertyNameVarName = propertyNameVarName, - IsReadOnly = isReadOnly, - CanUseGetter = canUseGetter, - CanUseSetter = canUseSetter, - GetterIsVirtual = getterIsVirtual, - SetterIsVirtual = setterIsVirtual, - DefaultIgnoreCondition = ignoreCondition, - NumberHandling = numberHandling, - Order = order, - HasJsonInclude = hasJsonInclude, - TypeGenerationSpec = GetOrAddTypeGenerationSpec(memberCLRType, generationMode), - DeclaringTypeRef = memberInfo.DeclaringType.GetCompilableName(), - ConverterInstantiationLogic = converterInstantiationLogic - }; } private static bool PropertyAccessorCanBeReferenced(MethodInfo? accessor) => accessor != null && (accessor.IsPublic || accessor.IsAssembly); - private string? GetConverterInstantiationLogic(CustomAttributeData attributeData) + private string? GetConverterInstantiationLogic( + Type type, CustomAttributeData attributeData, + bool forType, // whether for a type or a property + ref bool hasFactoryConverter) { if (attributeData.AttributeType.FullName != JsonConverterAttributeFullName) { @@ -1092,6 +1178,20 @@ private static bool PropertyAccessorCanBeReferenced(MethodInfo? accessor) return null; } + if (converterType.GetCompatibleBaseClass(JsonConverterFactoryFullName) != null) + { + hasFactoryConverter = true; + + if (forType) + { + return $"{Emitter.GetConverterFromFactoryMethodName}(typeof({type.GetCompilableName()}), new {converterType.GetCompilableName()}())"; + } + else + { + return $"{Emitter.JsonContextVarName}.{Emitter.GetConverterFromFactoryMethodName}<{type.GetCompilableName()}>(new {converterType.GetCompilableName()}())"; + } + } + return $"new {converterType.GetCompilableName()}()"; } diff --git a/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs b/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs index 74740cc65af86..886381a22a8be 100644 --- a/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs @@ -89,5 +89,7 @@ internal sealed class PropertyGenerationSpec /// Source code to instantiate design-time specified custom converter. /// public string? ConverterInstantiationLogic { get; init; } + + public bool HasFactoryConverter { get; init; } } } diff --git a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs index 21648615f21fd..be51f949d4fbd 100644 --- a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs @@ -60,6 +60,10 @@ internal class TypeGenerationSpec public string? ConverterInstantiationLogic { get; private set; } + // Only generate certain helper methods if necessary. + public bool HasPropertyFactoryConverters { get; private set; } + public bool HasTypeFactoryConverter { get; private set; } + public string FastPathSerializeMethodName { get @@ -107,7 +111,9 @@ public void Initialize( TypeGenerationSpec? nullableUnderlyingTypeMetadata, string? converterInstantiationLogic, bool implementsIJsonOnSerialized, - bool implementsIJsonOnSerializing) + bool implementsIJsonOnSerializing, + bool hasTypeFactoryConverter, + bool hasPropertyFactoryConverters) { GenerationMode = generationMode; TypeRef = type.GetCompilableName(); @@ -127,6 +133,8 @@ public void Initialize( ConverterInstantiationLogic = converterInstantiationLogic; ImplementsIJsonOnSerialized = implementsIJsonOnSerialized; ImplementsIJsonOnSerializing = implementsIJsonOnSerializing; + HasTypeFactoryConverter = hasTypeFactoryConverter; + HasPropertyFactoryConverters = hasPropertyFactoryConverters; } public bool TryFilterSerializableProps( diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs index ab9352249395a..47bfe5a63a880 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs @@ -33,8 +33,12 @@ public interface ITestContext public JsonTypeInfo ClassWithEnumAndNullable { get; } public JsonTypeInfo ClassWithCustomConverter { get; } public JsonTypeInfo StructWithCustomConverter { get; } + public JsonTypeInfo ClassWithCustomConverterFactory { get; } + public JsonTypeInfo StructWithCustomConverterFactory { get; } public JsonTypeInfo ClassWithCustomConverterProperty { get; } public JsonTypeInfo StructWithCustomConverterProperty { get; } + public JsonTypeInfo ClassWithCustomConverterPropertyFactory { get; } + public JsonTypeInfo StructWithCustomConverterPropertyFactory { get; } public JsonTypeInfo ClassWithBadCustomConverter { get; } public JsonTypeInfo StructWithBadCustomConverter { get; } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs index 6db87ac740cae..201ef07e5253b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs @@ -29,8 +29,12 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))] [JsonSerializable(typeof(ClassWithCustomConverter))] [JsonSerializable(typeof(StructWithCustomConverter))] + [JsonSerializable(typeof(ClassWithCustomConverterFactory))] + [JsonSerializable(typeof(StructWithCustomConverterFactory))] [JsonSerializable(typeof(ClassWithCustomConverterProperty))] [JsonSerializable(typeof(StructWithCustomConverterProperty))] + [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory))] + [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory))] [JsonSerializable(typeof(ClassWithBadCustomConverter))] [JsonSerializable(typeof(StructWithBadCustomConverter))] internal partial class MetadataAndSerializationContext : JsonSerializerContext, ITestContext @@ -63,12 +67,17 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(MetadataAndSerializationContext.Default.MyNestedClass.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.MyNestedNestedClass.Serialize); Assert.Null(MetadataAndSerializationContext.Default.ObjectArray.Serialize); + Assert.Null(MetadataAndSerializationContext.Default.SampleEnum.Serialize); Assert.Null(MetadataAndSerializationContext.Default.String.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithEnumAndNullable.Serialize); Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverter); Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverter); + Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverterFactory); + Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverterFactory); Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverterProperty); Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverterProperty); + Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverterPropertyFactory); + Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverterPropertyFactory); Assert.Throws(() => MetadataAndSerializationContext.Default.ClassWithBadCustomConverter); Assert.Throws(() => MetadataAndSerializationContext.Default.StructWithBadCustomConverter); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs index 50f52d6f9aa6e..945082d7e5d7b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs @@ -28,10 +28,12 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] - [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] - [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(StructWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(ClassWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] internal partial class MetadataWithPerTypeAttributeContext : JsonSerializerContext, ITestContext @@ -62,10 +64,17 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataWithPerTypeAttributeContext.Default.MyNestedClass.Serialize); Assert.Null(MetadataWithPerTypeAttributeContext.Default.MyNestedNestedClass.Serialize); Assert.Null(MetadataWithPerTypeAttributeContext.Default.ObjectArray.Serialize); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.SampleEnum.Serialize); Assert.Null(MetadataWithPerTypeAttributeContext.Default.String.Serialize); Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithEnumAndNullable.Serialize); Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverter.Serialize); Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverter.Serialize); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverterFactory.Serialize); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverterFactory.Serialize); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverterProperty.Serialize); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverterProperty.Serialize); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverterPropertyFactory.Serialize); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverterPropertyFactory.Serialize); Assert.Throws(() => MetadataWithPerTypeAttributeContext.Default.ClassWithBadCustomConverter.Serialize); Assert.Throws(() => MetadataWithPerTypeAttributeContext.Default.StructWithBadCustomConverter.Serialize); } @@ -94,8 +103,12 @@ public override void EnsureFastPathGeneratedAsExpected() [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))] [JsonSerializable(typeof(ClassWithCustomConverter))] [JsonSerializable(typeof(StructWithCustomConverter))] + [JsonSerializable(typeof(ClassWithCustomConverterFactory))] + [JsonSerializable(typeof(StructWithCustomConverterFactory))] [JsonSerializable(typeof(ClassWithCustomConverterProperty))] [JsonSerializable(typeof(StructWithCustomConverterProperty))] + [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory))] + [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory))] [JsonSerializable(typeof(ClassWithBadCustomConverter))] [JsonSerializable(typeof(StructWithBadCustomConverter))] internal partial class MetadataContext : JsonSerializerContext, ITestContext @@ -103,6 +116,27 @@ internal partial class MetadataContext : JsonSerializerContext, ITestContext public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata; } + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum EnumWrittenAsString + { + A = 1 + } + + [JsonSerializable(typeof(EnumWrittenAsString))] + public partial class ContextWithExplicitStringEnum : JsonSerializerContext + { + } + + public class PocoWithEnum + { + public EnumWrittenAsString MyEnum { get; set; } + } + + [JsonSerializable(typeof(PocoWithEnum))] + public partial class ContextWithImplicitStringEnum : JsonSerializerContext + { + } + public sealed class MetadataContextTests : RealWorldContextTests { public MetadataContextTests() : base(MetadataContext.Default, (options) => new MetadataContext(options)) { } @@ -128,14 +162,56 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataContext.Default.MyNestedClass.Serialize); Assert.Null(MetadataContext.Default.MyNestedNestedClass.Serialize); Assert.Null(MetadataContext.Default.ObjectArray.Serialize); + Assert.Null(MetadataContext.Default.SampleEnum.Serialize); Assert.Null(MetadataContext.Default.String.Serialize); Assert.Null(MetadataContext.Default.ClassWithEnumAndNullable.Serialize); Assert.Null(MetadataContext.Default.ClassWithCustomConverter.Serialize); Assert.Null(MetadataContext.Default.StructWithCustomConverter.Serialize); + Assert.Null(MetadataContext.Default.ClassWithCustomConverterFactory.Serialize); + Assert.Null(MetadataContext.Default.StructWithCustomConverterFactory.Serialize); Assert.Null(MetadataContext.Default.ClassWithCustomConverterProperty.Serialize); Assert.Null(MetadataContext.Default.StructWithCustomConverterProperty.Serialize); + Assert.Null(MetadataContext.Default.ClassWithCustomConverterPropertyFactory.Serialize); + Assert.Null(MetadataContext.Default.StructWithCustomConverterPropertyFactory.Serialize); Assert.Throws(() => MetadataContext.Default.ClassWithBadCustomConverter.Serialize); Assert.Throws(() => MetadataContext.Default.StructWithBadCustomConverter.Serialize); } + + [Fact] + public void EnsureHelperMethodGenerated_TypeFactory() + { + // There are 2 helper methods generated for obtaining a converter from a factory: + // - JsonConverter version that is property-based (that calls the one below) + // - JsonConverter version that is Type-based + // and this test verifies the latter one is generated. Other tests also have property-level + // factories and thus verify both are created. + + const string Json = "\"A\""; + + EnumWrittenAsString obj = EnumWrittenAsString.A; + + string json = JsonSerializer.Serialize(obj, ContextWithExplicitStringEnum.Default.EnumWrittenAsString); + Assert.Equal(Json, json); + + obj = JsonSerializer.Deserialize(Json, ContextWithExplicitStringEnum.Default.EnumWrittenAsString); + Assert.Equal(EnumWrittenAsString.A, obj); + } + + [Fact] + public void EnsureHelperMethodGenerated_ImplicitPropertyFactory() + { + // ContextWithImplicitStringEnum does not have an entry for EnumWrittenAsString since it is + // implictly added by PocoWithEnum. Verify helper methods are still being created properly. + + const string Json = "{\"MyEnum\":\"A\"}"; + + PocoWithEnum obj = new() { MyEnum = EnumWrittenAsString.A }; + + string json = JsonSerializer.Serialize(obj, ContextWithImplicitStringEnum.Default.PocoWithEnum); + Assert.Equal(Json, json); + + obj = JsonSerializer.Deserialize(Json, ContextWithImplicitStringEnum.Default.PocoWithEnum); + Assert.Equal(EnumWrittenAsString.A, obj.MyEnum); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs index d45996341f800..ae0c2897daa98 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs @@ -28,8 +28,12 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] internal partial class MixedModeContext : JsonSerializerContext, ITestContext @@ -61,12 +65,17 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(MixedModeContext.Default.MyNestedClass.Serialize); Assert.NotNull(MixedModeContext.Default.MyNestedNestedClass.Serialize); Assert.Null(MixedModeContext.Default.ObjectArray.Serialize); + Assert.Null(MixedModeContext.Default.SampleEnum.Serialize); Assert.Null(MixedModeContext.Default.String.Serialize); Assert.NotNull(MixedModeContext.Default.ClassWithEnumAndNullable.Serialize); Assert.Null(MixedModeContext.Default.ClassWithCustomConverter.Serialize); Assert.Null(MixedModeContext.Default.StructWithCustomConverter.Serialize); + Assert.Null(MixedModeContext.Default.ClassWithCustomConverterFactory.Serialize); + Assert.Null(MixedModeContext.Default.StructWithCustomConverterFactory.Serialize); Assert.Null(MixedModeContext.Default.ClassWithCustomConverterProperty.Serialize); Assert.Null(MixedModeContext.Default.StructWithCustomConverterProperty.Serialize); + Assert.Null(MixedModeContext.Default.ClassWithCustomConverterPropertyFactory.Serialize); + Assert.Null(MixedModeContext.Default.StructWithCustomConverterPropertyFactory.Serialize); Assert.Throws(() => MixedModeContext.Default.ClassWithBadCustomConverter.Serialize); Assert.Throws(() => MixedModeContext.Default.StructWithBadCustomConverter.Serialize); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs index 5fc9bf7fb73f1..fef2dddc0fec5 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; @@ -127,6 +127,23 @@ public virtual void RoundTripWithCustomConverter_Class() Assert.Equal(42, obj.MyInt); } + [Fact] + public virtual void RoundTripWithCustomConverterFactory_Class() + { + const string Json = "{\"MyInt\":142}"; + + ClassWithCustomConverterFactory obj = new() + { + MyInt = 42 + }; + + string json = JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterFactory); + Assert.Equal(Json, json); + + obj = JsonSerializer.Deserialize(Json, DefaultContext.ClassWithCustomConverterFactory); + Assert.Equal(42, obj.MyInt); + } + [Fact] public virtual void RoundTripWithCustomConverter_Struct() { @@ -198,6 +215,68 @@ public virtual void RoundtripWithCustomConverterProperty_Struct() Assert.Equal(42, obj.Property.Value); } + [Fact] + public virtual void RoundTripWithCustomPropertyConverterFactory_Class() + { + const string Json = "{\"MyEnum\":\"A\"}"; + + ClassWithCustomConverterPropertyFactory obj = new() + { + MyEnum = SampleEnum.A + }; + + if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization) + { + Assert.Throws(() => JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterPropertyFactory)); + } + else + { + string json = JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterPropertyFactory); + Assert.Equal(Json, json); + } + + if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization) + { + Assert.Throws(() => JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterPropertyFactory)); + } + else + { + obj = JsonSerializer.Deserialize(Json, DefaultContext.ClassWithCustomConverterPropertyFactory); + Assert.Equal(SampleEnum.A, obj.MyEnum); + } + } + + [Fact] + public virtual void RoundTripWithCustomPropertyConverterFactory_Struct() + { + const string Json = "{\"MyEnum\":\"A\"}"; + + StructWithCustomConverterPropertyFactory obj = new() + { + MyEnum = SampleEnum.A + }; + + if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization) + { + Assert.Throws(() => JsonSerializer.Serialize(obj, DefaultContext.StructWithCustomConverterPropertyFactory)); + } + else + { + string json = JsonSerializer.Serialize(obj, DefaultContext.StructWithCustomConverterPropertyFactory); + Assert.Equal(Json, json); + } + + if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization) + { + Assert.Throws(() => JsonSerializer.Serialize(obj, DefaultContext.StructWithCustomConverterPropertyFactory)); + } + else + { + obj = JsonSerializer.Deserialize(Json, DefaultContext.StructWithCustomConverterPropertyFactory); + Assert.Equal(SampleEnum.A, obj.MyEnum); + } + } + [Fact] public virtual void BadCustomConverter_Class() { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs index ae87218f2f782..501d671f9863b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs @@ -29,8 +29,12 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))] [JsonSerializable(typeof(ClassWithCustomConverter))] [JsonSerializable(typeof(StructWithCustomConverter))] + [JsonSerializable(typeof(ClassWithCustomConverterFactory))] + [JsonSerializable(typeof(StructWithCustomConverterFactory))] [JsonSerializable(typeof(ClassWithCustomConverterProperty))] [JsonSerializable(typeof(StructWithCustomConverterProperty))] + [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory))] + [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory))] [JsonSerializable(typeof(ClassWithBadCustomConverter))] [JsonSerializable(typeof(StructWithBadCustomConverter))] internal partial class SerializationContext : JsonSerializerContext, ITestContext @@ -60,8 +64,12 @@ internal partial class SerializationContext : JsonSerializerContext, ITestContex [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] internal partial class SerializationWithPerTypeAttributeContext : JsonSerializerContext, ITestContext @@ -92,8 +100,12 @@ internal partial class SerializationWithPerTypeAttributeContext : JsonSerializer [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] internal partial class SerializationContextWithCamelCase : JsonSerializerContext, ITestContext @@ -135,6 +147,8 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(SerializationContext.Default.ClassWithEnumAndNullable.Serialize); Assert.Null(SerializationContext.Default.ClassWithCustomConverter.Serialize); Assert.Null(SerializationContext.Default.StructWithCustomConverter.Serialize); + Assert.Null(SerializationContext.Default.ClassWithCustomConverterFactory.Serialize); + Assert.Null(SerializationContext.Default.StructWithCustomConverterFactory.Serialize); Assert.Null(SerializationContext.Default.ClassWithCustomConverterProperty.Serialize); Assert.Null(SerializationContext.Default.StructWithCustomConverterProperty.Serialize); Assert.Throws(() => SerializationContext.Default.ClassWithBadCustomConverter.Serialize); @@ -395,12 +409,17 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.MyNestedClass.Serialize); Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.MyNestedNestedClass.Serialize); Assert.Null(SerializationWithPerTypeAttributeContext.Default.ObjectArray.Serialize); + Assert.Null(SerializationWithPerTypeAttributeContext.Default.SampleEnum.Serialize); Assert.Null(SerializationWithPerTypeAttributeContext.Default.String.Serialize); Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.ClassWithEnumAndNullable.Serialize); Assert.Null(SerializationWithPerTypeAttributeContext.Default.ClassWithCustomConverter.Serialize); Assert.Null(SerializationWithPerTypeAttributeContext.Default.StructWithCustomConverter.Serialize); + Assert.Null(SerializationWithPerTypeAttributeContext.Default.ClassWithCustomConverterFactory.Serialize); + Assert.Null(SerializationWithPerTypeAttributeContext.Default.StructWithCustomConverterFactory.Serialize); Assert.Null(SerializationWithPerTypeAttributeContext.Default.ClassWithCustomConverterProperty.Serialize); Assert.Null(SerializationWithPerTypeAttributeContext.Default.StructWithCustomConverterProperty.Serialize); + Assert.Null(SerializationWithPerTypeAttributeContext.Default.ClassWithCustomConverterPropertyFactory.Serialize); + Assert.Null(SerializationWithPerTypeAttributeContext.Default.StructWithCustomConverterPropertyFactory.Serialize); Assert.Throws(() => SerializationWithPerTypeAttributeContext.Default.ClassWithBadCustomConverter.Serialize); Assert.Throws(() => SerializationWithPerTypeAttributeContext.Default.StructWithBadCustomConverter.Serialize); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj index e848e3e568992..610accb41c322 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj @@ -71,12 +71,13 @@ + - + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.CustomConverters.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.CustomConverters.cs new file mode 100644 index 0000000000000..27cf7fb55586e --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.CustomConverters.cs @@ -0,0 +1,277 @@ +// 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; + +namespace System.Text.Json.SourceGeneration.Tests +{ + /// + /// Custom converter that adds\substract 100 from MyIntProperty. + /// + public class CustomConverter_ClassWithCustomConverter : JsonConverter + { + public override ClassWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("No StartObject"); + } + + ClassWithCustomConverter obj = new(); + + reader.Read(); + if (reader.TokenType != JsonTokenType.PropertyName && + reader.GetString() != "MyInt") + { + throw new JsonException("Wrong property name"); + } + + reader.Read(); + obj.MyInt = reader.GetInt32() - 100; + + reader.Read(); + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("No EndObject"); + } + + return obj; + } + + public override void Write(Utf8JsonWriter writer, ClassWithCustomConverter value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteNumber(nameof(ClassWithCustomConverter.MyInt), value.MyInt + 100); + writer.WriteEndObject(); + } + } + + /// + /// Custom converter that adds\substract 100 from MyIntProperty. + /// + public class CustomConverter_ClassWithCustomConverterFactory : JsonConverter + { + public override ClassWithCustomConverterFactory Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("No StartObject"); + } + + ClassWithCustomConverterFactory obj = new(); + + reader.Read(); + if (reader.TokenType != JsonTokenType.PropertyName && + reader.GetString() != "MyInt") + { + throw new JsonException("Wrong property name"); + } + + reader.Read(); + obj.MyInt = reader.GetInt32() - 100; + + reader.Read(); + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("No EndObject"); + } + + return obj; + } + + public override void Write(Utf8JsonWriter writer, ClassWithCustomConverterFactory value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteNumber(nameof(ClassWithCustomConverterFactory.MyInt), value.MyInt + 100); + writer.WriteEndObject(); + } + } + + /// + /// Custom converter that adds\substract 100 from MyIntProperty. + /// + public class CustomConverter_StructWithCustomConverter : JsonConverter + { + public override StructWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("No StartObject"); + } + + StructWithCustomConverter obj = new(); + + reader.Read(); + if (reader.TokenType != JsonTokenType.PropertyName && + reader.GetString() != "MyInt") + { + throw new JsonException("Wrong property name"); + } + + reader.Read(); + obj.MyInt = reader.GetInt32() - 100; + + reader.Read(); + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("No EndObject"); + } + + return obj; + } + + public override void Write(Utf8JsonWriter writer, StructWithCustomConverter value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteNumber(nameof(StructWithCustomConverter.MyInt), value.MyInt + 100); + writer.WriteEndObject(); + } + } + + /// + /// Custom converter that adds\substract 100 from MyIntProperty. + /// + public class CustomConverter_StructWithCustomConverterFactory : JsonConverter + { + public override StructWithCustomConverterFactory Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("No StartObject"); + } + + StructWithCustomConverterFactory obj = new(); + + reader.Read(); + if (reader.TokenType != JsonTokenType.PropertyName && + reader.GetString() != "MyInt") + { + throw new JsonException("Wrong property name"); + } + + reader.Read(); + obj.MyInt = reader.GetInt32() - 100; + + reader.Read(); + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("No EndObject"); + } + + return obj; + } + + public override void Write(Utf8JsonWriter writer, StructWithCustomConverterFactory value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteNumber(nameof(StructWithCustomConverterFactory.MyInt), value.MyInt + 100); + writer.WriteEndObject(); + } + } + + public class CustomConverterFactory : JsonConverterFactory + { + public CustomConverterFactory() + { + } + + public override bool CanConvert(Type typeToConvert) + { + return ( + typeToConvert == typeof(StructWithCustomConverterFactory) || + typeToConvert == typeof(ClassWithCustomConverterFactory)); + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + if (typeToConvert == typeof(StructWithCustomConverterFactory)) + { + return new CustomConverter_StructWithCustomConverterFactory(); + } + + if (typeToConvert == typeof(ClassWithCustomConverterFactory)) + { + return new CustomConverter_ClassWithCustomConverterFactory(); + } + + throw new InvalidOperationException("Not expected."); + } + } + + [JsonConverter(typeof(CustomConverter_ClassWithCustomConverter))] + public class ClassWithCustomConverter + { + public int MyInt { get; set; } + } + + [JsonConverter(typeof(CustomConverter_StructWithCustomConverter))] + public struct StructWithCustomConverter + { + public int MyInt { get; set; } + } + + [JsonConverter(typeof(CustomConverterFactory))] + public class ClassWithCustomConverterFactory + { + public int MyInt { get; set; } + } + + [JsonConverter(typeof(CustomConverterFactory))] + public struct StructWithCustomConverterFactory + { + public int MyInt { get; set; } + } + + public class ClassWithCustomConverterProperty + { + [JsonConverter(typeof(NestedPocoCustomConverter))] + public NestedPoco Property { get; set; } + + public class NestedPoco + { + public int Value { get; set; } + } + + public class NestedPocoCustomConverter : JsonConverter + { + public override NestedPoco? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => new NestedPoco { Value = reader.GetInt32() }; + public override void Write(Utf8JsonWriter writer, NestedPoco value, JsonSerializerOptions options) => writer.WriteNumberValue(value.Value); + } + } + + public struct StructWithCustomConverterProperty + { + [JsonConverter(typeof(ClassWithCustomConverterProperty.NestedPocoCustomConverter))] + public ClassWithCustomConverterProperty.NestedPoco Property { get; set; } + } + + public struct ClassWithCustomConverterPropertyFactory + { + [JsonConverter(typeof(JsonStringEnumConverter))] // This converter is a JsonConverterFactory + public SampleEnum MyEnum { get; set; } + } + + public struct StructWithCustomConverterPropertyFactory + { + [JsonConverter(typeof(JsonStringEnumConverter))] // This converter is a JsonConverterFactory + public SampleEnum MyEnum { get; set; } + } + + public enum SampleEnum + { + A = 1, + B = 2 + } + + [JsonConverter(typeof(CustomConverter_StructWithCustomConverter))] // Invalid + public class ClassWithBadCustomConverter + { + public int MyInt { get; set; } + } + + [JsonConverter(typeof(CustomConverter_StructWithCustomConverter))] // Invalid + public struct StructWithBadCustomConverter + { + public int MyInt { get; set; } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs index 05e3b0e3c0aed..887f65b80da83 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs @@ -151,133 +151,4 @@ public class JsonMessage } internal struct MyStruct { } - - /// - /// Custom converter that adds\substract 100 from MyIntProperty. - /// - public class CustomConverterForClass : JsonConverter - { - public override ClassWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException("No StartObject"); - } - - ClassWithCustomConverter obj = new(); - - reader.Read(); - if (reader.TokenType != JsonTokenType.PropertyName && - reader.GetString() != "MyInt") - { - throw new JsonException("Wrong property name"); - } - - reader.Read(); - obj.MyInt = reader.GetInt32() - 100; - - reader.Read(); - if (reader.TokenType != JsonTokenType.EndObject) - { - throw new JsonException("No EndObject"); - } - - return obj; - } - - public override void Write(Utf8JsonWriter writer, ClassWithCustomConverter value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - writer.WriteNumber(nameof(ClassWithCustomConverter.MyInt), value.MyInt + 100); - writer.WriteEndObject(); - } - } - - [JsonConverter(typeof(CustomConverterForClass))] - public class ClassWithCustomConverter - { - public int MyInt { get; set; } - } - - public class ClassWithCustomConverterProperty - { - [JsonConverter(typeof(NestedPocoCustomConverter))] - public NestedPoco Property { get; set; } - - public class NestedPoco - { - public int Value { get; set; } - } - - public class NestedPocoCustomConverter : JsonConverter - { - public override NestedPoco? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => new NestedPoco { Value = reader.GetInt32() }; - public override void Write(Utf8JsonWriter writer, NestedPoco value, JsonSerializerOptions options) => writer.WriteNumberValue(value.Value); - } - } - - public struct StructWithCustomConverterProperty - { - [JsonConverter(typeof(ClassWithCustomConverterProperty.NestedPocoCustomConverter))] - public ClassWithCustomConverterProperty.NestedPoco Property { get; set; } - } - - [JsonConverter(typeof(CustomConverterForStruct))] // Invalid - public class ClassWithBadCustomConverter - { - public int MyInt { get; set; } - } - - /// - /// Custom converter that adds\substract 100 from MyIntProperty. - /// - public class CustomConverterForStruct : JsonConverter - { - public override StructWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException("No StartObject"); - } - - StructWithCustomConverter obj = new(); - - reader.Read(); - if (reader.TokenType != JsonTokenType.PropertyName && - reader.GetString() != "MyInt") - { - throw new JsonException("Wrong property name"); - } - - reader.Read(); - obj.MyInt = reader.GetInt32() - 100; - - reader.Read(); - if (reader.TokenType != JsonTokenType.EndObject) - { - throw new JsonException("No EndObject"); - } - - return obj; - } - - public override void Write(Utf8JsonWriter writer, StructWithCustomConverter value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - writer.WriteNumber(nameof(StructWithCustomConverter.MyInt), value.MyInt + 100); - writer.WriteEndObject(); - } - } - - [JsonConverter(typeof(CustomConverterForStruct))] - public struct StructWithCustomConverter - { - public int MyInt { get; set; } - } - - [JsonConverter(typeof(CustomConverterForClass))] // Invalid - public struct StructWithBadCustomConverter - { - public int MyInt { get; set; } - } }