* Fix NativeAOT support for JsonStringEnumConverter.
* Fix merge issue.
* Implement JsonStringEnumConverter<TEnum>.
* Remove leftover comments.
* Revert unnecessary suppression.
* Update compatibility suppressions.
* Add missing XML docs
* Fix capitalization.
* add local project compatibility suppressions
* apply suggestion in all location
* Fix failing test.
* Ensure both converter factories throw an appropriate exception when passed invalid inputs.
public INamedTypeSymbol? JsonUnmappedMemberHandlingAttributeType => GetOrResolveType("System.Text.Json.Serialization.JsonUnmappedMemberHandlingAttribute", ref _JsonUnmappedMemberHandlingAttributeType);
private Option<INamedTypeSymbol?> _JsonUnmappedMemberHandlingAttributeType;
+ public INamedTypeSymbol? JsonStringEnumConverterType => GetOrResolveType("System.Text.Json.Serialization.JsonStringEnumConverter", ref _JsonStringEnumConverterType);
+ private Option<INamedTypeSymbol?> _JsonStringEnumConverterType;
+
// Unsupported types
public INamedTypeSymbol? DelegateType => _DelegateType ??= Compilation.GetSpecialType(SpecialType.System_Delegate);
private INamedTypeSymbol? _DelegateType;
category: JsonConstants.SystemTextJsonSourceGenerationName,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor JsonStringEnumConverterNotSupportedInAot { get; } = new DiagnosticDescriptor(
+ id: "SYSLIB1040",
+ title: new LocalizableResourceString(nameof(SR.JsonStringEnumConverterNotSupportedTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
+ messageFormat: new LocalizableResourceString(nameof(SR.JsonStringEnumConverterNotSupportedMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
+ category: JsonConstants.SystemTextJsonSourceGenerationName,
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
}
}
}
string typeFQN = typeMetadata.TypeRef.FullyQualifiedName;
string typeInfoPropertyName = typeMetadata.TypeInfoPropertyName;
- string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.{typeInfoPropertyName}Converter);";
- GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource);
+
+ GenerateTypeInfoFactoryHeader(writer, typeMetadata);
+ writer.WriteLine($"""
+ {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.{typeInfoPropertyName}Converter);
+ """);
+
+ GenerateTypeInfoFactoryFooter(writer);
return CompleteSourceFileAndReturnText(writer);
}
SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec);
string typeFQN = typeMetadata.TypeRef.FullyQualifiedName;
- string metadataInitSource = $$"""
- {{JsonConverterTypeRef}} converter = {{ExpandConverterMethodName}}(typeof({{typeFQN}}), new {{typeMetadata.ConverterType.FullyQualifiedName}}(), {{OptionsLocalVariableName}});
- {{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.{{GetCreateValueInfoMethodRef(typeFQN)}} ({{OptionsLocalVariableName}}, converter);
- """;
+ string converterFQN = typeMetadata.ConverterType.FullyQualifiedName;
+
+ GenerateTypeInfoFactoryHeader(writer, typeMetadata);
- GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource);
+ writer.WriteLine($"""
+ {JsonConverterTypeRef} converter = {ExpandConverterMethodName}(typeof({typeFQN}), new {converterFQN}(), {OptionsLocalVariableName});
+ {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)} ({OptionsLocalVariableName}, converter);
+ """);
+
+ GenerateTypeInfoFactoryFooter(writer);
return CompleteSourceFileAndReturnText(writer);
}
string typeFQN = typeMetadata.TypeRef.FullyQualifiedName;
string underlyingTypeFQN = typeMetadata.NullableUnderlyingType.FullyQualifiedName;
- string metadataInitSource = $$"""
- {{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.{{GetCreateValueInfoMethodRef(typeFQN)}}(
- {{OptionsLocalVariableName}},
- {{JsonMetadataServicesTypeRef}}.GetNullableConverter<{{underlyingTypeFQN}}>({{OptionsLocalVariableName}}));
- """;
+ GenerateTypeInfoFactoryHeader(writer, typeMetadata);
+
+ writer.WriteLine($$"""
+ {{JsonConverterTypeRef}} converter = {{JsonMetadataServicesTypeRef}}.GetNullableConverter<{{underlyingTypeFQN}}>({{OptionsLocalVariableName}});
+ {{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.{{GetCreateValueInfoMethodRef(typeFQN)}}({{OptionsLocalVariableName}}, converter);
+ """);
- GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource);
+ GenerateTypeInfoFactoryFooter(writer);
return CompleteSourceFileAndReturnText(writer);
}
SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec);
string typeFQN = typeMetadata.TypeRef.FullyQualifiedName;
- string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetUnsupportedTypeConverter<{typeFQN}>());";
- GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource);
+ GenerateTypeInfoFactoryHeader(writer, typeMetadata);
+ writer.WriteLine($"""
+ {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetUnsupportedTypeConverter<{typeFQN}>());
+ """);
+
+ GenerateTypeInfoFactoryFooter(writer);
return CompleteSourceFileAndReturnText(writer);
}
SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec);
string typeFQN = typeMetadata.TypeRef.FullyQualifiedName;
- string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetEnumConverter<{typeFQN}>({OptionsLocalVariableName}));";
- GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource);
+
+ GenerateTypeInfoFactoryHeader(writer, typeMetadata);
+ writer.WriteLine($"""
+ {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetEnumConverter<{typeFQN}>({OptionsLocalVariableName}));
+ """);
+
+ GenerateTypeInfoFactoryFooter(writer);
return CompleteSourceFileAndReturnText(writer);
}
break;
}
- string metadataInitSource = $$"""
- var {{InfoVarName}} = new {{JsonCollectionInfoValuesTypeRef}}<{{typeFQN}}>()
+ GenerateTypeInfoFactoryHeader(writer, typeGenerationSpec);
+
+ writer.WriteLine($$"""
+ var {{InfoVarName}} = new {{JsonCollectionInfoValuesTypeRef}}<{{typeFQN}}>
{
{{ObjectCreatorPropName}} = {{FormatDefaultConstructorExpr(typeGenerationSpec)}},
{{NumberHandlingPropName}} = {{GetNumberHandlingAsStr(typeGenerationSpec.NumberHandling)}},
};
{{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.{{createCollectionMethodExpr}};
- """;
+ """);
- GenerateTypeInfoProperty(writer, typeGenerationSpec, metadataInitSource);
+ GenerateTypeInfoFactoryFooter(writer);
if (serializeMethodName != null)
{
const string ObjectInfoVarName = "objectInfo";
string genericArg = typeMetadata.TypeRef.FullyQualifiedName;
- string metadataInitSource = $$"""
- var {{ObjectInfoVarName}} = new {{JsonObjectInfoValuesTypeRef}}<{{genericArg}}>()
+ GenerateTypeInfoFactoryHeader(writer, typeMetadata);
+
+ writer.WriteLine($$"""
+ var {{ObjectInfoVarName}} = new {{JsonObjectInfoValuesTypeRef}}<{{genericArg}}>
{
{{ObjectCreatorPropName}} = {{creatorInvocation}},
ObjectWithParameterizedConstructorCreator = {{parameterizedCreatorInvocation}},
};
{{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.CreateObjectInfo<{{typeMetadata.TypeRef.FullyQualifiedName}}>({{OptionsLocalVariableName}}, {{ObjectInfoVarName}});
- """;
+ """);
- if (typeMetadata.UnmappedMemberHandling != null)
+ if (typeMetadata is { UnmappedMemberHandling: not null } or { PreferredPropertyObjectCreationHandling: not null })
{
- metadataInitSource += $"""
-
- {JsonTypeInfoReturnValueLocalVariableName}.{UnmappedMemberHandlingPropName} = {GetUnmappedMemberHandlingAsStr(typeMetadata.UnmappedMemberHandling.Value)};
- """;
- }
+ writer.WriteLine();
- if (typeMetadata.PreferredPropertyObjectCreationHandling != null)
- {
- metadataInitSource += $"""
+ if (typeMetadata.UnmappedMemberHandling != null)
+ {
+ writer.WriteLine($"{JsonTypeInfoReturnValueLocalVariableName}.{UnmappedMemberHandlingPropName} = {GetUnmappedMemberHandlingAsStr(typeMetadata.UnmappedMemberHandling.Value)};");
+ }
- {JsonTypeInfoReturnValueLocalVariableName}.{PreferredPropertyObjectCreationHandlingPropName} = {GetObjectCreationHandlingAsStr(typeMetadata.PreferredPropertyObjectCreationHandling.Value)};
- """;
+ if (typeMetadata.PreferredPropertyObjectCreationHandling != null)
+ {
+ writer.WriteLine($"{JsonTypeInfoReturnValueLocalVariableName}.{PreferredPropertyObjectCreationHandlingPropName} = {GetObjectCreationHandlingAsStr(typeMetadata.PreferredPropertyObjectCreationHandling.Value)};");
+ }
}
- GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource);
+ GenerateTypeInfoFactoryFooter(writer);
if (propInitMethodName != null)
{
string? converterInstantiationExpr = null;
if (property.ConverterType != null)
{
+ string converterFQN = property.ConverterType.FullyQualifiedName;
TypeRef? nullableUnderlyingType = _typeIndex[property.PropertyType].NullableUnderlyingType;
_emitGetConverterForNullablePropertyMethod |= nullableUnderlyingType != null;
+
converterInstantiationExpr = nullableUnderlyingType != null
- ? $"{GetConverterForNullablePropertyMethodName}<{nullableUnderlyingType.FullyQualifiedName}>(new {property.ConverterType.FullyQualifiedName}(), {OptionsLocalVariableName})"
- : $"({JsonConverterTypeRef}<{propertyTypeFQN}>){ExpandConverterMethodName}(typeof({propertyTypeFQN}), new {property.ConverterType.FullyQualifiedName}(), {OptionsLocalVariableName})";
+ ? $"{GetConverterForNullablePropertyMethodName}<{nullableUnderlyingType.FullyQualifiedName}>(new {converterFQN}(), {OptionsLocalVariableName})"
+ : $"({JsonConverterTypeRef}<{propertyTypeFQN}>){ExpandConverterMethodName}(typeof({propertyTypeFQN}), new {converterFQN}(), {OptionsLocalVariableName})";
}
writer.WriteLine($$"""
- var {{InfoVarName}}{{i}} = new {{JsonPropertyInfoValuesTypeRef}}<{{propertyTypeFQN}}>()
+ var {{InfoVarName}}{{i}} = new {{JsonPropertyInfoValuesTypeRef}}<{{propertyTypeFQN}}>
{
IsProperty = {{FormatBool(property.IsProperty)}},
IsPublic = {{FormatBool(property.IsPublic)}},
};
}
- private static void GenerateTypeInfoProperty(SourceWriter writer, TypeGenerationSpec typeMetadata, string metadataInitSource)
+ private static void GenerateTypeInfoFactoryHeader(SourceWriter writer, TypeGenerationSpec typeMetadata)
{
string typeFQN = typeMetadata.TypeRef.FullyQualifiedName;
string typeInfoPropertyName = typeMetadata.TypeInfoPropertyName;
""");
writer.Indentation += 2;
- writer.WriteLine(metadataInitSource);
+ }
+
+ private static void GenerateTypeInfoFactoryFooter(SourceWriter writer)
+ {
writer.Indentation -= 2;
// NB OriginatingResolver should be the last property set by the source generator.
}
{{JsonTypeInfoReturnValueLocalVariableName}}.{{OriginatingResolverPropertyName}} = this;
- return jsonTypeInfo;
+ return {{JsonTypeInfoReturnValueLocalVariableName}};
}
""");
}
}
else if (!foundDesignTimeCustomConverter && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType))
{
- converterType = GetConverterTypeFromAttribute(contextType, attributeData);
+ converterType = GetConverterTypeFromAttribute(contextType, type, attributeData);
foundDesignTimeCustomConverter = true;
}
out bool isRequired,
out bool canUseGetter,
out bool canUseSetter,
- out bool isJsonIncludeInaccessible,
+ out bool hasJsonIncludeButIsInaccessible,
out bool setterIsInitOnly);
- if (isJsonIncludeInaccessible)
+ if (hasJsonIncludeButIsInaccessible)
{
ReportDiagnostic(DiagnosticDescriptors.InaccessibleJsonIncludePropertiesNotSupported, memberInfo.GetDiagnosticLocation(), new string[] { declaringType.Name, memberInfo.Name });
}
- if ((!canUseGetter && !canUseSetter && !isJsonIncludeInaccessible) ||
+ if ((!canUseGetter && !canUseSetter && !hasJsonIncludeButIsInaccessible) ||
!IsSymbolAccessibleWithin(memberType, within: contextType))
{
// Skip the member if either of the two conditions hold
if (converterType is null && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType))
{
- converterType = GetConverterTypeFromAttribute(contextType, attributeData);
+ converterType = GetConverterTypeFromAttribute(contextType, memberInfo, attributeData);
}
else if (attributeType.ContainingAssembly.Name == SystemTextJsonNamespace)
{
out bool isRequired,
out bool canUseGetter,
out bool canUseSetter,
- out bool isJsonIncludeInaccessible,
+ out bool hasJsonIncludeButIsInaccessible,
out bool isSetterInitOnly)
{
isPublic = false;
isRequired = false;
canUseGetter = false;
canUseSetter = false;
- isJsonIncludeInaccessible = false;
+ hasJsonIncludeButIsInaccessible = false;
isSetterInitOnly = false;
switch (memberInfo)
}
else
{
- isJsonIncludeInaccessible = hasJsonInclude;
+ hasJsonIncludeButIsInaccessible = hasJsonInclude;
}
}
}
else
{
- isJsonIncludeInaccessible = hasJsonInclude;
+ hasJsonIncludeButIsInaccessible = hasJsonInclude;
}
}
else
else
{
// Unlike properties JsonIncludeAttribute is not supported for internal fields.
- isJsonIncludeInaccessible = hasJsonInclude;
+ hasJsonIncludeButIsInaccessible = hasJsonInclude;
}
}
break;
}
}
- private TypeRef? GetConverterTypeFromAttribute(INamedTypeSymbol contextType, AttributeData attributeData)
+ private TypeRef? GetConverterTypeFromAttribute(INamedTypeSymbol contextType, ISymbol declaringSymbol, AttributeData attributeData)
{
Debug.Assert(_knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeData.AttributeClass));
var converterType = (INamedTypeSymbol?)attributeData.ConstructorArguments[0].Value;
return null;
}
+ if (_knownSymbols.JsonStringEnumConverterType.IsAssignableFrom(converterType))
+ {
+ ReportDiagnostic(DiagnosticDescriptors.JsonStringEnumConverterNotSupportedInAot, declaringSymbol.GetDiagnosticLocation(), declaringSymbol.ToDisplayString());
+ }
+
return new TypeRef(converterType);
}
<data name="FastPathPolymorphismNotSupportedMessageFormat" xml:space="preserve">
<value>Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'.</value>
</data>
+ <data name="JsonStringEnumConverterNotSupportedTitle" xml:space="preserve">
+ <value>The non-generic 'JsonStringEnumConverter' requires dynamic code.</value>
+ </data>
+ <data name="JsonStringEnumConverterNotSupportedMessageFormat" xml:space="preserve">
+ <value>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</value>
+ </data>
</root>
<target state="translated">Deserializace vlastností pouze pro inicializaci se v současnosti v režimu generování zdroje nepodporuje.</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">Typ {0} má více konstruktorů anotovaných s JsonConstructorAttribute. </target>
<target state="translated">Die Deserialisierung von reinen init-Eigenschaften wird im Quellgenerierungsmodus derzeit nicht unterstützt.</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">Typ "{0}" weist mehrere Konstruktoren mit dem Kommentar "JsonConstructorAttribute" auf.</target>
<target state="translated">Actualmente no se admite la deserialización de propiedades de solo inicialización en el modo de generación de origen.</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">El tipo '{0}' tiene varios constructores anotados con 'JsonConstructorAttribute'.</target>
<target state="translated">La désérialisation des propriétés d’initialisation uniquement n’est actuellement pas prise en charge en mode de génération de source.</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">Le type' {0} 'a plusieurs constructeurs annotés avec’JsonConstructorAttribute'.</target>
<target state="translated">La deserializzazione delle proprietà di sola inizializzazione al momento non è supportata nella modalità di generazione di origine.</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">Il tipo '{0}' contiene più costruttori che presentano l'annotazione 'JsonConstructorAttribute'.</target>
<target state="translated">現在、ソース生成モードでは init-only プロパティの逆シリアル化はサポートされていません。</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">型 '{0}' には、'JsonConstructorAttribute' で注釈が付けられた複数のコンストラクターがあります。</target>
<target state="translated">초기화 전용 속성의 역직렬화는 현재 원본 생성 모드에서 지원되지 않습니다.</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">'{0}' 형식에 'JsonConstructorAttribute'로 주석이 추가된 여러 생성자가 있습니다.</target>
<target state="translated">Deserializacja właściwości tylko do inicjowania nie jest obecnie obsługiwana w trybie generowania źródła.</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">Typ "{0}" ma wiele konstruktorów z adnotacją "JsonConstructorAttribute".</target>
<target state="translated">A desserialização de propriedades apenas de inicialização não é atualmente suportada no modo de geração de origem.</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">O tipo '{0}' tem vários construtores anotados com 'JsonConstructorAttribute'.</target>
<target state="translated">Десериализация свойств, предназначенных только для инициализации, сейчас не поддерживается в режиме создания исходного кода.</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">Тип "{0}" имеет несколько конструкторов, аннотированных с использованием JsonConstructorAttribute.</target>
<target state="translated">Yalnızca başlangıç özelliklerini seri durumdan çıkarma şu anda kaynak oluşturma modunda desteklenmiyor.</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">'{0}' türünün 'JsonConstructorAttribute' ile açıklanan birden çok oluşturucusu var.</target>
<target state="translated">源生成模式当前不支持仅初始化属性的反序列化。</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">类型“{0}”具有用 “JsonConstructorAttribute” 批注的多个构造函数。</target>
<target state="translated">來源產生模式目前不支援 init-only 屬性的還原序列化。</target>
<note />
</trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+ <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</source>
+ <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead.</target>
+ <note />
+ </trans-unit>
+ <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+ <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+ <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+ <note />
+ </trans-unit>
<trans-unit id="MultipleJsonConstructorAttributeFormat">
<source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
<target state="translated">類型 '{0}' 包含多個以 'JsonConstructorAttribute' 註解的建構函式。</target>
public System.Text.Json.Serialization.JsonKnownNamingPolicy PropertyNamingPolicy { get { throw null; } set { } }
public bool WriteIndented { get { throw null; } set { } }
}
- [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JsonStringEnumConverter cannot be statically analyzed and requires runtime code generation. Consider authoring a custom converter that is not a factory to work around the issue. See https://github.com/dotnet/runtime/issues/73124.")]
+ [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JsonStringEnumConverter cannot be statically analyzed and requires runtime code generation. Applications should use the generic JsonStringEnumConverter<TEnum> instead.")]
public partial class JsonStringEnumConverter : System.Text.Json.Serialization.JsonConverterFactory
{
public JsonStringEnumConverter() { }
public sealed override bool CanConvert(System.Type typeToConvert) { throw null; }
public sealed override System.Text.Json.Serialization.JsonConverter CreateConverter(System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; }
}
+ public partial class JsonStringEnumConverter<TEnum> : System.Text.Json.Serialization.JsonConverterFactory where TEnum : struct, System.Enum
+ {
+ public JsonStringEnumConverter() { }
+ public JsonStringEnumConverter(System.Text.Json.JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true) { }
+ public sealed override bool CanConvert(System.Type typeToConvert) { throw null; }
+ public sealed override System.Text.Json.Serialization.JsonConverter CreateConverter(System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; }
+ }
public enum JsonUnknownDerivedTypeHandling
{
FailSerialization = 0,
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateStackInfo<TCollection>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo, System.Action<TCollection, object?> addFunc) where TCollection : System.Collections.IEnumerable { throw null; }
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateStackInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.Stack<TElement> { throw null; }
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> CreateValueInfo<T>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.JsonConverter converter) { throw null; }
- public static System.Text.Json.Serialization.JsonConverter<T> GetEnumConverter<T>(System.Text.Json.JsonSerializerOptions options) where T : struct { throw null; }
+ public static System.Text.Json.Serialization.JsonConverter<T> GetEnumConverter<T>(System.Text.Json.JsonSerializerOptions options) where T : struct, System.Enum { throw null; }
public static System.Text.Json.Serialization.JsonConverter<T?> GetNullableConverter<T>(System.Text.Json.JsonSerializerOptions options) where T : struct { throw null; }
public static System.Text.Json.Serialization.JsonConverter<T?> GetNullableConverter<T>(System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> underlyingTypeInfo) where T : struct { throw null; }
public static System.Text.Json.Serialization.JsonConverter<T> GetUnsupportedTypeConverter<T>() { throw null; }
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
+<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Suppression>
+ <DiagnosticId>CP0015</DiagnosticId>
+ <Target>T:System.Text.Json.Serialization.JsonStringEnumConverter:[T:System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute]</Target>
+ <Left>lib/net7.0/System.Text.Json.dll</Left>
+ <Right>lib/net7.0/System.Text.Json.dll</Right>
+ <IsBaselineSuppression>true</IsBaselineSuppression>
+ </Suppression>
+</Suppressions>
\ No newline at end of file
<data name="SerializerConverterFactoryReturnsJsonConverterFactory" xml:space="preserve">
<value>The converter '{0}' cannot return an instance of JsonConverterFactory.</value>
</data>
+ <data name="SerializerConverterFactoryInvalidArgument" xml:space="preserve">
+ <value>The type '{0}' is not supported by the current JsonConverterFactory.</value>
+ </data>
<data name="NodeElementWrongType" xml:space="preserve">
<value>The element must be of type '{0}'</value>
</data>
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace System.Text.Json.Serialization.Converters
}
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
- => Create(type, EnumConverterOptions.AllowNumbers, namingPolicy: null, options);
+ {
+ Debug.Assert(CanConvert(type));
+ return Create(type, EnumConverterOptions.AllowNumbers, namingPolicy: null, options);
+ }
internal static JsonConverter Create(Type enumType, EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOptions options)
{
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
- Debug.Assert(typeToConvert.GetGenericArguments().Length > 0);
+ Debug.Assert(typeToConvert.IsNullableOfT());
Type valueTypeToConvert = typeToConvert.GetGenericArguments()[0];
-
JsonConverter valueConverter = options.GetConverterInternal(valueTypeToConvert);
- Debug.Assert(valueConverter != null);
// If the value type has an interface or object converter, just return that converter directly.
if (!valueConverter.TypeToConvert.IsValueType && valueTypeToConvert.IsValueType)
public static JsonConverter CreateValueConverter(Type valueTypeToConvert, JsonConverter valueConverter)
{
+ Debug.Assert(valueTypeToConvert.IsValueType && !valueTypeToConvert.IsNullableOfT());
return (JsonConverter)Activator.CreateInstance(
GetNullableConverterType(valueTypeToConvert),
BindingFlags.Instance | BindingFlags.Public,
}
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
- => CreateUnsupportedConverterForType(type);
+ {
+ Debug.Assert(CanConvert(type));
+ return CreateUnsupportedConverterForType(type);
+ }
internal static JsonConverter CreateUnsupportedConverterForType(Type type, string? errorMessage = null)
{
/// <remarks>
/// Reading is case insensitive, writing can be customized via a <see cref="JsonNamingPolicy" />.
/// </remarks>
+ /// <typeparam name="TEnum">The enum type that this converter targets.</typeparam>
+ public class JsonStringEnumConverter<TEnum> : JsonConverterFactory
+ where TEnum : struct, Enum
+ {
+ private readonly JsonNamingPolicy? _namingPolicy;
+ private readonly EnumConverterOptions _converterOptions;
+
+ /// <summary>
+ /// Constructor. Creates the <see cref="JsonStringEnumConverter"/> with the
+ /// default naming policy and allows integer values.
+ /// </summary>
+ public JsonStringEnumConverter() : this(namingPolicy: null, allowIntegerValues: true)
+ {
+ // An empty constructor is needed for construction via attributes
+ }
+
+ /// <summary>
+ /// Constructor.
+ /// </summary>
+ /// <param name="namingPolicy">
+ /// Optional naming policy for writing enum values.
+ /// </param>
+ /// <param name="allowIntegerValues">
+ /// True to allow undefined enum values. When true, if an enum value isn't
+ /// defined it will output as a number rather than a string.
+ /// </param>
+ public JsonStringEnumConverter(JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true)
+ {
+ _namingPolicy = namingPolicy;
+ _converterOptions = allowIntegerValues
+ ? EnumConverterOptions.AllowNumbers | EnumConverterOptions.AllowStrings
+ : EnumConverterOptions.AllowStrings;
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TEnum);
+
+ /// <inheritdoc />
+ public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (typeToConvert != typeof(TEnum))
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException_JsonConverterFactory_TypeNotSupported(typeToConvert);
+ }
+
+ return new EnumConverter<TEnum>(_converterOptions, _namingPolicy, options);
+ }
+ }
+
+ /// <summary>
+ /// Converter to convert enums to and from strings.
+ /// </summary>
+ /// <remarks>
+ /// Reading is case insensitive, writing can be customized via a <see cref="JsonNamingPolicy" />.
+ /// </remarks>
[RequiresDynamicCode(
"JsonStringEnumConverter cannot be statically analyzed and requires runtime code generation. " +
- "Consider authoring a custom converter that is not a factory to work around the issue. " +
- "See https://github.com/dotnet/runtime/issues/73124.")]
+ "Applications should use the generic JsonStringEnumConverter<TEnum> instead.")]
public class JsonStringEnumConverter : JsonConverterFactory
{
private readonly JsonNamingPolicy? _namingPolicy;
/// Constructor. Creates the <see cref="JsonStringEnumConverter"/> with the
/// default naming policy and allows integer values.
/// </summary>
- public JsonStringEnumConverter()
- : this(namingPolicy: null, allowIntegerValues: true)
+ public JsonStringEnumConverter() : this(namingPolicy: null, allowIntegerValues: true)
{
// An empty constructor is needed for construction via attributes
}
}
/// <inheritdoc />
- public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) =>
- EnumConverterFactory.Create(typeToConvert, _converterOptions, _namingPolicy, options);
+ public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (!typeToConvert.IsEnum)
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException_JsonConverterFactory_TypeNotSupported(typeToConvert);
+ }
+
+ return EnumConverterFactory.Create(typeToConvert, _converterOptions, _namingPolicy, options);
+ }
}
}
}
[DoesNotReturn]
+ public static void ThrowArgumentOutOfRangeException_JsonConverterFactory_TypeNotSupported(Type typeToConvert)
+ {
+ throw new ArgumentOutOfRangeException(nameof(typeToConvert), SR.Format(SR.SerializerConverterFactoryInvalidArgument, typeToConvert.FullName));
+ }
+
+ [DoesNotReturn]
public static void ThrowArgumentException_ArrayTooSmall(string paramName)
{
throw new ArgumentException(SR.ArrayTooSmall, paramName);
}
[Fact]
- [ActiveIssue("https://github.com/dotnet/runtime/issues/79311", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))]
public async Task TestTypeWithEnumParameters()
{
// Regression test for https://github.com/dotnet/runtime/issues/68647
}
}
- [JsonConverter(typeof(JsonStringEnumConverter))]
+ [JsonConverter(typeof(JsonStringEnumConverter<MyEnum>))]
public enum MyEnum
{
One = 1,
=> new DictionaryWithPrivateKeyAndValueType { _values = { [PrivateEnum.A] = PrivateEnum.B, [PrivateEnum.B] = PrivateEnum.C } };
public void Validate() => Assert.Equal(new KeyValuePair<PrivateEnum, PrivateEnum>[] { new(PrivateEnum.A, PrivateEnum.B), new(PrivateEnum.B, PrivateEnum.C) }, this);
- public string GetExpectedJson() => """{"A":1,"B":2}"""; // cf. https://github.com/dotnet/runtime/issues/87129
+ public string GetExpectedJson() => """{"A":1,"B":2}""";
PrivateEnum IDictionary<PrivateEnum, PrivateEnum>.this[PrivateEnum key] { get => _values[key]; set => _values[key] = value; }
ICollection<PrivateEnum> IDictionary<PrivateEnum, PrivateEnum>.Keys => _values.Keys;
{
}
- // Regression test for https://github.com/dotnet/runtime/issues/61860
[Fact]
- [ActiveIssue("https://github.com/dotnet/runtime/issues/79311", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))]
public static void SupportsGenericParameterWithCustomConverterFactory()
{
+ // Regression test for https://github.com/dotnet/runtime/issues/61860
var value = new List<TestEnum> { TestEnum.Cee };
string json = JsonSerializer.Serialize(value, GenericParameterWithCustomConverterFactoryContext.Default.ListTestEnum);
Assert.Equal(@"[""Cee""]", json);
}
- // Regression test for https://github.com/dotnet/runtime/issues/74652
[Fact]
public static void ClassWithStringValuesRoundtrips()
{
+ // Regression test for https://github.com/dotnet/runtime/issues/74652
JsonSerializerOptions options = ClassWithStringValuesContext.Default.Options;
ClassWithStringValues obj = new()
Assert.Equal("""{"StringValuesProperty":["abc","def"]}""", json);
}
- // Regression test for https://github.com/dotnet/runtime/issues/61734
[Fact]
public static void ClassWithDictionaryPropertyRoundtrips()
{
+ // Regression test for https://github.com/dotnet/runtime/issues/61734
JsonSerializerOptions options = ClassWithDictionaryPropertyContext.Default.Options;
ClassWithDictionaryProperty obj = new(new Dictionary<string, object?>()
Assert.Equal("""{"DictionaryProperty":{"foo":"bar","test":"baz"}}""", json);
}
- [JsonConverter(typeof(JsonStringEnumConverter))]
+ [JsonConverter(typeof(JsonStringEnumConverter<TestEnum>))]
public enum TestEnum
{
Aye, Bee, Cee
public bool IsIncludeFieldsEnabled => GetType().GetCustomAttribute<JsonSourceGenerationOptionsAttribute>()?.IncludeFields ?? false;
}
- [JsonConverter(typeof(JsonStringEnumConverter))]
+ [JsonConverter(typeof(JsonStringEnumConverter<EnumWrittenAsString>))]
public enum EnumWrittenAsString
{
A = 1
}
[Fact]
- [ActiveIssue("https://github.com/dotnet/runtime/issues/79311", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))]
public void EnsureHelperMethodGenerated_TypeFactory()
{
// There are 2 helper methods generated for obtaining a converter from a factory:
}
[Fact]
- [ActiveIssue("https://github.com/dotnet/runtime/issues/79311", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))]
public void EnsureHelperMethodGenerated_ImplicitPropertyFactory()
{
// ContextWithImplicitStringEnum does not have an entry for EnumWrittenAsString since it is
<!-- SYSLIB1037: Suppress init-only property deserialization warning -->
<!-- SYSLIB1038: Suppress JsonInclude on inaccessible members warning -->
<!-- SYSLIB1039: Suppress Polymorphic types not supported warning -->
- <NoWarn>$(NoWarn);SYSLIB0020;SYSLIB0049;SYSLIB1037;SYSLIB1038;SYSLIB1039</NoWarn>
+ <!-- SYSLIB1040: Suppress JsonStringEnumConverter use warnings -->
+ <NoWarn>$(NoWarn);SYSLIB0020;SYSLIB0049;SYSLIB1037;SYSLIB1038;SYSLIB1039;SYSLIB1040</NoWarn>
<IgnoreForCI Condition="'$(TargetsMobile)' == 'true' or '$(TargetsLinuxBionic)' == 'true' or '$(TargetArchitecture)' == 'ARMv6'">true</IgnoreForCI>
</PropertyGroup>
public class ClassWithCustomConverterFactoryProperty
{
- [JsonConverter(typeof(JsonStringEnumConverter))] // This converter is a JsonConverterFactory
+ [JsonConverter(typeof(JsonStringEnumConverter<SourceGenSampleEnum>))] // This converter is a JsonConverterFactory
public SourceGenSampleEnum MyEnum { get; set; }
}
public struct StructWithCustomConverterFactoryProperty
{
- [JsonConverter(typeof(JsonStringEnumConverter))] // This converter is a JsonConverterFactory
+ [JsonConverter(typeof(JsonStringEnumConverter<SourceGenSampleEnum>))] // This converter is a JsonConverterFactory
public SourceGenSampleEnum MyEnum { get; set; }
}
public class ClassWithCustomConverterFactoryNullableProperty
{
- [JsonConverter(typeof(JsonStringEnumConverter))] // This converter is a JsonConverterFactory
+ [JsonConverter(typeof(JsonStringEnumConverter<SourceGenSampleEnum>))] // This converter is a JsonConverterFactory
public SourceGenSampleEnum? MyEnum { get; set; }
}
return CreateCompilation(source);
}
- internal static void CheckDiagnosticMessages(
- DiagnosticSeverity level,
- ImmutableArray<Diagnostic> diagnostics,
- (Location Location, string Message)[] expectedDiags,
- bool sort = true)
+ public static Compilation CreateTypesAnnotatedWithJsonStringEnumConverter()
{
- ((string FileName, TextSpan, LinePositionSpan), string)[] actualDiags = diagnostics
- .Where(diagnostic => diagnostic.Severity == level)
- .Select(diagnostic => (GetLocationNormalForm(diagnostic.Location), diagnostic.GetMessage()))
- .ToArray();
-
- if (CultureInfo.CurrentUICulture.Name.StartsWith("en", StringComparison.OrdinalIgnoreCase))
- {
- ((string FileName, TextSpan, LinePositionSpan), string Message)[] expectedDiagsNormalized = expectedDiags
- .Select(diag => (GetLocationNormalForm(diag.Location), diag.Message))
- .ToArray();
+ string source = """
+ using System.Text.Json.Serialization;
- if (sort)
+ namespace HelloWorld
{
- // Can't depend on reflection order when generating type metadata.
- Array.Sort(actualDiags);
- Array.Sort(expectedDiagsNormalized);
+ [JsonSerializable(typeof(MyClass))]
+ internal partial class JsonContext : JsonSerializerContext
+ {
+ }
+
+ public class MyClass
+ {
+ public Enum1 Enum1Prop { get; set; }
+
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public Enum2 Enum2Prop { get; set; }
+ }
+
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum Enum1 { A, B, C };
+
+ public enum Enum2 { A, B, C };
}
+ """;
- Assert.Equal(expectedDiagsNormalized, actualDiags);
- }
- else
- {
- // for non-English runs, just compare the number of messages are the same
- Assert.Equal(expectedDiags.Length, actualDiags.Length);
- }
+ return CreateCompilation(source);
+ }
- static (string FileName, TextSpan, LinePositionSpan) GetLocationNormalForm(Location location)
- => (location.SourceTree?.FilePath ?? "", location.SourceSpan, location.GetLineSpan().Span);
+ internal static void AssertEqualDiagnosticMessages(
+ IEnumerable<DiagnosticData> expectedDiags,
+ IEnumerable<Diagnostic> actualDiags)
+ {
+ HashSet<DiagnosticData> expectedSet = new(expectedDiags);
+ HashSet<DiagnosticData> actualSet = new(actualDiags.Select(d => new DiagnosticData(d.Severity, d.Location, d.GetMessage())));
+ AssertExtensions.Equal(expectedSet, actualSet);
}
}
+
+ public record struct DiagnosticData(
+ DiagnosticSeverity Severity,
+ string FilePath,
+ LinePositionSpan LinePositionSpan,
+ string Message)
+ {
+ public DiagnosticData(DiagnosticSeverity severity, Location location, string message)
+ : this(severity, location.SourceTree?.FilePath ?? "", location.GetLineSpan().Span, TrimCultureSensitiveMessage(message))
+ {
+ }
+
+ // for non-English runs, trim the message content since it might be translated.
+ private static string TrimCultureSensitiveMessage(string message) => s_IsEnglishCulture ? message : "";
+ private readonly static bool s_IsEnglishCulture = CultureInfo.CurrentUICulture.Name.StartsWith("en", StringComparison.OrdinalIgnoreCase);
+ public override string ToString() => $"{Severity}, {Message}, {FilePath}@{LinePositionSpan}";
+ }
}
using var emitStream = new MemoryStream();
using var xmlStream = new MemoryStream();
var result = sourceGenResult.NewCompilation.Emit(emitStream, xmlDocumentationStream: xmlStream);
- var diagnostics = result.Diagnostics;
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
""";
MetadataReference[] additionalReferences = {
- MetadataReference.CreateFromImage(campaignImage),
- MetadataReference.CreateFromImage(eventImage),
- };
+ MetadataReference.CreateFromImage(campaignImage),
+ MetadataReference.CreateFromImage(eventImage),
+ };
Compilation compilation = CompilationHelper.CreateCompilation(source, additionalReferences);
-
JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
-
- Location location;
- if (explicitRef)
- {
- // Unsupported type is not in compiling assembly, but is indicated directly with [JsonSerializable], so location points to attribute application.
- INamedTypeSymbol symbol = (INamedTypeSymbol)compilation.GetSymbolsWithName("JsonContext").FirstOrDefault();
- SyntaxReference syntaxReference = symbol.GetAttributes().First().ApplicationSyntaxReference;
- TextSpan textSpan = syntaxReference.Span;
- location = syntaxReference.SyntaxTree.GetLocation(textSpan)!;
- }
- else
- {
- // Unsupported type is not in compiling assembly, and isn't indicated directly with [JsonSerializable], so location points to context type.
- location = compilation.GetSymbolsWithName("JsonContext").First().Locations[0];
- }
-
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
RunTest(explicitRef: true);
TextSpan textSpan = syntaxReference.Span;
Location location = syntaxReference.SyntaxTree.GetLocation(textSpan)!;
- (Location, string)[] expectedWarningDiagnostics = new (Location, string)[]
+ var expectedDiagnostics = new DiagnosticData[]
{
- (location, "There are multiple types named Location. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision.")
+ new(DiagnosticSeverity.Warning, location, "There are multiple types named Location. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision.")
};
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, expectedWarningDiagnostics);
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ CompilationHelper.AssertEqualDiagnosticMessages(expectedDiagnostics, result.Diagnostics);
// With resolution.
compilation = CompilationHelper.CreateRepeatedLocationsWithResolutionCompilation();
result = CompilationHelper.RunJsonSourceGenerator(compilation);
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
Compilation compilation = CompilationHelper.CreateCompilation(source);
JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
// With STJ usage.
source = """
compilation = CompilationHelper.CreateCompilation(source);
result = CompilationHelper.RunJsonSourceGenerator(compilation);
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
Compilation compilation = CompilationHelper.CreateCompilationWithInitOnlyProperties();
JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
Compilation compilation = CompilationHelper.CreateCompilationWithConstructorInitOnlyProperties();
JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
Compilation compilation = CompilationHelper.CreateCompilationWithMixedInitOnlyProperties();
JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
Compilation compilation = CompilationHelper.CreateCompilationWithRecordPositionalParameters();
JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
Compilation compilation = CompilationHelper.CreateCompilationWithRequiredProperties();
JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
Location internalFieldLocation = compilation.GetSymbolsWithName("internalField").First().Locations[0];
Location privateFieldLocation = compilation.GetSymbolsWithName("privateField").First().Locations[0];
- (Location, string)[] expectedWarningDiagnostics = new (Location, string)[]
+ var expectedDiagnostics = new DiagnosticData[]
{
- (idLocation, "The member 'Location.Id' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
- (address2Location, "The member 'Location.Address2' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
- (countryLocation, "The member 'Location.Country' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
- (internalFieldLocation, "The member 'Location.internalField' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
- (privateFieldLocation, "The member 'Location.privateField' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
+ new(DiagnosticSeverity.Warning, idLocation, "The member 'Location.Id' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
+ new(DiagnosticSeverity.Warning, address2Location, "The member 'Location.Address2' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
+ new(DiagnosticSeverity.Warning, countryLocation, "The member 'Location.Country' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
+ new(DiagnosticSeverity.Warning, internalFieldLocation, "The member 'Location.internalField' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
+ new(DiagnosticSeverity.Warning, privateFieldLocation, "The member 'Location.privateField' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
};
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, expectedWarningDiagnostics, sort: false);
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ CompilationHelper.AssertEqualDiagnosticMessages(expectedDiagnostics, result.Diagnostics);
}
[Fact]
Location myBaseClassLocation = compilation.GetSymbolsWithName("MyBaseClass").First().Locations[0];
- (Location, string)[] expectedWarningDiagnostics = new (Location, string)[]
+ var expectedDiagnostics = new DiagnosticData[]
+ {
+ new(DiagnosticSeverity.Warning, myBaseClassLocation, "Type 'HelloWorld.MyBaseClass' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'."),
+ };
+
+ CompilationHelper.AssertEqualDiagnosticMessages(expectedDiagnostics, result.Diagnostics);
+ }
+
+ [Fact]
+ public void JsonStringEnumConverterWarns()
+ {
+ Compilation compilation = CompilationHelper.CreateTypesAnnotatedWithJsonStringEnumConverter();
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
+
+ Location enum2PropLocation = compilation.GetSymbolsWithName("Enum2Prop").First().Locations[0];
+ Location enum1TypeLocation = compilation.GetSymbolsWithName("Enum1").First().Locations[0];
+
+ var expectedDiagnostics = new DiagnosticData[]
{
- (myBaseClassLocation, "Type 'HelloWorld.MyBaseClass' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'."),
+ new(DiagnosticSeverity.Warning, enum1TypeLocation, "The member 'HelloWorld.Enum1' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead."),
+ new(DiagnosticSeverity.Warning, enum2PropLocation, "The member 'HelloWorld.MyClass.Enum2Prop' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead."),
};
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, expectedWarningDiagnostics, sort: false);
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ CompilationHelper.AssertEqualDiagnosticMessages(expectedDiagnostics, result.Diagnostics);
}
}
}
Assert.Empty(result.AllGeneratedTypes);
}
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Theory]
JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
Assert.Empty(result.AllGeneratedTypes);
-
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
ImmutableArray<Diagnostic> generatorDiags = result.NewCompilation.GetDiagnostics();
// No diagnostics expected.
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
ImmutableArray<Diagnostic> generatorDiags = result.NewCompilation.GetDiagnostics();
// No diagnostics expected.
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
ImmutableArray<Diagnostic> generatorDiags = result.NewCompilation.GetDiagnostics();
// No diagnostics expected.
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>());
+ Assert.Empty(result.Diagnostics);
}
[Fact]
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
+using System.Reflection;
using Xunit;
namespace System.Text.Json.Serialization.Tests
public ICollection<InvalidTypeConverterEnum> MyEnumValues { get; set; }
}
+ private class InvalidEnumTypeConverterClass
+ {
+ [JsonConverter(typeof(JsonStringEnumConverter<BindingFlags>))]
+ public InvalidTypeConverterEnum MyEnumValues { get; set; }
+ }
+
private enum InvalidTypeConverterEnum
{
Value1,
Value2,
}
- [Fact]
- public static void AttributeOnPropertyFail()
+ [Theory]
+ [InlineData(typeof(InvalidTypeConverterClass),
+ "System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClass.MyEnumValues",
+ "System.Collections.Generic.ICollection`1[System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterEnum]")]
+ [InlineData(typeof(InvalidEnumTypeConverterClass),
+ "System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidEnumTypeConverterClass.MyEnumValues",
+ "System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterEnum")]
+ public static void AttributeOnPropertyFail(Type type, string propertyName, string propertyTypeName)
{
+ object value = Activator.CreateInstance(type);
InvalidOperationException ex;
- ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new InvalidTypeConverterClass()));
+ ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(value, type));
// Message should be in the form "The converter specified on 'System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClass.MyEnumValues' is not compatible with the type 'System.Collections.Generic.ICollection`1[System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterEnum]'."
- Assert.Contains("'System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClass.MyEnumValues'", ex.Message);
+ Assert.Contains($"'{propertyName}'", ex.Message);
- ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<InvalidTypeConverterClass>("{}"));
- Assert.Contains("'System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClass.MyEnumValues'", ex.Message);
- Assert.Contains("'System.Collections.Generic.ICollection`1[System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterEnum]'", ex.Message);
+ ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize("{}", type));
+ Assert.Contains($"'{propertyName}'", ex.Message);
+ Assert.Contains($"'{propertyTypeName}'", ex.Message);
}
[JsonConverter(typeof(JsonStringEnumConverter))]
private class InvalidTypeConverterClassWithAttribute { }
- [Fact]
- public static void AttributeOnClassFail()
- {
- const string expectedSubStr = "'System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClassWithAttribute'";
+ [JsonConverter(typeof(JsonStringEnumConverter<BindingFlags>))]
+ private enum InvalidTypeConverterEnumWithAttribute { }
+ [Theory]
+ [InlineData(typeof(InvalidTypeConverterClassWithAttribute), "System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClassWithAttribute")]
+ [InlineData(typeof(InvalidTypeConverterEnumWithAttribute), "System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterEnumWithAttribute")]
+ public static void AttributeOnClassFail(Type type, string expectedSubStr)
+ {
InvalidOperationException ex;
+ object value = Activator.CreateInstance(type);
- ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new InvalidTypeConverterClassWithAttribute()));
- // Message should be in the form "The converter specified on 'System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClassWithAttribute' is not compatible with the type 'System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClassWithAttribute'."
+ ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(value, type));
int pos = ex.Message.IndexOf(expectedSubStr);
Assert.True(pos > 0);
Assert.Contains(expectedSubStr, ex.Message.Substring(pos + expectedSubStr.Length)); // The same string is repeated again.
- ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<InvalidTypeConverterClassWithAttribute>("{}"));
+ ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize("{}", type));
pos = ex.Message.IndexOf(expectedSubStr);
Assert.True(pos > 0);
Assert.Contains(expectedSubStr, ex.Message.Substring(pos + expectedSubStr.Length));
{
public class EnumConverterTests
{
- [Fact]
- public void ConvertDayOfWeek()
+ [Theory]
+ [InlineData(typeof(JsonStringEnumConverter), typeof(DayOfWeek))]
+ [InlineData(typeof(JsonStringEnumConverter), typeof(MyCustomEnum))]
+ [InlineData(typeof(JsonStringEnumConverter<DayOfWeek>), typeof(DayOfWeek))]
+ [InlineData(typeof(JsonStringEnumConverter<MyCustomEnum>), typeof(MyCustomEnum))]
+ public static void JsonStringEnumConverter_SupportedType_WorksAsExpected(Type converterType, Type supportedType)
+ {
+ var options = new JsonSerializerOptions();
+ var factory = (JsonConverterFactory)Activator.CreateInstance(converterType);
+
+ Assert.True(factory.CanConvert(supportedType));
+
+ JsonConverter converter = factory.CreateConverter(supportedType, options);
+ // TODO use https://github.com/dotnet/runtime/issues/63898 once implemented
+ Type expectedConverterType = typeof(JsonConverter<>).MakeGenericType(supportedType);
+ Assert.IsAssignableFrom(expectedConverterType, converter);
+ }
+
+ [Theory]
+ [InlineData(typeof(JsonStringEnumConverter), typeof(int))]
+ [InlineData(typeof(JsonStringEnumConverter), typeof(string))]
+ [InlineData(typeof(JsonStringEnumConverter), typeof(JsonStringEnumConverter))]
+ [InlineData(typeof(JsonStringEnumConverter<DayOfWeek>), typeof(int))]
+ [InlineData(typeof(JsonStringEnumConverter<DayOfWeek>), typeof(string))]
+ [InlineData(typeof(JsonStringEnumConverter<DayOfWeek>), typeof(JsonStringEnumConverter<MyCustomEnum>))]
+ [InlineData(typeof(JsonStringEnumConverter<DayOfWeek>), typeof(MyCustomEnum))]
+ [InlineData(typeof(JsonStringEnumConverter<MyCustomEnum>), typeof(DayOfWeek))]
+ public static void JsonStringEnumConverter_InvalidType_ThrowsArgumentOutOfRangeException(Type converterType, Type unsupportedType)
+ {
+ var options = new JsonSerializerOptions();
+ var factory = (JsonConverterFactory)Activator.CreateInstance(converterType);
+
+ Assert.False(factory.CanConvert(unsupportedType));
+ ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(() => factory.CreateConverter(unsupportedType, options));
+ Assert.Contains(unsupportedType.FullName, ex.Message);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void ConvertDayOfWeek(bool useGenericVariant)
{
- JsonSerializerOptions options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter());
+ JsonSerializerOptions options = CreateStringEnumOptionsForType<DayOfWeek>(useGenericVariant);
WhenClass when = JsonSerializer.Deserialize<WhenClass>(@"{""Day"":""Monday""}", options);
Assert.Equal(DayOfWeek.Monday, when.Day);
Assert.Equal(@"""Friday""", json);
// Try a unique naming policy
- options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter(new ToLowerNamingPolicy()));
+ options = CreateStringEnumOptionsForType<DayOfWeek>(useGenericVariant, new ToLowerNamingPolicy());
json = JsonSerializer.Serialize(DayOfWeek.Friday, options);
Assert.Equal(@"""friday""", json);
Assert.Equal(@"-1", json);
// Not permitting integers should throw
- options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: false));
+ options = CreateStringEnumOptionsForType<DayOfWeek>(useGenericVariant, allowIntegerValues: false);
Assert.Throws<JsonException>(() => JsonSerializer.Serialize((DayOfWeek)(-1), options));
}
public DayOfWeek Day { get; set; }
}
- [Fact]
- public void ConvertFileAttributes()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void ConvertFileAttributes(bool useGenericVariant)
{
- JsonSerializerOptions options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter());
+ JsonSerializerOptions options = CreateStringEnumOptionsForType<FileAttributes>(useGenericVariant);
FileState state = JsonSerializer.Deserialize<FileState>(@"{""Attributes"":""ReadOnly""}", options);
Assert.Equal(FileAttributes.ReadOnly, state.Attributes);
Assert.Equal(@"""Temporary, Offline""", json);
// Try a unique casing
- options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter(new ToLowerNamingPolicy()));
+ options = CreateStringEnumOptionsForType<FileAttributes>(useGenericVariant, new ToLowerNamingPolicy());
json = JsonSerializer.Serialize(FileAttributes.NoScrubData, options);
Assert.Equal(@"""noscrubdata""", json);
Assert.Equal(@"-1", json);
// Not permitting integers should throw
- options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: false));
+ options = CreateStringEnumOptionsForType<FileAttributes>(useGenericVariant, allowIntegerValues: false);
Assert.Throws<JsonException>(() => JsonSerializer.Serialize((FileAttributes)(-1), options));
// Flag values honor naming policy correctly
- options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter(new SimpleSnakeCasePolicy()));
+ options = CreateStringEnumOptionsForType<FileAttributes>(useGenericVariant, new SimpleSnakeCasePolicy());
json = JsonSerializer.Serialize(
FileAttributes.Directory | FileAttributes.Compressed | FileAttributes.IntegrityStream,
public DayOfWeek WorkEnd { get; set; }
[JsonConverter(typeof(LowerCaseEnumConverter))]
public DayOfWeek WeekEnd { get; set; }
+
+ [JsonConverter(typeof(JsonStringEnumConverter<DayOfWeek>))]
+ public DayOfWeek WorkStart2 { get; set; }
+ [JsonConverter(typeof(LowerCaseEnumConverter<DayOfWeek>))]
+ public DayOfWeek WeekEnd2 { get; set; }
}
private class LowerCaseEnumConverter : JsonStringEnumConverter
}
}
+ private class LowerCaseEnumConverter<TEnum> : JsonStringEnumConverter<TEnum>
+ where TEnum : struct, Enum
+ {
+ public LowerCaseEnumConverter() : base(new ToLowerNamingPolicy())
+ {
+ }
+ }
+
[Fact]
public void ConvertEnumUsingAttributes()
{
- Week week = new Week { WorkStart = DayOfWeek.Monday, WorkEnd = DayOfWeek.Friday, WeekEnd = DayOfWeek.Saturday };
+ Week week = new Week {
+ WorkStart = DayOfWeek.Monday,
+ WorkEnd = DayOfWeek.Friday,
+ WeekEnd = DayOfWeek.Saturday,
+ WorkStart2 = DayOfWeek.Tuesday,
+ WeekEnd2 = DayOfWeek.Thursday,
+ };
+
string json = JsonSerializer.Serialize(week);
- Assert.Equal(@"{""WorkStart"":""Monday"",""WorkEnd"":5,""WeekEnd"":""saturday""}", json);
+ Assert.Equal("""{"WorkStart":"Monday","WorkEnd":5,"WeekEnd":"saturday","WorkStart2":"Tuesday","WeekEnd2":"thursday"}""", json);
week = JsonSerializer.Deserialize<Week>(json);
Assert.Equal(DayOfWeek.Monday, week.WorkStart);
Assert.Equal(DayOfWeek.Friday, week.WorkEnd);
Assert.Equal(DayOfWeek.Saturday, week.WeekEnd);
+ Assert.Equal(DayOfWeek.Tuesday, week.WorkStart2);
+ Assert.Equal(DayOfWeek.Thursday, week.WeekEnd2);
}
[Fact]
Second = 2
}
- [Fact]
- public void EnumWithConverterAttribute()
+ [JsonConverter(typeof(JsonStringEnumConverter<MyCustomEnum2>))]
+ private enum MyCustomEnum2
{
- string json = JsonSerializer.Serialize(MyCustomEnum.Second);
- Assert.Equal(@"""Second""", json);
+ First = 1,
+ Second = 2
+ }
- MyCustomEnum obj = JsonSerializer.Deserialize<MyCustomEnum>("\"Second\"");
- Assert.Equal(MyCustomEnum.Second, obj);
+ [Theory]
+ [InlineData(typeof(MyCustomEnum), MyCustomEnum.Second, "\"Second\"", "2")]
+ [InlineData(typeof(MyCustomEnum2), MyCustomEnum2.Second, "\"Second\"", "2")]
+ public void EnumWithConverterAttribute(Type enumType, object value, string expectedJson, string alternativeJson)
+ {
+ string json = JsonSerializer.Serialize(value, enumType);
+ Assert.Equal(expectedJson, json);
+
+ object? result = JsonSerializer.Deserialize(json, enumType);
+ Assert.Equal(value, result);
- obj = JsonSerializer.Deserialize<MyCustomEnum>("2");
- Assert.Equal(MyCustomEnum.Second, obj);
+ result = JsonSerializer.Deserialize(alternativeJson, enumType);
+ Assert.Equal(value, result);
}
- [Fact]
- public static void EnumWithNoValues()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public static void EnumWithNoValues(bool useGenericVariant)
{
- var options = new JsonSerializerOptions
- {
- Converters = { new JsonStringEnumConverter() }
- };
+ JsonSerializerOptions options = CreateStringEnumOptionsForType<EmptyEnum>(useGenericVariant);
Assert.Equal("-1", JsonSerializer.Serialize((EmptyEnum)(-1), options));
Assert.Equal("1", JsonSerializer.Serialize((EmptyEnum)(1), options));
public enum EmptyEnum { };
- [Fact]
- public static void MoreThan64EnumValuesToSerialize()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public static void MoreThan64EnumValuesToSerialize(bool useGenericVariant)
{
- var options = new JsonSerializerOptions
- {
- Converters = { new JsonStringEnumConverter() }
- };
+ JsonSerializerOptions options = CreateStringEnumOptionsForType<MyEnum>(useGenericVariant);
for (int i = 0; i < 128; i++)
{
}
}
- [Fact]
- public static void MoreThan64EnumValuesToSerializeWithNamingPolicy()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public static void MoreThan64EnumValuesToSerializeWithNamingPolicy(bool useGenericVariant)
{
- var options = new JsonSerializerOptions
- {
- Converters = { new JsonStringEnumConverter(new ToLowerNamingPolicy()) }
- };
+ JsonSerializerOptions options = CreateStringEnumOptionsForType<MyEnum>(useGenericVariant, new ToLowerNamingPolicy());
for (int i = 0; i < 128; i++)
{
G = 1 << 6,
}
- [Fact]
- public static void Honor_EnumNamingPolicy_On_Deserialization()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public static void Honor_EnumNamingPolicy_On_Deserialization(bool useGenericVariant)
{
- JsonSerializerOptions options = new()
- {
- Converters =
- {
- new JsonStringEnumConverter(namingPolicy: new SimpleSnakeCasePolicy() )
- }
- };
+ JsonSerializerOptions options = CreateStringEnumOptionsForType<BindingFlags>(useGenericVariant, new SimpleSnakeCasePolicy());
BindingFlags bindingFlags = JsonSerializer.Deserialize<BindingFlags>(@"""non_public""", options);
Assert.Equal(BindingFlags.NonPublic, bindingFlags);
Assert.Null(JsonSerializer.Deserialize<BindingFlags?>("null", options));
}
- [Fact]
- public static void EnumDictionaryKeyDeserialization()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public static void EnumDictionaryKeyDeserialization(bool useGenericVariant)
{
JsonNamingPolicy snakeCasePolicy = new SimpleSnakeCasePolicy();
- JsonSerializerOptions options = new()
- {
- Converters =
- {
- new JsonStringEnumConverter(namingPolicy: snakeCasePolicy)
- },
- DictionaryKeyPolicy = snakeCasePolicy
- };
+ JsonSerializerOptions options = CreateStringEnumOptionsForType<BindingFlags>(useGenericVariant);
+ options.DictionaryKeyPolicy = snakeCasePolicy;
// Baseline.
var dict = JsonSerializer.Deserialize<Dictionary<BindingFlags, int>>(@"{""NonPublic, Public"": 1}", options);
{
public override string ConvertName(string name) => name + "0";
}
+
+ private static JsonSerializerOptions CreateStringEnumOptionsForType<TEnum>(bool useGenericVariant, JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true) where TEnum : struct, Enum
+ {
+ return new JsonSerializerOptions
+ {
+ Converters =
+ {
+ useGenericVariant
+ ? new JsonStringEnumConverter<TEnum>(namingPolicy, allowIntegerValues)
+ : new JsonStringEnumConverter(namingPolicy, allowIntegerValues)
+ }
+ };
+ }
}
}
<Left>net7.0/System.Runtime.Loader.dll</Left>
<Right>net8.0/System.Runtime.Loader.dll</Right>
</Suppression>
+ <Suppression>
+ <DiagnosticId>CP0015</DiagnosticId>
+ <Target>T:System.Text.Json.Serialization.JsonStringEnumConverter:[T:System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute]</Target>
+ <Left>net7.0/System.Text.Json.dll</Left>
+ <Right>net8.0/System.Text.Json.dll</Right>
+ </Suppression>
</Suppressions>
\ No newline at end of file